code.gitea.io/gitea@v1.19.3/modules/process/manager.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package process
     6  
     7  import (
     8  	"context"
     9  	"log"
    10  	"runtime/pprof"
    11  	"strconv"
    12  	"sync"
    13  	"time"
    14  )
    15  
    16  // TODO: This packages still uses a singleton for the Manager.
    17  // Once there's a decent web framework and dependencies are passed around like they should,
    18  // then we delete the singleton.
    19  
    20  var (
    21  	manager     *Manager
    22  	managerInit sync.Once
    23  
    24  	// DefaultContext is the default context to run processing commands in
    25  	DefaultContext = context.Background()
    26  )
    27  
    28  // DescriptionPProfLabel is a label set on goroutines that have a process attached
    29  const DescriptionPProfLabel = "process-description"
    30  
    31  // PIDPProfLabel is a label set on goroutines that have a process attached
    32  const PIDPProfLabel = "pid"
    33  
    34  // PPIDPProfLabel is a label set on goroutines that have a process attached
    35  const PPIDPProfLabel = "ppid"
    36  
    37  // ProcessTypePProfLabel is a label set on goroutines that have a process attached
    38  const ProcessTypePProfLabel = "process-type"
    39  
    40  // IDType is a pid type
    41  type IDType string
    42  
    43  // FinishedFunc is a function that marks that the process is finished and can be removed from the process table
    44  // - it is simply an alias for context.CancelFunc and is only for documentary purposes
    45  type FinishedFunc = context.CancelFunc
    46  
    47  var Trace = defaultTrace // this global can be overridden by particular logging packages - thus avoiding import cycles
    48  
    49  func defaultTrace(start bool, pid IDType, description string, parentPID IDType, typ string) {
    50  	if start && parentPID != "" {
    51  		log.Printf("start process %s: %s (from %s) (%s)", pid, description, parentPID, typ)
    52  	} else if start {
    53  		log.Printf("start process %s: %s (%s)", pid, description, typ)
    54  	} else {
    55  		log.Printf("end process %s: %s", pid, description)
    56  	}
    57  }
    58  
    59  // Manager manages all processes and counts PIDs.
    60  type Manager struct {
    61  	mutex sync.Mutex
    62  
    63  	next     int64
    64  	lastTime int64
    65  
    66  	processMap map[IDType]*process
    67  }
    68  
    69  // GetManager returns a Manager and initializes one as singleton if there's none yet
    70  func GetManager() *Manager {
    71  	managerInit.Do(func() {
    72  		manager = &Manager{
    73  			processMap: make(map[IDType]*process),
    74  			next:       1,
    75  		}
    76  	})
    77  	return manager
    78  }
    79  
    80  // AddContext creates a new context and adds it as a process. Once the process is finished, finished must be called
    81  // to remove the process from the process table. It should not be called until the process is finished but must always be called.
    82  //
    83  // cancel should be used to cancel the returned context, however it will not remove the process from the process table.
    84  // finished will cancel the returned context and remove it from the process table.
    85  //
    86  // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the
    87  // process table.
    88  func (pm *Manager) AddContext(parent context.Context, description string) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) {
    89  	ctx, cancel = context.WithCancel(parent)
    90  
    91  	ctx, _, finished = pm.Add(ctx, description, cancel, NormalProcessType, true)
    92  
    93  	return ctx, cancel, finished
    94  }
    95  
    96  // AddTypedContext creates a new context and adds it as a process. Once the process is finished, finished must be called
    97  // to remove the process from the process table. It should not be called until the process is finished but must always be called.
    98  //
    99  // cancel should be used to cancel the returned context, however it will not remove the process from the process table.
   100  // finished will cancel the returned context and remove it from the process table.
   101  //
   102  // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the
   103  // process table.
   104  func (pm *Manager) AddTypedContext(parent context.Context, description, processType string, currentlyRunning bool) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) {
   105  	ctx, cancel = context.WithCancel(parent)
   106  
   107  	ctx, _, finished = pm.Add(ctx, description, cancel, processType, currentlyRunning)
   108  
   109  	return ctx, cancel, finished
   110  }
   111  
   112  // AddContextTimeout creates a new context and add it as a process. Once the process is finished, finished must be called
   113  // to remove the process from the process table. It should not be called until the process is finished but must always be called.
   114  //
   115  // cancel should be used to cancel the returned context, however it will not remove the process from the process table.
   116  // finished will cancel the returned context and remove it from the process table.
   117  //
   118  // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the
   119  // process table.
   120  func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Duration, description string) (ctx context.Context, cancel context.CancelFunc, finshed FinishedFunc) {
   121  	if timeout <= 0 {
   122  		// it's meaningless to use timeout <= 0, and it must be a bug! so we must panic here to tell developers to make the timeout correct
   123  		panic("the timeout must be greater than zero, otherwise the context will be cancelled immediately")
   124  	}
   125  
   126  	ctx, cancel = context.WithTimeout(parent, timeout)
   127  
   128  	ctx, _, finshed = pm.Add(ctx, description, cancel, NormalProcessType, true)
   129  
   130  	return ctx, cancel, finshed
   131  }
   132  
   133  // Add create a new process
   134  func (pm *Manager) Add(ctx context.Context, description string, cancel context.CancelFunc, processType string, currentlyRunning bool) (context.Context, IDType, FinishedFunc) {
   135  	parentPID := GetParentPID(ctx)
   136  
   137  	pm.mutex.Lock()
   138  	start, pid := pm.nextPID()
   139  
   140  	parent := pm.processMap[parentPID]
   141  	if parent == nil {
   142  		parentPID = ""
   143  	}
   144  
   145  	process := &process{
   146  		PID:         pid,
   147  		ParentPID:   parentPID,
   148  		Description: description,
   149  		Start:       start,
   150  		Cancel:      cancel,
   151  		Type:        processType,
   152  	}
   153  
   154  	var finished FinishedFunc
   155  	if currentlyRunning {
   156  		finished = func() {
   157  			cancel()
   158  			pm.remove(process)
   159  			pprof.SetGoroutineLabels(ctx)
   160  		}
   161  	} else {
   162  		finished = func() {
   163  			cancel()
   164  			pm.remove(process)
   165  		}
   166  	}
   167  
   168  	pm.processMap[pid] = process
   169  	pm.mutex.Unlock()
   170  	Trace(true, pid, description, parentPID, processType)
   171  
   172  	pprofCtx := pprof.WithLabels(ctx, pprof.Labels(DescriptionPProfLabel, description, PPIDPProfLabel, string(parentPID), PIDPProfLabel, string(pid), ProcessTypePProfLabel, processType))
   173  	if currentlyRunning {
   174  		pprof.SetGoroutineLabels(pprofCtx)
   175  	}
   176  
   177  	return &Context{
   178  		Context: pprofCtx,
   179  		pid:     pid,
   180  	}, pid, finished
   181  }
   182  
   183  // nextPID will return the next available PID. pm.mutex should already be locked.
   184  func (pm *Manager) nextPID() (start time.Time, pid IDType) {
   185  	start = time.Now()
   186  	startUnix := start.Unix()
   187  	if pm.lastTime == startUnix {
   188  		pm.next++
   189  	} else {
   190  		pm.next = 1
   191  	}
   192  	pm.lastTime = startUnix
   193  	pid = IDType(strconv.FormatInt(start.Unix(), 16))
   194  
   195  	if pm.next == 1 {
   196  		return
   197  	}
   198  	pid = IDType(string(pid) + "-" + strconv.FormatInt(pm.next, 10))
   199  	return start, pid
   200  }
   201  
   202  func (pm *Manager) remove(process *process) {
   203  	pm.mutex.Lock()
   204  	defer pm.mutex.Unlock()
   205  	if p := pm.processMap[process.PID]; p == process {
   206  		delete(pm.processMap, process.PID)
   207  		Trace(false, process.PID, process.Description, process.ParentPID, process.Type)
   208  	}
   209  }
   210  
   211  // Cancel a process in the ProcessManager.
   212  func (pm *Manager) Cancel(pid IDType) {
   213  	pm.mutex.Lock()
   214  	process, ok := pm.processMap[pid]
   215  	pm.mutex.Unlock()
   216  	if ok && process.Type != SystemProcessType {
   217  		process.Cancel()
   218  	}
   219  }