Add logger, requeue, config safety and agent docs for 3.0.0 - #63
Merged
Merged
Conversation
QueueableJobSetting__mdt is editable in production with no deploy and no test run, and the "All" record reaches every job in the org. Three settings could throw from addJob, so a single typo there stopped every async job at once: MaxRetries__c above the cap, an unknown BackoffStrategy__c, and, since the state gate landed, MaxRetries__c on any job that had not declared how its state resets. The last one is the worst, because turning retry on org-wide is a reasonable thing for an admin to try. Configuration is now treated as untrusted input from a live system. Each case degrades to the safe behaviour, records the reason on the job's retry history so it reaches AsyncResult__c.RetryHistory__c, and writes it to the debug log: retry is not applied, backoff is dropped, the count is clamped. Every fallback degrades toward doing less, never toward doing something the developer did not ask for. Skipping retry leaves the job running exactly once, which is what its code was written and tested against; silently enabling state-carrying retry would not have been safe. Mistakes written in Apex still throw, unchanged. A developer calling retry(n) without a declared reset gets the same loud gate as before, because it is their own code, it fails in their own tests, and the blast radius is one class. Two tests that asserted the old throwing behaviour are removed rather than converted, since the behaviour they pinned no longer exists. A new test pins that the code-triggered path still throws, so the asymmetry itself is covered. website/explanations/configuration-safety.md explains why the two are treated differently, since on the surface it reads as an inconsistency.
Closes #58. Standardising on an external logger meant overriding onFinalFailure in every job, maintaining a shared base class and relying on discipline, or bolting a trigger onto AsyncResult__c and storing records nobody wanted. Registering one class in QueueableJobSetting__mdt.LoggerClass__c now routes every job to it, with the All record covering the org and per-job records overriding it, reusing the lookup retry settings already use. No new metadata type. Events are four separate capability interfaces rather than one base class or one fat interface. Apex has no default methods, so a fat interface could never gain a fifth member without breaking every implementer at install time, and a base class would spend the consumer's single inheritance slot on a logging adapter. Separate interfaces mean a fifth event is additive and a logger implements only what it wants. The same interfaces work directly on a QueueableJob, so per-job hooks needed no new members on the class every consumer extends. The registered class must be declared global. Measured on a real package install: Type.forName from package code resolves a global subscriber class through either the one-arg or empty-namespace form, and returns null for a public one. The 'c' namespace alias does not work. Only the class needs global, not its methods. info() attaches per-invocation key/value metadata that arrives on every context. A global logger is handed an interface-typed listener and cannot cast to a concrete job to read its fields, still less across a namespace, so this is the only channel from the enqueue site to the org-wide sink. Being per invocation rather than per class, the same job enqueued by two flows can be attributed differently. FailureContext also gains nextAttemptDelayMinutes. A listener that throws is caught and reported; the job has already done its work by the time most events fire, so failing it would turn an observability problem into a data problem. A LoggerClass__c that cannot be resolved degrades to no logging and records why, per the configuration safety rule. docs/code-style.md writes down the comment policy that pmd/ruleset.xml already referenced but that existed nowhere in the repo, and links it from the PR checklist.
Custom Metadata cannot be inserted in Apex, and the injection point the framework used was a @testvisible member on QueueableChain, which is not global. So a subscriber could not test any QueueableJobSetting__mdt behaviour: retry defaults, backoff, retryable exceptions, result creation, disabled jobs, or the logger registered in the previous commit. Step 6b caught this, because the consumer test for logger routing would not even compile against an installed package. AsyncMock.jobSettings(List<QueueableJobSetting__mdt>) is the way in. AsyncMock is already the consumer-facing testing class and is @istest, so the guard against production use is structural rather than a Test.isRunningTest() throw. Settings now live in one static property that lazily loads Custom Metadata unless something has been injected, replacing a per-chain @testvisible property, a branch on Test.isRunningTest() in the read path, and a second parallel seam. The setter is private with @testvisible on the property, so production code in the namespace cannot assign org configuration by accident. Note that @testvisible has no effect when placed on the accessor; it has to sit on the property declaration, and the difference compiles either way. The old getter rebuilt the map on every read, and it is read by resolveRetrySetting, loggerClassFor and resultEnabledFor, so a chain of N jobs rebuilt it many times per transaction. It is now built once, which is the correct scope because Custom Metadata cannot change mid-transaction. The 21 framework tests that assigned the per-chain property move to the static, so there is one seam rather than two, and NsTestLogging drops its reach into QueueableChain. That test now uses the btcdev__ prefixed Custom Metadata form, which resolves both in a namespaced org and in a subscriber org, so a single file exercises the same API a real consumer writes. Writing consumer tests against a path consumers cannot use is what hid this defect in the first place.
AsyncResult__c showed 4 of its 15 fields, without Status__c or ExceptionMessage__c, so an admin opening a failed job saw nothing useful. QueueableJobSetting__mdt showed 3 of 8, hiding retry, backoff and the LoggerClass__c that aae5446 shipped. Neither had a related list. Every field is on its layout now, grouped, and AsyncResult__c gains a related list for DependsOnResult__c. CONTRIBUTING gets the checklist line that would have caught this.
Async.requeue(resultIds) rebuilds jobs from a snapshot stored on their result row and runs them again as one chain. The snapshot is the job as it was handed to enqueue(), taken before any chain bookkeeping, so a replay starts where the original did. Storage is opt-in through QueueableJobSetting__mdt.StoreJobPayload__c, a picklist so a job record can say No against an org-wide Yes. Storing a payload writes a row for failed and skipped jobs regardless of CreateResult__c; successful rows never carry one, so a bulk replay cannot re-run a job that worked. New on AsyncResult__c: JobPayload__c, PayloadSize__c, RequeueStatus__c, RequeuedFrom__c. Selection goes through RequeueStatus__c because Long Text cannot be filtered on, and Requeued on the source is what stops a scheduled replay picking a row up twice. Payloads are read in a second FOR UPDATE query only after the 2,000,000-character budget passes. JSON cannot cross a namespace boundary in either direction, so a packaged install registers one global Async.JobSerializer in JobSerializerClass__c; extras/classes/AsyncJobSerializer.cls is ready to copy. GlobalClassFactory now builds any class named in Custom Metadata, for the logger and the serializer alike, and QueueableManager.settingFor replaces three copies of the job-record-or-All lookup. QueueableJob.className is no longer transient. It was recomputed on every hop for every job by throwing a TypeException, 7,400 times in a 37-hop test. Docs: a requeue explanation, and installation split into a hub with one guide for the package and one for a source deploy. The deploy button now pins the release tag and release.sh bumps it with the package id. AsyncTest is reordered: fields, tests, helpers, inner types. Closes #50
website/ai-usage.md is the whole public surface on one page: every Async.* entry point, every builder method, what you implement, what you get back, the Custom Metadata fields, AsyncResult__c, AsyncMock, seven recipes and the gotchas an agent gets wrong on the first try. Signatures come from the API files, not from memory. llms.txt and llms-full.txt were already generated from the sidebar; the new page is listed there and the index now opens by pointing agents at it. CONTRIBUTING gets the rows that keep the page, the consumer tests, the extras tables and the sidebar in step with the code. Closes #59
The notes covered the breaking change, the logger, requeue and the layout fix, and missed the rest of the release: the Async.Backoff factories, three of the four fixes (rollback leak, silent deepClone retry, Custom Metadata landmines), the className recompute, the agent cheat-sheet with llms.txt, the installation split and PMD in CI. Every entry links to the page that goes deeper.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🧪 Apex Test Results✅ All Tests Passed📦 Download Full Test Results & Logs 📊 Stats: 310 total | ✅ 310 passed | ❌ 0 failed |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #63 +/- ##
==========================================
+ Coverage 98.54% 99.11% +0.56%
==========================================
Files 23 27 +4
Lines 1581 1919 +338
==========================================
+ Hits 1558 1902 +344
+ Misses 23 17 -6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Codecov reported 34 patch lines with no test. Most were paths the consumer-app suite exercises but the source run never did: info() on both builders, the single-id requeue overload, the empty-set return, a logger whose constructor throws, and the two-argument type lookup for an inner class name. The last four were the re-check after the row lock, reachable only when two transactions requeue the same rows at once, which an Apex test cannot produce. The lock now happens in the first query instead of the second, so the re-check has nothing left to check and is gone. Claude-Session: https://claude.ai/code/session_01WXtSWhbJQUNGyguG7x3uor
Mateusz7410
force-pushed
the
feature/async-0010
branch
from
September 15, 2026 18:08
3a96f84 to
34176f2
Compare
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second half of 3.0.0. The breaking change (jobs must declare how state resets between runs) and the
Async.Backofffactories are already onmain; this PR carries the rest.globalclass inQueueableJobSetting__mdt.LoggerClass__cand every job reports to it. Four capability interfaces,Async.OnJobEnqueued/OnJobSucceeded/OnJobFailed/OnRetryEnqueued, usable on a job directly or on the registered class.info(key, value)attaches metadata that arrives on every context. A listener that throws never affects the job. LoggingAsync.requeue(resultIds)rebuilds failed jobs from a snapshot stored onAsyncResult__cand runs them as one chain. Opt-in throughStoreJobPayload__c, a picklist so a job record can sayNoagainst an org-wideYes. Works withCreateResult__coff: storing a payload writes a row for failed and skipped jobs on its own, never for successful ones. Packaged installs register oneglobalAsync.JobSerializer, ready to copy fromextras/, because JSON cannot cross a namespace boundary in either direction. Four new fields,JobPayload__c,PayloadSize__c,RequeueStatus__c,RequeuedFrom__c. RequeueAllrecord could throw from enqueue and stop every async job at once. Each now falls back to the safe behaviour and records why inRetryHistory__c. Mistakes written in Apex still throw. Configuration SafetyAsyncMock.jobSettings(...)injectsQueueableJobSetting__mdtrecords in tests, so all ten settings are testable by consumers without real Custom Metadata.website/ai-usage.md: the whole public surface on one page with recipes and gotchas, signatures taken from the API files.llms.txtnow points agents at it. Async Lib for AI Agentsrelease.shbumps it alongside the package id.AsyncResult__cshowed 4 of 15 fields, withoutStatus__corExceptionMessage__c;QueueableJobSetting__mdtshowed 3 of 8. Every field is on its layout now, with related lists forDependsOnResult__candRequeuedFrom__c.QueueableJob.classNameis no longertransient. It was recomputed on every hop for every job by throwing aTypeException, 200 exceptions per hop in a 200-job chain.GlobalClassFactorybuilds any class named in Custom Metadata,QueueableManager.settingForreplaces three copies of the job-record-or-Alllookup,AsyncTestreordered into fields, tests, helpers, inner types.release-notes/v3.0.0.mdis the full release text, breaking change included, with a link from every entry to the page that goes deeper.Why
Closes the five-issue scope for 3.0.0. The logger and requeue are the two operational features the release was planned around; the config-safety fix came out of asking what else a single Custom Metadata typo could take down; the layouts, the cheat-sheet and the installation split fell out of doing those properly.
Test plan
bash scripts/agent/verify.sh staticpassed (prettier + eslint + PMD)bash scripts/agent/verify.sh fullpassed, 307/307 at 97%bash scripts/agent/verify.sh nspassed, 27/27--no-namespacescratch,package-tests/consumer-appandextras/classesdeployed, 37/37 consumer tests against the installed package. Throwaway version deleted from the dev hub./llms.txtand/ai-usage.mdonce.Files
56 files, +5,458 / -1,363. Most of the
AsyncTest.clscount is the reorder; content is line-for-line identical apart from the 26 new tests.Closes #50
Closes #58
Closes #59