Releases
0.9.2
2026-04-22
Bug Fixes
- SQL Server push setup stall is now cancellable —
SqlServerNotificationTransport.PublishAsyncandListenAsyncnow await the cached_setup.ValueviaTask.WaitAsync(ct)instead of a rawawait. A stalled broker setup (e.g., schema lock contention on a busy SQL Server) no longer blocks every caller indefinitely — each caller'sCancellationTokenbails out of the wait without invalidating the cached setup task. The next caller with a live token re-awaits and proceeds. This closes the class of intermittent"Test execution timed out after 30000 milliseconds"failures inSqlServerDatabasePushIntegrationTestsunder heavy CI contention. - Race in
ServerTaskLoop.Signalfixed —Signal()had a check-then-act TOCTOU on itsSemaphoreSlim(0, 1): two threads could both passCurrentCount == 0and both callRelease(), second throwingSemaphoreFullException. Surfaced under dispatcher-mode contention (many workers callingSignalJobFinalizedconcurrently as batches flush) as"Adding the specified count to the semaphore would cause it to exceed its maximum count"and cascading downstream failures in the affected task loop. Fixed with a lock around the check-and-release — same patternJoblyDispatcher.SignalAllalready used. Regression test runs 32 threads × 500 calls concurrently and asserts no exception.
Test Suite Improvements
TimedFactdefault 30s → 10s — Individual tests should finish in seconds; the 30s default was a band-aid for overly generous inner waits and could hide real hangs. Tests exercising deliberately slow behaviour (retry chains, multi-job integration workloads, two-server orchestration) opt in explicitly with[TimedFact(N_000)]. Twenty-three inner-wait timeouts (retry / cancellation / batch / continuation tests) were tightened from 15–30s to 5–10s to match actual runtime, with comfortable headroom for CI jitter.- Durability tests for "no job left unprocessed" — Added three integration tests covering the core recovery guarantees that had no prior coverage:
DispatcherShutdownIntegrationTests.GivenWorkInProgress_WhenServerReplaced_ThenAllJobsEventuallyComplete— pod-rolling-restart scenario. Server A is disposed mid-flight, server B takes over the same queue; every enqueued job must reachCompletedvia whichever recovery path fires (UnclaimUndelivered, channel drain, orStaleJobRecovery).PushFailurePollingBackstopTests.GivenPushEnabledButTransportBroken_WhenJobEnqueued_ThenPollingStillPicksItUp— proves that when the notification transport is completely broken (bothPublishAsyncandListenAsyncthrow), polling still delivers the job within a small multiple ofPollingInterval. Protects the "polling is the correctness backstop for push" invariant.ListenerReconnectDrainTests.GivenListenerAlwaysFails_WhenJobEnqueued_ThenReconnectDrainStillDelivers— proves thatNotificationListenerTask.DrainSignalsfires on every reconnect iteration, waking the dispatcher even while the listener connection is permanently down. Jobs enqueued during the listener's offline window are not stranded.
- Full suite runtime — now ~1m 30s for 1,024 tests (was ~2m 40s for 947 in 0.8.0), down primarily from the inner-wait tightenings and the shorter
TimedFactdefault.
0.9.1
2026-04-21
Bug Fixes
- Docs site build —
website/docs/features/db-push.mdwas referenced from the 0.9.0 release notes but never added, breaking the Docusaurus production build withonBrokenLinks: 'throw'. The page is now present and mirrors the DB Push section of the README.
Maintenance
- Dashboard UI dependencies —
npm audit fixinsrc/uipatchesvite,axios,hono,@hono/node-server, andfollow-redirects(5 advisories, 1 high + 4 moderate). No API-surface changes; dashboard bundle hashes change because Vite re-bundles with updated transitive deps. - Docs site dependencies — Docusaurus 3.9.2 → 3.10.0, added
@docusaurus/faster(required by 3.10 withfuture.v4), and pinnedserialize-javascript ^7.0.5viaoverridesto close GHSA-qj8w-gfj5-8c6v — the build-chain DoS that Docusaurus's bundler transitively pulled in.npm auditnow reports 0 vulnerabilities on both frontends.
0.9.0
2026-04-21
New Features
- Database Push — Opt-in push notifications replace polling wake-up for the dispatcher,
MessageRoutingTask, andOrchestrationTask. Uses PostgreSQLLISTEN/NOTIFYor SQL Server Service Broker natively. Enable viaopt.UseDatabasePush()inside theAddJobly/AddJoblyWorkerlambda. Dispatcher pickup drops from ~500ms to <50ms; burst-and-idle workloads roughly halve wall-clock time. Idle deployments see ~16% fewer SELECTs (emptyFOR UPDATE SKIP LOCKEDfetches during idle go away). Worker-fetch push only wires whenUseDispatcher = true— individual-worker mode keeps polling to avoid thundering-herd. Zero overhead if you don't opt in. See DB Push. State.Scheduled— Future-dated jobs (Schedule(job, at)) now land inState.Scheduledinstead ofState.Enqueuedwith a futureScheduleTime.ScheduledJobActivationTaskflips them toEnqueuedwhen due and fires aJobEnqueuedpush. Cleans up the worker fetch predicate to a pureCurrentState = Enqueuedcheck. Pre-upgrade rows still execute correctly thanks to a defensiveScheduleTime <= nowfilter.- Per-database Provider Packages — Provider-specific code moves out of
Jobly.Core/Jobly.Workerinto two new NuGet packages:Moberg.Jobly.Provider.PostgreSqlandMoberg.Jobly.Provider.SqlServer. Install the one that matches your database and opt in viaopt.UsePostgreSql()/opt.UseSqlServer()inside the registration lambda. Core now stays fully provider-agnostic — noNpgsqlorMicrosoft.Data.SqlClientreferences. - Builder-based DI API —
AddJobly<TContext>(opt => ...)/AddJoblyWorker<TContext>(opt => ...)take a single lambda overIJoblyBuilder<TContext>. Config fields live on the builder directly (inheritsJoblyConfiguration); addons chain as extension methods (opt.AddRetry(),opt.AddMutex(),opt.AddCircuitBreaker(),opt.AddNoRestart(),opt.UseDatabasePush()).
Improvements
- Server-task architecture refactor (
IServerTask) — Every background task (Heartbeat,ServerCleanup,StaleJobRecovery,CounterAggregator,ExpirationCleanup,RecurringJobScheduler,ScheduledJobActivation,Orchestrator,MessageRouter) is now a plain DI-registeredIServerTaskservice driven by a singleServerTaskHost<TContext>. Previously each task was aBackgroundServicesubclass ofServerTaskBase— the split separates domain logic from hosting mechanics. Task inner methods that werepublic staticfor test access are nowinternalinstance methods, closing a class of lock-bypass bugs where tests and operators could accidentally invoke work outside the distributed lock. - Disable cleanup tasks at the config level —
CounterAggregationInterval,ServerCleanupInterval,StaleJobRecoveryInterval,ExpirationCleanupInterval, andRecurringJobSchedulerIntervalare nowTimeSpan?. Set any tonulland the host won't auto-run that task's loop. Useful for multi-tenant setups where you want only one server running cleanup. - Signal plumbing consolidated —
OrchestratorandMessageRouterwake on push events via a singleServerTaskSignals<TContext>singleton with namedSignalJobFinalized/SignalMessageEnqueuedmethods. Tasks declare their subscriptions viaIServerTask.Signals. Replaces the old static_instanceslists. No user-visible behaviour change. - Heartbeat no longer writes a
ServerLogrow per run — under the oldServerTaskBasedefault, every server wrote a heartbeat success row every 3s. Now explicit per task viaLogOnSuccess; heartbeat opts out. Failed heartbeats still log. - Faster CI builds — Test workflow builds only
tests/Jobly.Tests/Jobly.Tests.csprojinstead of the full solution. Benchmarks, mutation tests, and demo apps no longer built in the test job; they come in transitively only where tests reference them. - Atomic-claim fetch — Worker and dispatcher fetch are now
UPDATE ... RETURNING(PG) /UPDATE ... OUTPUT INSERTED.*(SQL Server) via newIJoblySqlQueries<TContext>implementations in the provider packages. Closes the SELECT→UPDATE race window that produced rare double-claims under concurrent-worker load. The old regex-basedRowLockInterceptoris retired. - Shutdown safety — Three races that could leave jobs as
State=Processingorphans on shutdown are fixed:JoblyDispatcherun-claims rows it fetched but didn't deliver;JoblyDispatcherWorkerdrains its channel fully before exiting; post-handler bookkeeping usesCancellationToken.Noneso a job whose handler already ran can't be abandoned mid-finalize. - Retry / CircuitBreaker respect
State.Scheduled— Delayed retries and circuit-breaker reschedules land inState.Scheduledwhen the target time is in the future. NewJobOutcome.RescheduledState(scheduleTime, now)helper is shared by both pipeline behaviors. No change for immediate retries. - Worker host split —
JoblyWorkerSetupis replaced by three DI-registeredIHostedServices:JoblyServerRegistration,JoblyDispatcherHost,JoblySingleWorkerHost. Each mode no-ops when the other is selected viaUseDispatcher. State flows via a newServerRegistrationStatesingleton instead of re-querying. - Source layout — Libraries under
src/core/, providers undersrc/core/providers/, tests insrc/tests/, demo apps insrc/demo/.Directory.Build.propsscoped to match.
Bug Fixes
- Queue-name encoding collision (SQL Server) —
JobHelpernow rejects queue names containing the unit-separator () that SQL Server'sSTRING_SPLITuses internally for encoding. Previously a job published to a-containing queue could be delivered to the wrong worker group.
Migration
Breaking release because of the provider package split and the DI lambda API.
- Install a provider NuGet: add
Moberg.Jobly.Provider.PostgreSqlorMoberg.Jobly.Provider.SqlServeralongsideMoberg.Jobly.Core. The provider package registers the row-lock / atomic-claim queries, the exception classifier, and the notification-transport factory — all of which used to live in Core. - Wrap registration in a lambda + call the provider:
// beforeservices.AddJobly<MyContext>();services.AddJoblyDatabasePush<MyContext>(); // if using push// afterservices.AddJobly<MyContext>(opt =>{opt.UsePostgreSql(); // or UseSqlServer()opt.UseDatabasePush(); // if using push, now chains on the builder});
AddJoblyDatabasePush<TContext>()removed — callopt.UseDatabasePush()on the builder instead. Must be afterUsePostgreSql/UseSqlServer.State.Scheduledis new — Future-dated jobs existing from a previous version still execute correctly (worker fetch has a defensiveScheduleTime <= nowpredicate) but won't show in the dashboard's Scheduled list until their time arrives.- Retry / CircuitBreaker reschedules land in
State.Scheduledwhen delayed — dashboard filters and any external tooling that queriedEnqueued + future ScheduleTimeshould now also look atScheduled.
0.8.0
2026-04-19
New Features
- Exponential Polling Backoff — Workers and the batch-fetch dispatcher now back off geometrically when queues are idle, reducing database load during quiet periods. Configure via
MaxPollingInterval(default30s, ceiling) andPollingIntervalFactor(default2.0, multiplier).PollingIntervalbecomes the floor. On any processed job, the delay resets to the floor instantly, so throughput under load is unchanged. Paused workers stay at the floor (no compounding while paused). Available on both top-levelJoblyWorkerConfigurationand per-groupWorkerGroupConfiguration. SetPollingIntervalFactor = 1.0to disable backoff. See Operations → Configuration → Exponential Polling Backoff. - Retry Jitter — New
JitterFactoroption onRetryOptionsapplies multiplicative random jitter to each computed retry delay:delay * (1 + JitterFactor * rand(-1, 1)). Clamped to[0, 1]. Global only — no per-job override. Defaults to0.0(no jitter) so existing behavior is unchanged. Use to spread retry attempts and avoid thundering herds when many jobs fail at once (e.g. downstream outage). The actual jitteredScheduleTimeis recorded in theRequeuedJobLog entry so operators can diagnose from the dashboard. - NoRestart (stale-recovery opt-out) — New addon
AddJoblyNoRestart()lets specific job types stayFailedon worker crash instead of being auto-requeued. Apply with[NoRestart]/[Restart]attributes on the job class (inherits through the class hierarchy),.WithRestart(bool)per-enqueue, or flip the global default withRestartStaleJobsByDefault = false. Override chain: per-publish > attribute > global. For non-idempotent work (payments, emails, webhooks). See NoRestart. - Circuit Breaker — New addon
AddJoblyCircuitBreaker<TContext>()stops hammering a failing downstream when failures cross a threshold. Opens afterThresholdconsecutive failures, stays open forDuration, then transitions to a half-open state with an atomic probe gate — exactly one worker probes while others reschedule, preventing thundering herd on recovery. Customise per-handler via[CircuitBreaker(Group = "...", Threshold = N)]. Adds aCircuitBreakerStateentity to your DbContext (migration required). See Circuit Breaker. - Batched Completions (Dispatcher Mode) — When
UseDispatcher = true, each worker now buffers job completions in memory and commits them as a single multi-row transaction, collapsing N per-job commits into one. Tune viaCompletionBatchSize(default50) andCompletionFlushInterval(default100ms); setCompletionBatchSize = 1to opt out. Poison-entry isolation: a single bad row in a batch of 50 is dropped via recursive split; the other 49 still commit. SIGTERM mid-flush is safe —FlushAsynccommits to completion usingCancellationToken.Noneinternally. Trade-off is at-least-once semantics; pair with[NoRestart]for non-idempotent handlers. See Batched Completions. - Configurable Log Flush Interval — New
LogFlushIntervalonJoblyWorkerConfiguration(default1s) controls how often the job monitor drains handlerILoggeroutput into the JobLog table. Lower values surface dashboard logs faster at the cost of more DB writes.
Improvements
- Resilient worker/dispatcher exception handling —
JoblyWorkernow catches transient exceptions in its poll loop (matching the dispatcher), so a single DB hiccup or handler pipeline fault no longer silently terminates the BackgroundService. The exception path uses a fixed floor delay instead of compounding the polling backoff, so jobs resume withinPollingIntervalof recovery instead of sitting atMaxPollingIntervalfor 30s. [NoRestart]and[Restart]attributes are inherited — Declare the policy on a base class (e.g.PaymentJobBase) and every derived concrete job inherits it. A derived class with its own attribute overrides the base — closest direct declaration wins.- Circuit breaker exception handling narrowed — The
DbUpdateExceptioncatch inCircuitBreakerStore.RecordFailureAsyncnow only suppresses unique-constraint violations (Npgsql23505, SqlClient2627/2601). CHECK, FK, and column-length violations propagate instead of being silently swallowed. - Test suite 48% faster, flake-free — Disabled idle-polling backoff inside the test server, tuned
StaleJobRecoveryIntervalfor tests only, madeLogFlushIntervalconfigurable, and bumped theTimedFactdefault from 10s to 30s. Full suite (947 tests) runs in ~2m 39s (was ~5m 05s). Eliminated the latentTimedFact/WaitForJobStatetimeout-mismatch class of flakes.
Bug Fixes
- Dispatcher SIGTERM completion-loss — Pre-fix,
CompletionBatch.FlushAsyncdrained its buffer before observing cancellation, and a shutdown mid-flush silently dropped the drained batch. Now commits withCancellationToken.Noneinternally. - Circuit breaker thundering herd on half-open — Without the new HalfOpen CAS, every worker polling when
OpenUntillapsed fired a concurrent probe against the recovering downstream. Fix guarantees exactly one probe fires per recovery window. - Retry jitter
ScheduleTimenot logged — TheRequeuedJobLog entry now includes(next attempt scheduled: <ISO timestamp>)so operators can see the actual delay jitter applied. - MultiServer cross-test flakiness — A too-aggressive
StaleJobRecoveryIntervalin the test fixture raced worker keep-alive refreshes under two-server SQL Server load, producing sporadicDbUpdateConcurrencyException.
Migration
- Circuit Breaker migration — If you register
AddJoblyCircuitBreaker<TContext>(), a newCircuitBreakerStateentity is added to your DbContext. Run an EF Core migration to create the table (jobly.circuit_breaker_stateunder default schema + snake_case naming). CompletionBatch.FlushAsyncparameter removal — If you were callingCompletionBatch.FlushAsync(CancellationToken)directly (rare — it's internal toJobly.Worker), the parameter has been removed. The method now always commits to completion.[NoRestart]/[Restart]now inherit — Behaviour change: a base class decorated with[NoRestart]now affects every derived concrete job. If you relied on the pre-releaseInherited = falsebehaviour, add[Restart]on the specific derived types you want to opt back in.StaleJobRecoveryTask.RequeueStaleJobsremoved — Replaced byRecoverStaleJobsreturningStaleJobRecoveryResult(includesRequeued,Failed,Deletedcounts). No[Obsolete]bridge; callers must migrate to the new signature.
0.7.0
2026-04-17
New Features
- Stream Requests — New
IStreamRequest<TResponse>pattern for lazy, item-by-item streaming viaIAsyncEnumerable<TResponse>. ExtendsIRequest<IAsyncEnumerable<TResponse>>to preserve the unified type hierarchy —IPipelineBehaviorapplies automatically at the request level. NewIStreamPipelineBehavior<TRequest, TResponse>wraps the actual enumeration for per-item concerns (timing, transforms). Resolved viaIMediator.CreateStream(). Source generator provides zero-allocation dispatch. - Addon Architecture — New
OutcomeonIJobContext(formerlyFailureOutcome) lets pipeline behaviors control what happens on both success and failure. The worker is a generic state machine that applies the pipeline's decision. Combined with typed metadata and publish pipeline behaviors, this enables building composable addons (retry, mutex, dead letter queue, circuit breaker) entirely on top of Jobly's public API. See Building Addons. - Retry Addon — Retry logic extracted from the worker into an opt-in module at
Jobly.Core.Retry. Declare retry policy with[Retry(3)]on either the handler or the job class, override per-enqueue withnew JobParameters().WithRetry(maxRetries: 5), or set global defaults viaservices.AddJoblyRetry(o => { o.MaxRetries = 3; o.Delays = [15, 60, 300]; }). Priority: per-enqueue metadata > handler attribute > job attribute > global options. - Mutex Addon — Mutex extracted from the worker hot path into an opt-in module at
Jobly.Core.Mutex. Register viaservices.AddJoblyMutex(). Set keys withnew JobParameters().WithMutex("payment:123")or[Mutex("payment-processing")]on the job class. Uses the newIJoblyLockProviderabstraction for distributed locking. - Typed Metadata — Access job metadata through strongly-typed interfaces. Define an interface extending
IJobMetadata, and read it in handlers viactx.GetMetadata<IMyMetadata>()or configure it at publish time withnew JobParameters().Configure<IMyMetadata>(m => m.CustomerName = "John"). The source generator produces dictionary-backed implementations.MetadataSerializeruses native JSON deserialization for round-trip fidelity (integers stay aslong, arrays asList<object>). - Recurring Job Enable/Disable — Disable a recurring job to temporarily stop it from creating new jobs. The scheduler still fires on schedule but records a "Skipped" entry in the execution history. Re-enabling resumes from the next natural cron occurrence with no catchup burst. API:
POST /api/recurring/{id}/enable|disable. Dashboard shows Enabled/Disabled badges and Skipped entries in history. - Worker Scope Isolation — Worker and handler now use separate DI scopes. The handler's DbContext lives in its own scope — on failure, the scope is disposed and tracked entities are discarded. No partial handler work leaks into the worker's save. On success, handler changes are committed first (outbox pattern), then Jobly state.
- Extensible Dashboard UI — New
IJoblyUIExtensioninterface lets external NuGet packages extend the dashboard without forking. Extensions ship an ES-module as an embedded resource, served at/jobly/_ext/{name}/. The SPA dynamically imports each module and callsinstall(jobly). Extensions targetdata-jobly-slotelements withmount/append/insertBefore/insertAfteroperations, or register whole new pages viaaddPage(). React, ReactDOM, Axios, and shadcn components are exposed onwindow.Joblyso extensions don't bundle them. The built-inRetryUIExtensionis the reference implementation — renders a retry progress card with attempts/max and next-delay info on the job detail page.
Improvements
- Handler Registration Split —
AddHandlers(assembly)replaces the oldAddJobHandlers. New granular methods:AddJobHandlers(job + message handlers only),AddMediatorHandlers(request + stream handlers only).AddHandlerscalls both. - Dispatcher Split —
JobDispatcher(worker job execution) andMediatorDispatcher(in-memory request/stream dispatch) are now separate classes with independent method caches. - xUnit v3 + Microsoft Testing Platform — Test suite migrated to xUnit v3 with
UseMicrosoftTestingPlatformRunner. New[TimedFact]/[TimedTheory]attributes enforce a 10-second default timeout per test, surfacing deadlocks and hangs globally. - Server Memory Benchmarks — New benchmark project at
src/benchmarks/Jobly.ServerBenchmarks/with four benchmarks (ScopeMemoryBenchmark,WorkerMemoryBenchmark,ServerMemoryBenchmark,MemoryStressTest) and a customTotalAllocatedDiagnoserthat tracks allocations across all threads. Baseline: ~50 KB per job regardless of scale; 100K-job stress test shows 0.3 MB retained growth (no leak) at 420–496 jobs/sec steady throughput. Documented in Operations → Benchmarks. - Mutation Testing — New
Jobly.Tests.Mutationproject with an in-memory SQLite fixture runs 293 tests in ~10 seconds, enabling a full Core mutation run in ~30 minutes viadotnet-stryker. Baseline scores: Core 99.60% (743 killed / 3 survived), Worker 51.53%. Fixed aRecurringJobPublisherrace condition surfaced during mutation analysis —AddOrUpdateRecurringJobnow usesIJoblyLockProviderto prevent duplicate inserts on concurrent calls. - .slnx Solution Format — Migrated from
src/Jobly.slntosrc/Jobly.slnx(XML-based solution format).
Bug Fixes
- Recurring job race on concurrent update —
RecurringJobPublisher.AddOrUpdateRecurringJobcould insert duplicate rows when called concurrently with the same name. Now usesIJoblyLockProviderfor exclusive access during the upsert. - Trace page group node highlighting — Fixed group node highlighting and edge behavior in the trace visualization page.
Migration
This is a large release with several breaking changes. Plan the upgrade accordingly.
- Retry is opt-in — Add
services.AddJoblyRetry()to enable retries. Without it, failed jobs go directly toFailed. Replace the removedmaxRetriespublisher overloads andJoblyConfiguration.RetryCountwith[Retry(n)]attributes,new JobParameters().WithRetry(n), or the global options callback. - Mutex is opt-in — Add
services.AddJoblyMutex()to enable mutex enforcement. TheJobParameters.Mutexproperty is removed — use.WithMutex("key")or[Mutex("key")]instead. TheConcurrencyKeycolumn on theJobentity is removed (keys now live in metadata). - Typed metadata API —
IJobContext<T>/JobContext<T>are removed. Read typed metadata viactx.GetMetadata<IMyMetadata>()and configure at publish vianew JobParameters().Configure<IMyMetadata>(m => ...). - Reduced public surface —
JobHelper,JobDispatcher,MetadataSerializer, and EF interceptors are nowinternal. The Retry and Mutex addons demonstrate that everything needed to build addons is available through the public API — noInternalsVisibleToneeded. - Database migration required — New
DisabledAtcolumn onRecurringJob,Skippedcolumn onRecurringJobLog,ConcurrencyKeydropped fromJob. Run an EF Core migration after upgrading. - Solution file renamed — Update build scripts and IDE shortcuts from
Jobly.slntoJobly.slnx.
Stats
- ~770 tests (PostgreSQL + SQL Server) + 293 SQLite mutation tests
- Core mutation score: 99.60% (746 mutants, 743 killed)
0.6.1
2026-04-13
Bug Fixes
- Scoped service resolution in lock provider —
IDistributedLockProvidersingleton factory resolvedDbContextOptions<TContext>from the root provider, butAddDbContextregisters it as scoped. This threwInvalidOperationExceptionwhen scope validation is enabled (e.g.WebApplication.CreateBuilder()in Development). Fixed by creating a scope inside the factory.
Code Quality
- Fix all 401 build warnings — Zero warnings across the entire solution. Fixes include: regex DoS hardening (NonBacktracking), collection expression simplification, constructor formatting, CancellationToken propagation, string.Equals usage, and async call consistency. All rule suppressions via
.editorconfig— no#pragmaor[SuppressMessage]attributes.
0.6.0
2026-04-12
New Features
- OpenTelemetry Distributed Tracing — Every job execution creates a
System.Diagnostics.Activitywith W3C-format TraceId, SpanId, and ParentSpanId. Trace context is automatically propagated through job chains: when a handler enqueues a child job, the child'sParentSpanIdlinks back to the handler's span. Message routing and batch creation also propagate span context. NewParentSpanIdcolumn on the Job entity stores the spawner's span ID. - Span Attributes — Job execution spans include OTel semantic convention tags (
messaging.system,messaging.destination.name,messaging.operation.name,messaging.message.id) and Jobly-specific tags (jobly.job.type,jobly.job.kind,jobly.job.status,jobly.job.duration_ms,jobly.job.retry_count). Failed spans are marked withActivityStatusCode.Error. - Span Events — Key lifecycle moments recorded as events on the span:
jobly.job.completed,jobly.job.failed(with exception info),jobly.job.retried(with retry/max counts),jobly.job.cancelled. - OTel Metrics — Four
System.Diagnostics.Metricsinstruments via aMeternamed"Jobly":jobly.job.duration(histogram, ms),jobly.job.active(up-down counter),jobly.job.completed(counter with status tag),jobly.job.enqueued(counter with kind tag). All tagged by queue and type for filtering. - Automatic Log Correlation —
AddJoblyWorkerconfiguresActivityTrackingOptionsso TraceId, SpanId, and ParentId appear in log output by default. No additional configuration needed.
Integration
All features are on by default with zero configuration. To export to OTel backends:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("Jobly"))
.WithMetrics(m => m.AddMeter("Jobly"));
Migration
This release adds a new nullable ParentSpanId column to the Job table. Run an EF Core migration after upgrading.
Bug Fixes
- Distributed lock credential stripping — Connection string resolution now reads from
DbContextOptionsRelationalOptionsExtension instead ofDatabase.GetConnectionString(), which strips passwords via NpgsqlPersistSecurityInfo=false. Also handlesNpgsqlDataSourceconfigurations. - Server status indicators — Dashboard servers page now shows heartbeat-based status dots: green (active), red (stale >30s), amber (paused). "Inactive" badge shown when heartbeat is stale.
Stats
- 658 tests (310 PostgreSQL + 310 SQL Server + 38 unit)
0.5.0
2026-04-09
New Features
- Job Metadata — Attach key-value metadata to jobs at publish time via
JobParameters.Metadata. Metadata is inherited by child jobs, accessible in handlers viaIJobContext, and visible in the dashboard. NewIPublishPipelineBehavior<T>interface for cross-cutting metadata (e.g., adding tenant ID to every job automatically). - Pause / Resume — Pause and resume job processing at the server or worker group level via dashboard or API. Paused workers stop picking up new jobs; in-progress jobs continue to completion.
- Real-time Handler Logs — Handler
ILoggeroutput is now flushed to the database every ~1 second during execution, instead of only after the handler completes. Logs are visible in the dashboard while the job is still processing. - Multi-server Integration Tests — 16 new tests (8 per database) verify distributed coordination: row locks, advisory locks, orchestration, message routing, and mutex enforcement across two independent servers sharing one database.
- Deterministic Query Ordering — Job and message fetch queries now use explicit ordering by queue and schedule time, ensuring predictable behavior in multi-server deployments.
- Naming Convention Support — Entity configurations respect EF Core naming conventions (e.g.,
UseSnakeCaseNamingConvention()). All Jobly tables default to thejoblyschema, configurable viaJoblyConfiguration.Schema. - Configurable Handler Logging —
EnableHandlerLoggingoption (default true) to suppress handlerILoggeroutput from the JobLog table when not needed. Lifecycle events are always recorded. - AI-friendly Documentation — Added
llms.txtandllms-full.txtfor LLM/agent consumption, following the llms.txt convention.
Improvements
- Sidebar reorganized into logical groups: Patterns, Features, Operations, Dashboard
- Dashboard shows metadata alongside job payload as formatted JSON
- NuGet badges added to README
- Deterministic query ordering for predictable multi-server behavior
Stats
- 632 tests (316 PostgreSQL + 316 SQL Server)
0.4.0
2026-04-08
New Features
- Source Generator — Zero-allocation mediator and worker dispatch via compile-time source generation. Replaces runtime reflection in
JobDispatcherfor handler discovery and execution.
Links
0.3.0
2026-04-07
New Features
- Initial public release with core job processing, message queue, in-memory mediator, dashboard, recurring jobs, batches, cancellation, mutex, crash recovery, and tracing.