@@ -212,6 +212,50 @@ func (s *SourceManager) Enumerate(ctx context.Context, sourceName string, source
212
212
case s .firstErr <- err :
213
213
default :
214
214
}
215
+ progress .ReportError (Fatal {err })
216
+ }
217
+ }()
218
+ return progress .Ref (), nil
219
+ }
220
+
221
+ // Scan blocks until a resource is available to run the source against a single
222
+ // SourceUnit, then asynchronously runs it. Error information is stored and
223
+ // accessible via the JobProgressRef as it becomes available.
224
+ func (s * SourceManager ) Scan (ctx context.Context , sourceName string , source Source , unit SourceUnit ) (JobProgressRef , error ) {
225
+ sourceID , jobID := source .SourceID (), source .JobID ()
226
+ // Do preflight checks before waiting on the pool.
227
+ if err := s .preflightChecks (ctx ); err != nil {
228
+ return JobProgressRef {
229
+ SourceName : sourceName ,
230
+ SourceID : sourceID ,
231
+ JobID : jobID ,
232
+ }, err
233
+ }
234
+ // Create a JobProgress object for tracking progress.
235
+ ctx , cancel := context .WithCancelCause (ctx )
236
+ progress := NewJobProgress (jobID , sourceID , sourceName , WithHooks (s .hooks ... ), WithCancel (cancel ))
237
+ if err := s .sem .Acquire (ctx , 1 ); err != nil {
238
+ // Context cancelled.
239
+ progress .ReportError (Fatal {err })
240
+ return progress .Ref (), Fatal {err }
241
+ }
242
+ s .wg .Add (1 )
243
+ go func () {
244
+ // Call Finish after the semaphore has been released.
245
+ defer progress .Finish ()
246
+ defer s .sem .Release (1 )
247
+ defer s .wg .Done ()
248
+ ctx := context .WithValues (ctx ,
249
+ "source_manager_worker_id" , common .RandomID (5 ),
250
+ )
251
+ defer common .Recover (ctx )
252
+ defer cancel (nil )
253
+ if err := s .scan (ctx , source , progress , unit ); err != nil {
254
+ select {
255
+ case s .firstErr <- err :
256
+ default :
257
+ }
258
+ progress .ReportError (Fatal {err })
215
259
}
216
260
}()
217
261
return progress .Ref (), nil
@@ -320,7 +364,7 @@ func (s *SourceManager) run(ctx context.Context, source Source, report *JobProgr
320
364
ctx = context .WithValue (ctx , "source_type" , source .Type ().String ())
321
365
}
322
366
323
- // Check for the preferred method of tracking source units .
367
+ // Check if source units are supported and configured .
324
368
canUseSourceUnits := len (targets ) == 0 && s .useSourceUnitsFunc != nil
325
369
if enumChunker , ok := source .(SourceUnitEnumChunker ); ok && canUseSourceUnits && s .useSourceUnitsFunc () {
326
370
ctx .Logger ().Info ("running source" ,
@@ -359,7 +403,7 @@ func (s *SourceManager) enumerate(ctx context.Context, source Source, report *Jo
359
403
ctx = context .WithValue (ctx , "source_type" , source .Type ().String ())
360
404
}
361
405
362
- // Check for the preferred method of tracking source units .
406
+ // Check if source units are supported and configured .
363
407
canUseSourceUnits := s .useSourceUnitsFunc != nil
364
408
if enumChunker , ok := source .(SourceUnitEnumerator ); ok && canUseSourceUnits && s .useSourceUnitsFunc () {
365
409
ctx .Logger ().Info ("running source" ,
@@ -369,6 +413,42 @@ func (s *SourceManager) enumerate(ctx context.Context, source Source, report *Jo
369
413
return fmt .Errorf ("Enumeration not supported or configured for source: %s" , source .Type ().String ())
370
414
}
371
415
416
+ // scan runs a scan against a single SourceUnit as its only job. This method
417
+ // manages the lifecycle of the provided report.
418
+ func (s * SourceManager ) scan (ctx context.Context , source Source , report * JobProgress , unit SourceUnit ) error {
419
+ report .Start (time .Now ())
420
+ defer func () { report .End (time .Now ()) }()
421
+
422
+ defer func () {
423
+ if err := context .Cause (ctx ); err != nil {
424
+ report .ReportError (Fatal {err })
425
+ }
426
+ }()
427
+
428
+ report .TrackProgress (source .GetProgress ())
429
+ if ctx .Value ("job_id" ) == "" {
430
+ ctx = context .WithValue (ctx , "job_id" , report .JobID )
431
+ }
432
+ if ctx .Value ("source_id" ) == "" {
433
+ ctx = context .WithValue (ctx , "source_id" , report .SourceID )
434
+ }
435
+ if ctx .Value ("source_name" ) == "" {
436
+ ctx = context .WithValue (ctx , "source_name" , report .SourceName )
437
+ }
438
+ if ctx .Value ("source_type" ) == "" {
439
+ ctx = context .WithValue (ctx , "source_type" , source .Type ().String ())
440
+ }
441
+
442
+ // Check if source units are supported and configured.
443
+ canUseSourceUnits := s .useSourceUnitsFunc != nil
444
+ if unitChunker , ok := source .(SourceUnitChunker ); ok && canUseSourceUnits && s .useSourceUnitsFunc () {
445
+ ctx .Logger ().Info ("running source" ,
446
+ "with_units" , true )
447
+ return s .scanWithUnit (ctx , unitChunker , report , unit )
448
+ }
449
+ return fmt .Errorf ("source units not supported or configured for source: %s (%s)" , report .SourceName , source .Type ().String ())
450
+ }
451
+
372
452
// enumerateWithUnits is a helper method to enumerate a Source that is also a
373
453
// SourceUnitEnumerator. This allows better introspection of what is getting
374
454
// enumerated and any errors encountered.
@@ -511,6 +591,44 @@ func (s *SourceManager) runWithUnits(ctx context.Context, source SourceUnitEnumC
511
591
}
512
592
}
513
593
594
+ // scanWithUnit produces chunks from a single SourceUnit.
595
+ func (s * SourceManager ) scanWithUnit (ctx context.Context , source SourceUnitChunker , report * JobProgress , unit SourceUnit ) error {
596
+ // Create a function that will save the first error encountered (if
597
+ // any) and discard the rest.
598
+ chunkReporter := & mgrChunkReporter {
599
+ unit : unit ,
600
+ chunkCh : make (chan * Chunk , defaultChannelSize ),
601
+ report : report ,
602
+ }
603
+ // Produce chunks from the given unit.
604
+ var chunkErr error
605
+ go func () {
606
+ report .StartUnitChunking (unit , time .Now ())
607
+ // TODO: Catch panics and add to report.
608
+ defer close (chunkReporter .chunkCh )
609
+ id , kind := unit .SourceUnitID ()
610
+ ctx := context .WithValues (ctx , "unit_kind" , kind , "unit" , id )
611
+ ctx .Logger ().V (3 ).Info ("chunking unit" )
612
+ if err := source .ChunkUnit (ctx , unit , chunkReporter ); err != nil {
613
+ report .ReportError (Fatal {ChunkError {Unit : unit , Err : err }})
614
+ chunkErr = Fatal {err }
615
+ }
616
+ }()
617
+ // Consume chunks and export chunks.
618
+ // This anonymous function blocks until the chunkReporter.chunkCh is
619
+ // closed in the above goroutine.
620
+ func () {
621
+ defer func () { report .EndUnitChunking (unit , time .Now ()) }()
622
+ for chunk := range chunkReporter .chunkCh {
623
+ if src , ok := source .(Source ); ok {
624
+ chunk .JobID = src .JobID ()
625
+ }
626
+ s .outputChunks <- chunk
627
+ }
628
+ }()
629
+ return chunkErr
630
+ }
631
+
514
632
// headlessAPI implements the apiClient interface locally.
515
633
type headlessAPI struct {
516
634
// Counters for assigning source and job IDs.
0 commit comments