toga-ai 1.0.456 → 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.
|
@@ -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
|
|
@@ -138,22 +138,30 @@ Do not add a worker class without registering it. Unregistered workers cannot be
|
|
|
138
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.
|
|
139
139
|
|
|
140
140
|
```php
|
|
141
|
-
public function
|
|
141
|
+
public static function ProcessOrder(int $orderId): string {
|
|
142
142
|
try {
|
|
143
|
-
|
|
143
|
+
self::processOrder(orderId: $orderId);
|
|
144
144
|
} catch (_Exception_OrderNotFound $e) {
|
|
145
145
|
// Do not retry — log and discard
|
|
146
146
|
error_log('Worker: order not found, discarding job: ' . $e->getMessage());
|
|
147
|
-
return;
|
|
147
|
+
return json_encode(['isSuccess' => false, 'isRetriable' => false, 'orderId' => $orderId, 'error' => $e->getMessage()]);
|
|
148
148
|
} catch (_Exception_Database $e) {
|
|
149
|
-
// Retriable — re-throw so queue retries
|
|
149
|
+
// Retriable — re-throw so the queue retries
|
|
150
150
|
error_log('Worker: DB error, will retry: ' . $e->getMessage());
|
|
151
151
|
throw $e;
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
return json_encode(['isSuccess' => true, 'orderId' => $orderId]);
|
|
153
155
|
}
|
|
154
156
|
```
|
|
155
157
|
|
|
156
|
-
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.
|
|
157
165
|
|
|
158
166
|
## Change history
|
|
159
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