@@ -79,28 +79,26 @@ func (r *PoolBuilder) WithSpinner(message ...string) contractsprocess.PoolBuilde
7979 return r
8080}
8181
82- type job struct {
83- id int
84- command * PoolCommand
85- }
86-
8782type result struct {
8883 key string
8984 res contractsprocess.Result
9085}
9186
92- // start initiates the execution of all configured commands concurrently but does not wait for them to complete.
93- // It orchestrates a pool of worker goroutines to process commands up to the specified concurrency limit .
87+ // start initiates the execution of all configured commands concurrently based on the
88+ // concurrency limit, but does not wait for them to complete .
9489//
95- // This method is non-blocking. It returns a RunningPool instance immediately, which can be used to
96- // wait for the completion of all processes and retrieve their results.
90+ // This method is non-blocking. It returns a RunningPool instance immediately, which
91+ // can be used to wait for the completion of all processes and retrieve their results.
9792//
9893// The core concurrency pattern is as follows:
9994// 1. A job channel (`jobCh`) distributes commands to a pool of worker goroutines.
100- // 2. A result channel (`resultCh`) collects the outcome of each command from a dedicated waiter goroutine.
101- // 3. A separate "collector" goroutine safely populates the final results map from the result channel.
102- // 4. WaitGroups synchronize the completion of all workers and the collection of all results
103- // before the entire operation is marked as "done".
95+ // 2. Workers pick up a job, start the process, and wait synchronously for it to finish.
96+ // This synchronous wait ensures the concurrency limit is strictly respected.
97+ // 3. A result channel (`resultCh`) collects the outcome (success/failure) of each command.
98+ // 4. A separate "collector" goroutine safely populates the RunningPool's internal map
99+ // from the result channel to avoid concurrent map write panics.
100+ // 5. A background orchestrator waits for all workers and results to finish, then
101+ // cleanly closes resources and signals the `done` channel.
104102func (r * PoolBuilder ) start (configurer func (contractsprocess.Pool )) (contractsprocess.RunningPool , error ) {
105103 if configurer == nil {
106104 return nil , errors .ProcessPoolNilConfigurer
@@ -118,25 +116,28 @@ func (r *PoolBuilder) start(configurer func(contractsprocess.Pool)) (contractspr
118116 var cancel context.CancelFunc
119117 if r .timeout > 0 {
120118 ctx , cancel = context .WithTimeout (ctx , r .timeout )
119+ } else {
120+ ctx , cancel = context .WithCancel (ctx )
121121 }
122122
123- concurrency := r .concurrency
124- if concurrency <= 0 || concurrency > len (commands ) {
125- concurrency = len (commands )
126- }
127-
128- jobCh := make (chan job , len (commands ))
123+ jobCh := make (chan * PoolCommand , len (commands ))
129124 resultCh := make (chan result , len (commands ))
130125 done := make (chan struct {})
131126
132- results := make (map [string ]contractsprocess.Result , len (commands ))
133- runningProcesses := make ([]contractsprocess.Running , len (commands ))
134127 keys := make ([]string , len (commands ))
128+ for i , cmd := range commands {
129+ keys [i ] = cmd .key
130+ }
131+
132+ runningPool := NewRunningPool (ctx , cancel , keys , done , r .loading , r .loadingMessage )
133+
134+ concurrency := r .concurrency
135+ if concurrency <= 0 || concurrency > len (commands ) {
136+ concurrency = len (commands )
137+ }
135138
136139 var resultsWg sync.WaitGroup
137140 var workersWg sync.WaitGroup
138- var startsWg sync.WaitGroup
139- var mu sync.Mutex
140141
141142 // The results collector goroutine centralizes writing to the results map
142143 // to avoid race conditions, as map writes are not concurrent-safe.
@@ -145,9 +146,7 @@ func (r *PoolBuilder) start(configurer func(contractsprocess.Pool)) (contractspr
145146 go func () {
146147 for i := 0 ; i < len (commands ); i ++ {
147148 rc := <- resultCh
148- mu .Lock ()
149- results [rc .key ] = rc .res
150- mu .Unlock ()
149+ runningPool .setResult (rc .key , rc .res )
151150 resultsWg .Done ()
152151 }
153152 }()
@@ -156,8 +155,16 @@ func (r *PoolBuilder) start(configurer func(contractsprocess.Pool)) (contractspr
156155 workersWg .Add (1 )
157156 go func () {
158157 defer workersWg .Done ()
159- for currentJob := range jobCh {
160- command := currentJob .command
158+ for command := range jobCh {
159+ if ctx .Err () != nil {
160+ // If the pool was stopped (Stop() called or timeout reached), we skip execution.
161+ // We must still send a result to ensure resultsWg decrements correctly.
162+ resultCh <- result {
163+ key : command .key ,
164+ res : NewResult (ctx .Err (), - 1 , "" , "" , "" ),
165+ }
166+ continue
167+ }
161168 cmdCtx := command .ctx
162169 if cmdCtx == nil {
163170 cmdCtx = ctx
@@ -184,36 +191,19 @@ func (r *PoolBuilder) start(configurer func(contractsprocess.Pool)) (contractspr
184191 if err != nil {
185192 resultCh <- result {key : command .key , res : NewResult (err , - 1 , command .name , "" , "" )}
186193 } else {
187- mu .Lock ()
188- runningProcesses [currentJob .id ] = run
189- mu .Unlock ()
190-
191- // Launch a dedicated goroutine to wait for the process to finish.
192- // This prevents the worker from being blocked by a long-running process
193- // and allows it to immediately pick up the next job from jobCh.
194- go func (p contractsprocess.Running , k string ) {
195- res := p .Wait ()
196- resultCh <- result {key : k , res : res }
197- }(run , command .key )
194+ runningPool .setProcess (command .key , run )
195+ res := run .Wait ()
196+ resultCh <- result {key : command .key , res : res }
198197 }
199-
200- // Signal that this process has completed its start attempt
201- startsWg .Done ()
202198 }
203199 }()
204200 }
205201
206- startsWg .Add (len (commands ))
207- for i , command := range commands {
208- keys [i ] = command .key
209- jobCh <- job {id : i , command : command }
202+ for _ , command := range commands {
203+ jobCh <- command
210204 }
211205 close (jobCh )
212206
213- // Wait for all processes to complete their start attempts before returning.
214- // This ensures the runningProcesses slice is fully populated and safe to access.
215- startsWg .Wait ()
216-
217207 // This goroutine orchestrates the clean shutdown. It waits for all workers
218208 // to finish processing jobs, then waits for all results to be collected.
219209 // Finally, it cancels the context (if a timeout was set) and signals
@@ -227,7 +217,7 @@ func (r *PoolBuilder) start(configurer func(contractsprocess.Pool)) (contractspr
227217 close (done )
228218 }()
229219
230- return NewRunningPool ( ctx , runningProcesses , keys , cancel , results , done , r . loading , r . loadingMessage ) , nil
220+ return runningPool , nil
231221}
232222
233223type Pool struct {
0 commit comments