You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Introduces an Observability section in the documentation, detailing built-in OpenTelemetry support.
Refactors the job storage provider to leverage parameters for filtering, simplifying the query logic and making it more extensible.
Adds scenario notes throughout the documentation to highlight the optional nature of extensions.
Updates the documentation to reflect that GUSTO has no more plugin based JobRunner
Removes the cron rescheduling logic from the completion handler, as this is now handled in the updated example job storage provider (not shown)
<divclass="section-tag">Own the data, use the worker</div>
35
37
<h1>Customize GUSTO without touching the worker loop.</h1>
36
38
<p>
37
-
This is probably the most flexible and easy to understand job runner out there. Iimplement an <spanclass="code-inline">IJobStorageRecord</span> and an
38
-
<spanclass="code-inline">IJobStorageProvider</span>, then the libraries hosted worker does the rest.
39
-
The drop-in samples below borrow from a real-world deployment to demonstrate what’s possible,
40
-
but they exist purely as reference code—you still own your schema, provider, and policies while GUSTO continues
41
-
to supply the hosted worker. These examples assume PostgreSQL plus EF Core for concreteness, yet the patterns
42
-
translate to any database as long as your provider implements the two interfaces.
39
+
This is probably the most flexible and easy to understand job runner out there. Implement an <spanclass="code-inline">IJobStorageRecord</span> and an
40
+
<spanclass="code-inline">IJobStorageProvider</span>, then the library’s hosted worker does the rest. Out of the box you get a WYSIWYG background queue: enqueue work, the worker runs it, that’s it.
41
+
</p>
42
+
<pclass="intro-note">
43
+
Every section below shows an <em>optional</em> way to extend that base behavior. Because you own the record and the provider, you can bolt on batching, continuations, cron schedules, richer retries, and telemetry whenever you need them—and ignore what you don’t.
<pclass="scenario-note">Optional extension: add a shared <spanclass="code-inline">BatchId</span> so related jobs run in parallel but unlock dependents together.</p>
214
199
<p>
215
200
A batch is just a <spanclass="code-inline">Guid</span> written onto each job record. Workers process batch members independently,
216
201
but continuations won’t fire until <spanclass="code-inline">CanProcessContinuations</span> reports every row finished (or whatever rule
@@ -258,6 +243,7 @@ <h2>Fan out work, gate downstream steps.</h2>
258
243
<sectionid="continuations">
259
244
<divclass="section-tag">03 · Continuations</div>
260
245
<h2>Parent/child orchestration managed in the DB.</h2>
246
+
<pclass="scenario-note">Optional extension: park follow-up work in <spanclass="code-inline">WaitingForParent</span> until a parent job (or entire batch) finishes.</p>
261
247
<p>
262
248
Continuations are “waiting” jobs tied to either a specific parent (or an entire batch). The helper below writes the row and parks it
263
249
in <spanclass="code-inline">JobStatus.WaitingForParent</span>. The provider’s <spanclass="code-inline">ProcessContinuations</span> and
@@ -313,11 +299,8 @@ <h2>Parent/child orchestration managed in the DB.</h2>
@@ -395,6 +378,7 @@ <h2>Parent/child orchestration managed in the DB.</h2>
395
378
<sectionid="cron">
396
379
<divclass="section-tag">04 · Cron</div>
397
380
<h2>Recurring jobs without another service.</h2>
381
+
<pclass="scenario-note">Optional extension: add cron metadata so the provider reschedules recurring work without external schedulers.</p>
398
382
<p>
399
383
By combining <spanclass="code-inline">Cronos</span> with provider-level upserts, you can schedule resilient recurring work.
400
384
Add two things: (1) cron metadata on the record, and (2) provider methods that reschedule rows instead of marking them complete.
@@ -486,6 +470,7 @@ <h2>Recurring jobs without another service.</h2>
486
470
<sectionid="retries">
487
471
<divclass="section-tag">05 · Retries</div>
488
472
<h2>Exponential backoff handled entirely in SQL.</h2>
473
+
<pclass="scenario-note">Optional extension: track failure metadata and control backoff directly in your provider.</p>
489
474
<p>
490
475
The README calls out “customizable strategies.” This is where you plug them in. The worker invokes
491
476
<spanclass="code-inline">OnHandlerExecutionFailureAsync</span> when any handler throws, and you decide how to recover.
@@ -494,6 +479,13 @@ <h2>Exponential backoff handled entirely in SQL.</h2>
494
479
<p>
495
480
Because this strategy updates <spanclass="code-inline">job.Status</span>, make sure the <spanclass="code-inline">JobStatus</span> enum/field from Section 03 is present.
<preid="retry-record"><codeclass="language-csharp">// JobRecord additions for advanced retry tracking
485
+
public int FailureCount { get; set; }
486
+
public string LastFailureReason { get; set; }
487
+
public DateTime? LastFailureTime { get; set; }</code></pre>
488
+
</div>
497
489
<p>
498
490
Swap the simple retry body from section 01 with a call to the helper below:
499
491
</p>
@@ -553,9 +545,72 @@ <h2>Exponential backoff handled entirely in SQL.</h2>
553
545
</p>
554
546
</section>
555
547
548
+
<sectionid="observability">
549
+
<divclass="section-tag">06 · Observability</div>
550
+
<h2>Telemetry out of the box.</h2>
551
+
<pclass="scenario-note">Optional extension: plug the built-in ActivitySource and Meter into your existing OpenTelemetry pipeline.</p>
552
+
<p>
553
+
GUSTO ships an OpenTelemetry <spanclass="code-inline">ActivitySource</span> and <spanclass="code-inline">Meter</span> named <spanclass="code-inline">ByteBard.GUSTO.JobQueue</span>. Hook them into your existing pipeline and the worker immediately emits spans and counters for every batch and job your provider executes.
<td>Job execution time in ms (<spanclass="code-inline">job.type</span>, <spanclass="code-inline">job.method</span>, <spanclass="code-inline">job.status</span>)</td>
Tracing emits two spans: <spanclass="code-inline">ProcessBatch</span> (one per polling cycle) and <spanclass="code-inline">ExecuteJob</span> (one per job). Use them to correlate queue throughput with handler performance, and extend the meter with your own counters if you need additional signals.
607
+
</p>
608
+
</section>
609
+
556
610
<sectionid="testing">
557
-
<divclass="section-tag">06 · Testing</div>
611
+
<divclass="section-tag">07 · Testing</div>
558
612
<h2>Deterministic integration tests.</h2>
613
+
<pclass="scenario-note">Optional extension: use the built-in test barriers to run the worker in-process without sleeps.</p>
559
614
<p>
560
615
The library ships with test barriers built in, so you can run the worker in-process without flakiness. Set <spanclass="code-inline">BatchStartBarrier</span> to pause the worker right before it drains a batch, and <spanclass="code-inline">BatchCompletedBarrier</span> to know when the batch is done. This gives you deterministic integration tests without polling.
Keep barriers confined to tests—they’re static, so always reset them when your test finishes. This pattern makes GUSTO easy to exercise in CI without resorting to sleeps or polling loops.
644
+
Keep barriers confined to tests—they’re static, so always reset them (e.g., set them back to <spanclass="code-inline">null</span>) when your test finishes. This pattern makes GUSTO easy to exercise in CI without resorting to sleeps or polling loops.
645
+
</p>
646
+
</section>
647
+
648
+
<sectionid="faq">
649
+
<divclass="section-tag">08 · FAQ</div>
650
+
<h2>FAQ & comparison.</h2>
651
+
<h3>What does GUSTO stand for?</h3>
652
+
<p>
653
+
GUSTO is the <strong>G</strong>eneric <strong>U</strong>tility for <strong>S</strong>cheduling & <strong>T</strong>ask <strong>O</strong>rchestration. The name reflects the library’s goal: ship a worker, let you own everything else.
654
+
</p>
655
+
<h3>Why build another job runner?</h3>
656
+
<p>
657
+
Existing runners hide orchestration behind filters, triggers, or custom DSLs. GUSTO keeps the worker loop simple and hands storage orchestration back to your C# code—no hidden magic, no framework migrations when requirements change.
658
+
</p>
659
+
<h3>How does it compare?</h3>
660
+
<tableclass="comparison-table">
661
+
<thead>
662
+
<tr>
663
+
<th>Project</th>
664
+
<th>Extension model</th>
665
+
<th>Type safety</th>
666
+
<th>Operational feel</th>
667
+
<th>Best when…</th>
668
+
</tr>
669
+
</thead>
670
+
<tbody>
671
+
<tr>
672
+
<td><strong>GUSTO</strong></td>
673
+
<td>Implement two interfaces; add helpers for batching/continuations</td>
674
+
<td>Expression enqueue keeps jobs strongly typed</td>
675
+
<td>You control storage, retries, logging</td>
676
+
<td>You want to extend behavior with plain C# and own the data model</td>
677
+
</tr>
678
+
<tr>
679
+
<td>Hangfire</td>
680
+
<td>Dashboard filters & attributes</td>
681
+
<td>Strongly-typed expression enqueue (similar to GUSTO)</td>
682
+
<td>Easy start, complex filters get dense</td>
683
+
<td>You want a hosted dashboard and shared storage</td>
684
+
</tr>
685
+
<tr>
686
+
<td>Quartz.NET</td>
687
+
<td>Triggers, calendars, listeners</td>
688
+
<td>IoC friendly but verbose scheduling API</td>
689
+
<td>Powerful—also heavyweight</td>
690
+
<td>You need enterprise-grade calendars and cluster scheduling</td>
691
+
</tr>
692
+
<tr>
693
+
<td>TickerQ</td>
694
+
<td>Pipeline-based timers</td>
695
+
<td>Strings for dispatch</td>
696
+
<td>Minimal overhead, limited orchestration</td>
697
+
<td>You need lightweight distributed timers without custom storage</td>
698
+
</tr>
699
+
</tbody>
700
+
</table>
701
+
<h3>What is expected from a <spanclass="code-inline">GetBatchAsync</span> implementation?</h3>
702
+
<p>
703
+
The worker calls your <spanclass="code-inline">GetBatchAsync</span> every poll and passes a <spanclass="code-inline">JobSearchParams</span> with a <spanclass="code-inline">Match</span> predicate that enforces <strong>not complete</strong>, <strong>ready to run</strong>, and <strong>not expired</strong>. You can apply additional filters or ordering (tenancy, priority, status flags) before returning the records, but make sure to honor the provided predicate and the <spanclass="code-inline">Limit</span>. Returning fewer items than requested is fine; returning jobs that fail the predicate will cause the worker to skip them and log warnings.
590
704
</p>
591
705
</section>
592
706
593
707
<footer>
594
-
Built on ByteBard.GUSTO · Storage-owned orchestration since 2024
708
+
Built on ByteBard.GUSTO · No more plugin based JobRunner since 2024
0 commit comments