toga-ai 1.0.455 → 1.0.457
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/api2/architecture.md +37 -6
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/architecture.md +38 -1
- package/knowledge/2.0/apps/worker2/features/platform-cache-cleanup.md +80 -11
- package/knowledge/2.0/standards/framework-rules.md +42 -20
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: [jcardinal, bala, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Controller/Index.php
|
|
@@ -141,6 +141,8 @@ Aliases: `DB_CORE`, `DB_CLIENT`, `DB_CLIENT_LOGS`, `DB_CLIENT_ARCHIVE`, `DB_LOGS
|
|
|
141
141
|
`DB_STORE_1`/`DB_VISION_1` (legacy). `_Database::registerClientDatabases(clientId,
|
|
142
142
|
environment)` joins `Clients`/`Databases`/`DatabaseHosts`/`Environments`, preferring the
|
|
143
143
|
instance's own region; reads→readers, writes→writers, per-connection transactions.
|
|
144
|
+
`DB_CACHE` (shared Cache cluster) is resolved **by name** (`Databases.name = 'Cache'`) — never by
|
|
145
|
+
a hardcoded id, which differs per Core instance.
|
|
144
146
|
|
|
145
147
|
## Deployment (EB)
|
|
146
148
|
|
|
@@ -169,11 +171,39 @@ build. These must be **rotated** (treat the committed tokens as compromised) and
|
|
|
169
171
|
Parameter Store / EB env properties. **Flag this if you touch config or deploy.** (Location +
|
|
170
172
|
remediation only — do not record the token value anywhere.)
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
## Known issues / accepted risks
|
|
175
|
+
|
|
176
|
+
Open items a maintainer should know before changing this tier. None are "bugs to fix right now" —
|
|
177
|
+
they are the known sharp edges. Do not re-discover these from scratch.
|
|
178
|
+
|
|
179
|
+
1. **Raw exception disclosure to clients (deferred, separate ticket).** The error paths that
|
|
180
|
+
surface a caught `\Throwable` (now that Route.php rethrows and the bootstrap guard reports
|
|
181
|
+
failures) must not return raw `getMessage()`/`getTrace()` output in the client-facing envelope —
|
|
182
|
+
that leaks internal paths, schema names, and stack frames to API consumers. Sanitize the client
|
|
183
|
+
envelope (generic message + code; full detail to Sentry/logs only). Not yet done.
|
|
184
|
+
2. **Committed plaintext credentials, not yet rotated.** See the Security note above —
|
|
185
|
+
`Config/*.ini` secrets and the per-env `.ebextensions/git.*.json` GitHub PAT. Treat as
|
|
186
|
+
compromised until rotated and moved to SSM / EB env properties.
|
|
187
|
+
3. **The pre-execute phase still runs before `execute()`'s try/catch.** The 2026-07-23 fix wraps the
|
|
188
|
+
Core/Logs bootstrap specifically; it did not move the phase inside the main guard. Any *new* code
|
|
189
|
+
added to the pre-execute block is again outside `execute()`'s protection and must carry its own
|
|
190
|
+
`try/catch (\Throwable)`.
|
|
191
|
+
4. **Local Logs DB name mismatch reads as "missing".** The Core Logs schema name comes from a
|
|
192
|
+
`Core.Database` row (`id = CORE_LOGS_DATABASE_ID`); a local Logs DB whose actual schema name
|
|
193
|
+
differs throws `Unknown database`. This is environment config, not a code bug — fix the row or
|
|
194
|
+
the local schema name, don't patch the bootstrap.
|
|
195
|
+
5. **CORS is fully permissive.** `Access-Control-Allow-*` is wide open on every route. Acceptable
|
|
196
|
+
only because auth is bearer-token (not cookie) based — if any cookie/session-backed auth is ever
|
|
197
|
+
added here, this becomes an exploitable hole and must be tightened first.
|
|
198
|
+
6. **`_underscore` is cloned at build from a moving branch** (`_<ENVIRONMENT>`), not pinned to a
|
|
199
|
+
commit. Two deploys of the same api2 commit can produce different runtime behavior. Check the
|
|
200
|
+
framework branch state when triaging an "it worked yesterday" regression.
|
|
201
|
+
7. **`V2.php::execute()` is a ~2,000-line monolith** with `processRoutePairs()` recursion beneath
|
|
202
|
+
it. There is no unit-test harness around it; changes are validated by integration traffic. Make
|
|
203
|
+
surgical edits and preserve the transaction/logging invariant.
|
|
204
|
+
8. **JWT signing-secret rotation accepts current + previous.** During the overlap window a token
|
|
205
|
+
signed with the retired secret still validates. Revocation is therefore not immediate —
|
|
206
|
+
don't rely on rotation alone to lock out a compromised token.
|
|
177
207
|
|
|
178
208
|
## When making changes here
|
|
179
209
|
|
|
@@ -191,5 +221,6 @@ internal paths, schema names, and stack frames to API consumers. Sanitize the cl
|
|
|
191
221
|
or `execute()`.
|
|
192
222
|
|
|
193
223
|
## Change history
|
|
224
|
+
- 2026-07-28 — Added a consolidated **Known issues / accepted risks** section (8 items), absorbing the previously free-floating deferred raw-exception-disclosure follow-up as item 1, so the tier's sharp edges (unrotated committed secrets, pre-execute phase still outside the main guard, local Logs DB name mismatch, permissive CORS, unpinned `_underscore` build clone, untested `V2.php` monolith, JWT rotation overlap window) are in one place instead of scattered. Recorded that `DB_CACHE` is resolved by name (`Databases.name = 'Cache'`), never by a hardcoded id, which differs per Core instance. (jcardinal)
|
|
194
225
|
- 2026-07-27 — Sharpened the committed-secret note: the plaintext GitHub PAT lives in the per-env **`.ebextensions/git.*.json`** files (used by the `prebuild/git.sh` clone hook to pull `_underscore`), must be rotated and moved to SSM / EB env properties (location + remediation only, no value). (mhammontree)
|
|
195
226
|
- 2026-07-23 — Documented the now-guarded Core/Logs DB bootstrap in the front controller: the pre-execute block runs before the `execute()` try/catch, the Core Logs schema name is resolved from a `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`) so a name-mismatched local Logs DB reads as missing, and the failure is now wrapped in `try/catch (\Throwable)` returning `INVALID_CONFIGURATION` + Sentry instead of a fatal (guarded no-op rollback, `Database.php:219–226`). Added the deferred raw-getMessage/getTrace client-disclosure follow-up to the Security note. (jcardinal)
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
25
25
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
26
26
|
| [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php |
|
|
27
|
-
| [Platform Cache Cleanup (_Worker_Platform_Cache
|
|
27
|
+
| [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
28
28
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
29
29
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
30
30
|
| [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Controller/Index.php
|
|
@@ -163,6 +163,37 @@ file `Worker/Team/Github.php`.
|
|
|
163
163
|
`_Worker_Infrastructure_Worker_Cleanup`: `WorkerJobs()` deletes `isSuccess=1` rows >90 days
|
|
164
164
|
(≤1000/run); `WebhookLogs()` deletes `Logs.Webhook` >90 days. Two daily CronJobs at 2 AM.
|
|
165
165
|
|
|
166
|
+
## Cron actions must return a result on BOTH paths
|
|
167
|
+
|
|
168
|
+
**A new cron action is expected to return a result for a success as well as a failure.** Both
|
|
169
|
+
outcomes are recorded against the job, and the **successful** results are what auditing reads — a
|
|
170
|
+
run that completed but returned nothing is indistinguishable in the audit trail from one that never
|
|
171
|
+
executed.
|
|
172
|
+
|
|
173
|
+
So an action must not return only on the failure path, and must not return nothing on success
|
|
174
|
+
because "there was nothing to do". Return a result describing the outcome — including the no-op case
|
|
175
|
+
(`0 deleted, 0 dropped` is a meaningful audit record; silence is not). Make it structured and
|
|
176
|
+
countable rather than prose, so one run can be compared against its neighbours:
|
|
177
|
+
|
|
178
|
+
```php
|
|
179
|
+
public static function Clean(): string {
|
|
180
|
+
// ... work ...
|
|
181
|
+
return json_encode([
|
|
182
|
+
'isSuccess' => true,
|
|
183
|
+
'deletedRowCount' => $deletedRowCount,
|
|
184
|
+
'droppedTableCount' => $droppedCount,
|
|
185
|
+
'failedDropCount' => $failedCount,
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Reserve throwing for genuinely **retriable** conditions, where the queue re-running the job is the
|
|
191
|
+
behaviour you want. A handled, non-retriable failure should return a result saying so rather than
|
|
192
|
+
throwing — throwing costs the full retry cycle and still records nothing useful for the audit.
|
|
193
|
+
|
|
194
|
+
See the worker contract in [2.0 framework rules](../../standards/framework-rules.md) for the
|
|
195
|
+
class/method shape and the named-argument parameter contract.
|
|
196
|
+
|
|
166
197
|
## Critical transaction pattern
|
|
167
198
|
|
|
168
199
|
**Any INSERT/UPDATE that must be visible to another connection before an SQS message is
|
|
@@ -229,6 +260,12 @@ the MySQL-first design was departed from **on purpose** here.
|
|
|
229
260
|
|
|
230
261
|
## Change history
|
|
231
262
|
|
|
263
|
+
- 2026-07-28 — Added **Cron actions must return a result on BOTH paths**: a new cron action is
|
|
264
|
+
expected to return a structured result for a success as well as a failure, because both are
|
|
265
|
+
recorded against the job and the successful results are what auditing reads (a completed run that
|
|
266
|
+
returned nothing is indistinguishable from one that never executed). Formalises the intent of the
|
|
267
|
+
2026-07-21 `output` change below. Also documents `_Worker_Platform_Cache::Truncate()`, a manual
|
|
268
|
+
companion to the scheduled `Clean()` action. (jcardinal)
|
|
232
269
|
- 2026-07-21 — `Core.WorkerJobs.failureReason` renamed to `output` and repurposed to capture
|
|
233
270
|
successful run results as well as failures (success writes the composed "Successfully Executed"
|
|
234
271
|
message, return value `json_encode`d). Multi-producer rename: PHP worker (both paths), `Retry()`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Platform Cache Cleanup (_Worker_Platform_Cache
|
|
2
|
+
title: Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: worker2
|
|
5
5
|
project: Worker
|
|
@@ -17,20 +17,28 @@ related:
|
|
|
17
17
|
- ../../api2/features/cross-client-data-retrieval.md
|
|
18
18
|
- ./creating-worker-actions.md
|
|
19
19
|
- ../../_underscore/features/per-client-database-connections.md
|
|
20
|
+
- ../architecture.md
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
## Summary
|
|
23
24
|
|
|
24
|
-
`_Worker_Platform_Cache
|
|
25
|
-
[multi-client data retrieval](../../api2/features/cross-client-data-retrieval.md). It
|
|
26
|
-
|
|
27
|
-
longer have a parent. Without it the Cache schema grows one table per distinct cross-client query,
|
|
28
|
-
forever.
|
|
25
|
+
`_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's
|
|
26
|
+
[multi-client data retrieval](../../api2/features/cross-client-data-retrieval.md). It exposes **two**
|
|
27
|
+
actions on the same class (both listed in the class docblock in `Worker/Platform/Cache.php`):
|
|
29
28
|
|
|
30
|
-
Action
|
|
31
|
-
|
|
29
|
+
| Action | Path | Scheduled? | Scope |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `Clean()` | `Platform/Cache/Clean` | yes — `*/5` cron | **expired** entries only, with a 2-minute grace |
|
|
32
|
+
| `Truncate()` | `Platform/Cache/Truncate` | **no — deliberately manual** | **everything**, no grace, spares nothing |
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Without `Clean()` the Cache schema grows one `TableResults_<Tables.id>` table per distinct
|
|
35
|
+
cross-client query, forever. `Truncate()` is the administrative "reset the whole cache" hammer.
|
|
36
|
+
|
|
37
|
+
The `Clean()` cron is registered in `dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql`.
|
|
38
|
+
Both actions return a summary string on **every** path (success and handled failure) — see
|
|
39
|
+
[cron actions must return a result on both paths](../architecture.md#cron-actions-must-return-a-result-on-both-paths).
|
|
40
|
+
|
|
41
|
+
## How it works — `Clean()` (scheduled reaper)
|
|
34
42
|
|
|
35
43
|
1. **Reap expired parents.** Delete `Cache.Tables` rows whose TTL has elapsed, with a **2-minute
|
|
36
44
|
grace period** so a request that is mid-read cannot have its rows deleted out from under it.
|
|
@@ -41,6 +49,44 @@ Action path `Platform/Cache/Clean`, scheduled on a **`*/5`** cron registered in
|
|
|
41
49
|
status is **re-verified on the write host immediately before each `DROP`** — a `DROP TABLE` is
|
|
42
50
|
irreversible and non-transactional, so a stale read replica must never be the basis for one.
|
|
43
51
|
|
|
52
|
+
## How it works — `Truncate()` (manual, unscheduled)
|
|
53
|
+
|
|
54
|
+
`_Worker_Platform_Cache::Truncate(): string` removes the **entire** cross-client cache. It is
|
|
55
|
+
deliberately **not** registered as a cron — invoke it explicitly:
|
|
56
|
+
|
|
57
|
+
```php
|
|
58
|
+
_Worker::runTask('Platform/Cache/Truncate', []);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
1. `_Database::useQueryCache(bool: false)` at the top, because the action re-reads tables it has
|
|
62
|
+
just emptied and a cached result set would make it act on stale contents.
|
|
63
|
+
2. `DELETE FROM Tables` — `Tables_Clients` follows automatically via `ON DELETE CASCADE`.
|
|
64
|
+
3. A **defensive** `DELETE FROM Tables_Clients` afterwards, to catch any cursor row orphaned by an
|
|
65
|
+
earlier partial failure (the cascade only covers rows whose parent still existed).
|
|
66
|
+
4. Drop every `TableResults_*` table.
|
|
67
|
+
|
|
68
|
+
**Each delete is committed immediately** with
|
|
69
|
+
`_Database::transactionCommit(database: _underscore::DB_CACHE)`. This is not optional:
|
|
70
|
+
`_Database::register()` opens a **lazy transaction**, so anything left uncommitted is rolled back
|
|
71
|
+
at connection close and the truncate silently does nothing.
|
|
72
|
+
|
|
73
|
+
Drops still go through the existing `dropResultTable()` **allowlist** (`^TableResults_[1-9][0-9]*$`),
|
|
74
|
+
so a table that merely shares the prefix is never dropped. A drop failure is logged and counted and
|
|
75
|
+
does **not** abort the remaining drops.
|
|
76
|
+
|
|
77
|
+
### `Clean()` vs `Truncate()` — the difference that matters
|
|
78
|
+
|
|
79
|
+
| | `Clean()` | `Truncate()` |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Selects | **expired** entries only | **everything** |
|
|
82
|
+
| Grace period | **2 minutes** — in-flight requests keep their rows | **none** |
|
|
83
|
+
| Safe on a busy environment | yes, by design | **no** |
|
|
84
|
+
|
|
85
|
+
`Truncate()` gives a request that is mid-build no protection: it loses its rows and serves an empty
|
|
86
|
+
or short page. Nothing is *corrupted* — the cache is derived data and rebuilds on the next request —
|
|
87
|
+
but the visible-wrong-output window is real, so it must not be run casually against a busy
|
|
88
|
+
environment.
|
|
89
|
+
|
|
44
90
|
## Key rules
|
|
45
91
|
|
|
46
92
|
- **Delete-parent-then-drop-child, and re-check on the writer.** Never drop a result table based on
|
|
@@ -51,18 +97,41 @@ Action path `Platform/Cache/Clean`, scheduled on a **`*/5`** cron registered in
|
|
|
51
97
|
(`Databases.name = 'Cache'`) in `worker2/Controller/Index.php` — the same rule as api2. There is
|
|
52
98
|
deliberately no `CACHE_DATABASE_ID`: `Databases.id` is auto-assigned per Core instance and a
|
|
53
99
|
hardcoded id fails silently in every environment where it does not happen to match.
|
|
100
|
+
- **Commit each delete immediately in `Truncate()`.** `_Database::register()`'s lazy transaction
|
|
101
|
+
rolls back at connection close, so an uncommitted truncate is a no-op that *looks* like it worked.
|
|
102
|
+
- **Never bypass `dropResultTable()`'s allowlist.** `^TableResults_[1-9][0-9]*$` is the only thing
|
|
103
|
+
standing between a prefix-sharing table and an irreversible `DROP`.
|
|
104
|
+
- **Both actions return a summary string on every path** — success, no-op, and handled failure. See
|
|
105
|
+
the [worker2 architecture rule](../architecture.md#cron-actions-must-return-a-result-on-both-paths);
|
|
106
|
+
a run that returns nothing is indistinguishable in the audit trail from one that never executed.
|
|
54
107
|
|
|
55
108
|
## Gotchas
|
|
56
109
|
|
|
57
110
|
- **Worker classes are `abstract class _Worker_<Path>` with `public static` entry methods**,
|
|
58
111
|
dispatched as `$className::$functionName()`. They do **not** `extends _Worker` — that is the
|
|
59
112
|
dispatcher. See [Creating Worker Actions](./creating-worker-actions.md). (The 2.0
|
|
60
|
-
`standards/framework-rules.md` worker contract
|
|
61
|
-
|
|
113
|
+
`standards/framework-rules.md` worker contract now matches this shape — reconciled
|
|
114
|
+
2026-07-28.)
|
|
62
115
|
- **Adding the cron is a dbchanges2 change, not a code change** — the schedule lives in Core, so a
|
|
63
116
|
new worker action is not actually scheduled until that SQL is applied per environment.
|
|
117
|
+
- **`Truncate()` is unscheduled on purpose.** Do not "helpfully" add a cron for it — it has no grace
|
|
118
|
+
period, so on a schedule it would periodically blank live requests' cache rows.
|
|
119
|
+
- **`ON DELETE CASCADE` is not sufficient on its own.** The extra `DELETE FROM Tables_Clients` in
|
|
120
|
+
`Truncate()` exists because an earlier partial failure can leave cursor rows whose parent is
|
|
121
|
+
already gone — the cascade never fires for those.
|
|
122
|
+
- **Stale reads inside a truncate.** Without `_Database::useQueryCache(bool: false)` the action
|
|
123
|
+
re-reads its own just-emptied tables from the query cache and mis-decides what is left.
|
|
64
124
|
|
|
65
125
|
## Change history
|
|
126
|
+
- 2026-07-28 — Added second action `_Worker_Platform_Cache::Truncate()` on the same class: manual /
|
|
127
|
+
administrative, deliberately **not** scheduled. Removes the entire cross-client cache
|
|
128
|
+
(`DELETE FROM Tables` + cascade, defensive `DELETE FROM Tables_Clients` for orphaned cursors, then
|
|
129
|
+
drops every `TableResults_*`), committing each delete immediately because `_Database::register()`'s
|
|
130
|
+
lazy transaction would otherwise roll it back at connection close; `useQueryCache(false)` since it
|
|
131
|
+
re-reads tables it just emptied; drops still gated by the `dropResultTable()` allowlist and a
|
|
132
|
+
failed drop is logged/counted without aborting. Unlike `Clean()` it applies **no** grace period —
|
|
133
|
+
an in-flight request loses its rows and serves a short page (no corruption; cache rebuilds).
|
|
134
|
+
Both actions return a summary string on every path. (jcardinal)
|
|
66
135
|
- 2026-07-28 — Initial capture: `_Worker_Platform_Cache::Clean()` reaps expired `Cache.Tables` rows
|
|
67
136
|
(2-minute grace) and drops orphaned `TableResults_*` tables (parent deleted+committed first,
|
|
68
137
|
orphan status re-verified on the write host immediately before each DROP); `DB_CACHE` registered
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-07-28
|
|
9
9
|
owners: [jcardinal]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -26,41 +26,52 @@ All framework classes use a leading underscore prefix:
|
|
|
26
26
|
|
|
27
27
|
- Controllers: `_Controller` base, subclasses `_Controller_<Name>`
|
|
28
28
|
- Models: `_Model` base, subclasses `_Model_<Name>`
|
|
29
|
-
- Workers: `_Worker`
|
|
29
|
+
- Workers: `_Worker` dispatcher; action classes are `abstract class _Worker_<Name>` with static entry methods
|
|
30
30
|
- API handlers: `_Api` base or `_Api_<Name>`
|
|
31
31
|
|
|
32
32
|
Application code in `worker2` and `api2` follows the same leading-underscore convention for framework-extending classes.
|
|
33
33
|
|
|
34
34
|
## Worker contract
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Worker action classes **do not extend `_Worker`** and have no `run()` method. A worker is an
|
|
37
|
+
`abstract class` of static entry methods, one method per queue action, mirrored by its file path.
|
|
37
38
|
|
|
38
39
|
```php
|
|
39
|
-
// CORRECT
|
|
40
|
-
class
|
|
41
|
-
public function
|
|
42
|
-
//
|
|
40
|
+
// CORRECT — worker2/Worker/Platform/Cache.php
|
|
41
|
+
abstract class _Worker_Platform_Cache {
|
|
42
|
+
public static function Clean(int $graceSeconds = 60): void {
|
|
43
|
+
// do the work; parameters arrive as named arguments
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
The
|
|
48
|
+
The class is `abstract` (never instantiated), the entry methods are `public static`, and the
|
|
49
|
+
action's queue name is the `Category/Sub/File/MethodName` path — `Platform/Cache/Clean` above.
|
|
50
|
+
Job data arrives as **method parameters**, not as a payload property: the dispatcher spreads the
|
|
51
|
+
parameter array as named arguments, so parameter names are part of the public contract. Renaming
|
|
52
|
+
a parameter is a breaking change to every queued job already in flight. Give parameters defaults
|
|
53
|
+
where a sensible one exists, so an older enqueued job missing a newly added key still runs.
|
|
48
54
|
|
|
49
55
|
## Worker dispatch — never call directly
|
|
50
56
|
|
|
51
|
-
Workers must only be invoked
|
|
57
|
+
Workers must only be invoked through the queue. Never call a worker's entry method directly from a
|
|
58
|
+
controller, another worker, or any non-queue context.
|
|
52
59
|
|
|
53
60
|
```php
|
|
54
61
|
// CRITICAL VIOLATION — direct call
|
|
55
|
-
|
|
56
|
-
$worker->run();
|
|
62
|
+
_Worker_Platform_Cache::Clean();
|
|
57
63
|
|
|
58
|
-
// CORRECT — dispatch through queue
|
|
59
|
-
|
|
60
|
-
// or the framework's equivalent dispatch method
|
|
64
|
+
// CORRECT — dispatch through the queue
|
|
65
|
+
_Worker::runTask('Platform/Cache/Clean', ['graceSeconds' => 120]);
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
`_Worker::runTask(string $action, array|object $parameters)` is the only supported entry point. The
|
|
69
|
+
action is the `Category/Sub/File/MethodName` path; `parameters` is string-keyed and is spread as
|
|
70
|
+
**named arguments** at dispatch (`$className::$functionName(...$parameters)`), so every key must
|
|
71
|
+
match a parameter name on the target method.
|
|
72
|
+
|
|
73
|
+
Direct calls bypass retry logic, error handling, SQS visibility timeout, WorkerJobs tracking and
|
|
74
|
+
monitoring. Even in development, dispatch through the queue — do not invoke the method directly.
|
|
64
75
|
|
|
65
76
|
## API response envelope (api2 only)
|
|
66
77
|
|
|
@@ -127,19 +138,30 @@ Do not add a worker class without registering it. Unregistered workers cannot be
|
|
|
127
138
|
Workers must catch specific exceptions and handle them appropriately. A worker that throws an uncaught exception will be retried by the queue (up to the configured retry limit) and then dead-lettered.
|
|
128
139
|
|
|
129
140
|
```php
|
|
130
|
-
public function
|
|
141
|
+
public static function ProcessOrder(int $orderId): string {
|
|
131
142
|
try {
|
|
132
|
-
|
|
143
|
+
self::processOrder(orderId: $orderId);
|
|
133
144
|
} catch (_Exception_OrderNotFound $e) {
|
|
134
145
|
// Do not retry — log and discard
|
|
135
146
|
error_log('Worker: order not found, discarding job: ' . $e->getMessage());
|
|
136
|
-
return;
|
|
147
|
+
return json_encode(['isSuccess' => false, 'isRetriable' => false, 'orderId' => $orderId, 'error' => $e->getMessage()]);
|
|
137
148
|
} catch (_Exception_Database $e) {
|
|
138
|
-
// Retriable — re-throw so queue retries
|
|
149
|
+
// Retriable — re-throw so the queue retries
|
|
139
150
|
error_log('Worker: DB error, will retry: ' . $e->getMessage());
|
|
140
151
|
throw $e;
|
|
141
152
|
}
|
|
153
|
+
|
|
154
|
+
return json_encode(['isSuccess' => true, 'orderId' => $orderId]);
|
|
142
155
|
}
|
|
143
156
|
```
|
|
144
157
|
|
|
145
|
-
Distinguish between retriable errors (transient DB/network issues — re-throw) and non-retriable errors (bad data, missing record — log and return).
|
|
158
|
+
Distinguish between retriable errors (transient DB/network issues — re-throw) and non-retriable errors (bad data, missing record — log and return a failure result).
|
|
159
|
+
|
|
160
|
+
**Always return a result — on the failure path as well as the success path.** Both outcomes are
|
|
161
|
+
recorded against the job, and the successful results are relied on for auditing, so a worker that
|
|
162
|
+
returns nothing (or returns only when it succeeds) leaves a blind spot in the audit trail. Re-throw
|
|
163
|
+
only for genuinely retriable conditions, where the queue's retry is the intended behaviour; a
|
|
164
|
+
handled non-retriable failure should return a result describing what happened rather than throwing.
|
|
165
|
+
|
|
166
|
+
## Change history
|
|
167
|
+
- 2026-07-28 — Corrected the **Worker contract** and **Worker dispatch** sections to match the actual codebase: worker actions are `abstract class _Worker_<Name>` with `public static` entry methods dispatched via `_Worker::runTask(string $action, array|object $parameters)` on the `Category/Sub/File/MethodName` path, with parameters spread as **named arguments** (`$className::$functionName(...$parameters)`, worker2 `Controller/Index.php:325,567`; `_underscore/Worker.php:8`). This replaces the previous `extends _Worker` / `run()` / `_Queue::dispatch()` guidance — no worker in worker2 follows it, `_Queue::dispatch()` does not exist in the codebase, and following the old text would produce a worker that never dispatches. (jcardinal)
|
package/package.json
CHANGED