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 }