fsanyoto commited on
Commit
e325595
Β·
verified Β·
1 Parent(s): b39ca59

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
Files changed (4) hide show
  1. RELEASES.json +7 -1
  2. VERSION +1 -1
  3. api/automation_engine.py +66 -27
  4. api/rollup_sql.py +13 -2
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "v36 (51db3b7)",
3
  "releases": [
 
 
 
 
 
 
4
  {
5
  "version": "v36",
6
  "sha": "51db3b7",
 
1
  {
2
+ "current": "v37 (1ddd8cf)",
3
  "releases": [
4
+ {
5
+ "version": "v37",
6
+ "sha": "1ddd8cf",
7
+ "date": "2026-08-21",
8
+ "subject": "W39: coalesce derived workspace reads"
9
+ },
10
  {
11
  "version": "v36",
12
  "sha": "51db3b7",
VERSION CHANGED
@@ -1 +1 @@
1
- v36 (51db3b7)
 
1
+ v37 (1ddd8cf)
api/automation_engine.py CHANGED
@@ -12626,11 +12626,13 @@ def _table_handle(row, url_field):
12626
  or "").strip().lower()
12627
 
12628
 
12629
- def compute_metric_cells(rt, table_key, today=None):
12630
  """Recompute every metric cell on ONE table from the master series. One coalesced write,
12631
  only when something actually changed (the flush-ceiling law); zero reads when the table
12632
  has no metric fields or the master is off. Returns the number of rows touched."""
12633
- t = ut_get(rt, table_key)
 
 
12634
  mfields = metric_fields_of_table(t)
12635
  if not mfields:
12636
  return 0
@@ -12664,6 +12666,11 @@ def compute_metric_cells(rt, table_key, today=None):
12664
  tt.setdefault("rows", {}).setdefault(rid, {}).update(vals)
12665
  return cur
12666
 
 
 
 
 
 
12667
  rt.update(UT_STORE_KEY, _up, flush="sync")
12668
  return len(changes)
12669
 
@@ -13204,13 +13211,30 @@ def _refresh_relations_inplace(tables, log=print):
13204
  return touched
13205
 
13206
 
13207
- def refresh_relations(rt, log=print):
13208
  """The tick half of the relational pass β€” the twin of `refresh_metrics`.
13209
 
13210
  ⚠ Runs for EVERY table, because a link can point anywhere: a rollup on table A goes stale
13211
  when table B gains a row, and A has no way to know that happened. Cheap by construction β€” a
13212
  table declaring neither kind costs one dict scan.
13213
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13214
  snapshot = {
13215
  str(key): {**(table or {}),
13216
  "rows": {str(rid): dict(row or {})
@@ -13244,15 +13268,17 @@ def refresh_relations(rt, log=print):
13244
  return actual[0]
13245
 
13246
 
13247
- def refresh_metrics(rt, today=None, log=print):
13248
  """The tick half of the C7 amendment: `today` advances at tick cadence, so a date-window
13249
  metric can never go staler than one tick while a scheduler exists. Cheap by construction β€”
13250
  a table without metric fields costs a dict scan and nothing else."""
 
13251
  touched = 0
13252
- for tk, t in ut_all(rt).items():
13253
  if metric_fields_of_table(t):
13254
  try:
13255
- touched += compute_metric_cells(rt, tk, today=today)
 
13256
  except Exception as e: # noqa: BLE001
13257
  log(f"[aios-auto] metric refresh {tk} failed: {type(e).__name__}: {e}")
13258
  return touched
@@ -14531,25 +14557,37 @@ def tick(rt, tenant, now=None, log=print):
14531
  # C7's amendment: date-window metrics advance with the tick, so `today` is never staler
14532
  # than one tick while a scheduler exists. A tenant without metric fields pays a dict scan.
14533
  try:
14534
- refresh_metrics(rt, log=log)
 
 
 
 
14535
  except Exception as e: # noqa: BLE001
14536
  _derived_ok = False
14537
- log(f"[aios-auto] metric refresh {tenant} failed: {type(e).__name__}: {e}")
14538
- # ⭐ 2026-08-07 β€” the relational pass, on the same tick and for the same reason. A rollup
14539
- # on table A goes stale when table B gains a row, and A cannot know that happened; the
14540
- # tick is the only place that sees both. Separate failure handling is retained.
14541
- try:
14542
- refresh_relations(rt, log=log)
14543
- except Exception as e: # noqa: BLE001
14544
- _derived_ok = False
14545
- log(f"[aios-auto] relation refresh {tenant} failed: {type(e).__name__}: {e}")
14546
- # Source-backed rollups move with Odoo and with their own date windows, so they share the
14547
- # same durable-revision/day gate but retain their independent failure posture.
14548
- try:
14549
- _refresh_source_rollups(rt, tenant, log=log)
14550
- except Exception as e: # noqa: BLE001
14551
- _derived_ok = False
14552
- log(f"[aios-auto] source rollup {tenant} failed: {type(e).__name__}: {e}")
 
 
 
 
 
 
 
 
14553
  if _derived_ok and _derived_marker is not None:
14554
  _DERIVED_TICK_STATE[str(tenant)] = _derived_marker
14555
  if started:
@@ -14603,7 +14641,7 @@ def _derived_refresh_due(rt, tenant, now=None, state=None, backend=None):
14603
  return memo.get(str(tenant)) != marker, marker
14604
 
14605
 
14606
- def _refresh_source_rollups(rt, tenant, log=print):
14607
  """Refresh every source-backed rollup; callers provide the no-op policy.
14608
 
14609
  The existing per-pass error boundary lives in :func:`tick`. Keeping this helper focused on
@@ -14611,10 +14649,11 @@ def _refresh_source_rollups(rt, tenant, log=print):
14611
  schedule, vendor call, or a hidden duplicate of the loop.
14612
  """
14613
  import rollup_sql
14614
- for _tk in list((rt.get(UT_STORE_KEY) or {})):
14615
- if rollup_sql.source_fields((rt.get(UT_STORE_KEY) or {}).get(_tk) or {}):
 
14616
  # `today` is DEFAULTED INSIDE `compute`, matching `refresh_metrics` above.
14617
- n = rollup_sql.compute(rt, _tk)
14618
  if n:
14619
  log(f"[aios-auto] source rollups {tenant}/{_tk}: {n}")
14620
 
 
12626
  or "").strip().lower()
12627
 
12628
 
12629
+ def compute_metric_cells(rt, table_key, today=None, tables=None, persist=False):
12630
  """Recompute every metric cell on ONE table from the master series. One coalesced write,
12631
  only when something actually changed (the flush-ceiling law); zero reads when the table
12632
  has no metric fields or the master is off. Returns the number of rows touched."""
12633
+ owned = tables is not None
12634
+ blob = tables if owned else None
12635
+ t = ((blob or {}).get(str(table_key)) if owned else ut_get(rt, table_key))
12636
  mfields = metric_fields_of_table(t)
12637
  if not mfields:
12638
  return 0
 
12666
  tt.setdefault("rows", {}).setdefault(rid, {}).update(vals)
12667
  return cur
12668
 
12669
+ if owned:
12670
+ _up(blob)
12671
+ if persist:
12672
+ rt.update(UT_STORE_KEY, _up, flush="sync")
12673
+ return len(changes)
12674
  rt.update(UT_STORE_KEY, _up, flush="sync")
12675
  return len(changes)
12676
 
 
13211
  return touched
13212
 
13213
 
13214
+ def refresh_relations(rt, log=print, tables=None, persist=True):
13215
  """The tick half of the relational pass β€” the twin of `refresh_metrics`.
13216
 
13217
  ⚠ Runs for EVERY table, because a link can point anywhere: a rollup on table A goes stale
13218
  when table B gains a row, and A has no way to know that happened. Cheap by construction β€” a
13219
  table declaring neither kind costs one dict scan.
13220
  """
13221
+ if tables is not None:
13222
+ # The scheduler owns this mutable snapshot for one derived pass. Compute locally first
13223
+ # so an unchanged relation does not pay a second full-document read; a changed relation is
13224
+ # still reapplied against current locked state below.
13225
+ local = _refresh_relations_inplace(tables, log=log)
13226
+ if not local or not persist:
13227
+ return local
13228
+ actual = [0]
13229
+
13230
+ def _up(cur):
13231
+ cur = cur if isinstance(cur, dict) else {}
13232
+ actual[0] = _refresh_relations_inplace(cur, log=log)
13233
+ return cur
13234
+
13235
+ rt.update(UT_STORE_KEY, _up, flush="async")
13236
+ return actual[0]
13237
+
13238
  snapshot = {
13239
  str(key): {**(table or {}),
13240
  "rows": {str(rid): dict(row or {})
 
13268
  return actual[0]
13269
 
13270
 
13271
+ def refresh_metrics(rt, today=None, log=print, tables=None, persist=True):
13272
  """The tick half of the C7 amendment: `today` advances at tick cadence, so a date-window
13273
  metric can never go staler than one tick while a scheduler exists. Cheap by construction β€”
13274
  a table without metric fields costs a dict scan and nothing else."""
13275
+ snapshot = tables if tables is not None else ut_all(rt)
13276
  touched = 0
13277
+ for tk, t in snapshot.items():
13278
  if metric_fields_of_table(t):
13279
  try:
13280
+ touched += compute_metric_cells(rt, tk, today=today, tables=snapshot,
13281
+ persist=persist)
13282
  except Exception as e: # noqa: BLE001
13283
  log(f"[aios-auto] metric refresh {tk} failed: {type(e).__name__}: {e}")
13284
  return touched
 
14557
  # C7's amendment: date-window metrics advance with the tick, so `today` is never staler
14558
  # than one tick while a scheduler exists. A tenant without metric fields pays a dict scan.
14559
  try:
14560
+ # One full workspace snapshot is the budget for this whole derived pass. The
14561
+ # individual workers mutate only that local snapshot while they decide whether a
14562
+ # value changed, then use their established guarded RMW only when it did. A read
14563
+ # failure is not converted into an empty snapshot or a success memo.
14564
+ derived_tables = dict(rt.get(UT_STORE_KEY) or {})
14565
  except Exception as e: # noqa: BLE001
14566
  _derived_ok = False
14567
+ derived_tables = None
14568
+ log(f"[aios-auto] derived workspace snapshot {tenant} failed: "
14569
+ f"{type(e).__name__}: {e}")
14570
+ if derived_tables is not None:
14571
+ try:
14572
+ refresh_metrics(rt, log=log, tables=derived_tables)
14573
+ except Exception as e: # noqa: BLE001
14574
+ _derived_ok = False
14575
+ log(f"[aios-auto] metric refresh {tenant} failed: {type(e).__name__}: {e}")
14576
+ # ⭐ 2026-08-07 β€” the relational pass, on the same tick and for the same reason. A rollup
14577
+ # on table A goes stale when table B gains a row, and A cannot know that happened; the
14578
+ # tick is the only place that sees both. Separate failure handling is retained.
14579
+ try:
14580
+ refresh_relations(rt, log=log, tables=derived_tables)
14581
+ except Exception as e: # noqa: BLE001
14582
+ _derived_ok = False
14583
+ log(f"[aios-auto] relation refresh {tenant} failed: {type(e).__name__}: {e}")
14584
+ # Source-backed rollups move with Odoo and with their own date windows, so they share the
14585
+ # same durable-revision/day gate but retain their independent failure posture.
14586
+ try:
14587
+ _refresh_source_rollups(rt, tenant, log=log, tables=derived_tables)
14588
+ except Exception as e: # noqa: BLE001
14589
+ _derived_ok = False
14590
+ log(f"[aios-auto] source rollup {tenant} failed: {type(e).__name__}: {e}")
14591
  if _derived_ok and _derived_marker is not None:
14592
  _DERIVED_TICK_STATE[str(tenant)] = _derived_marker
14593
  if started:
 
14641
  return memo.get(str(tenant)) != marker, marker
14642
 
14643
 
14644
+ def _refresh_source_rollups(rt, tenant, log=print, tables=None):
14645
  """Refresh every source-backed rollup; callers provide the no-op policy.
14646
 
14647
  The existing per-pass error boundary lives in :func:`tick`. Keeping this helper focused on
 
14649
  schedule, vendor call, or a hidden duplicate of the loop.
14650
  """
14651
  import rollup_sql
14652
+ snapshot = tables if tables is not None else (rt.get(UT_STORE_KEY) or {})
14653
+ for _tk, _table in list(snapshot.items()):
14654
+ if rollup_sql.source_fields(_table or {}):
14655
  # `today` is DEFAULTED INSIDE `compute`, matching `refresh_metrics` above.
14656
+ n = rollup_sql.compute(rt, _tk, tables=snapshot, persist=True)
14657
  if n:
14658
  log(f"[aios-auto] source rollups {tenant}/{_tk}: {n}")
14659
 
api/rollup_sql.py CHANGED
@@ -125,7 +125,7 @@ def _today():
125
  return _dt.date.today().strftime("%Y-%m-%d")
126
 
127
 
128
- def compute(rt, table_key, today=None, tables=None):
129
  """Write changed source-backed rollup cells on `table_key`.
130
 
131
  Returns ``{field_key: cells_changed}``. A store-backed call first applies the already-resolved
@@ -137,7 +137,10 @@ def compute(rt, table_key, today=None, tables=None):
137
 
138
  `tables` (a live `user_tables` dict) is the gate's injection point β€” the same shape
139
  `automation_engine.compute_relation_cells` takes, so this can be proven against a fixture
140
- without a store.
 
 
 
141
  """
142
  ut = _ut()
143
  owned = tables is not None
@@ -183,6 +186,14 @@ def compute(rt, table_key, today=None, tables=None):
183
 
184
  if owned:
185
  _apply(blob)
 
 
 
 
 
 
 
 
186
  return {k: n for k, n in written.items() if n}
187
 
188
  # A snapshot preflight costs one full read only on a due source-rollup pass. It avoids the far
 
125
  return _dt.date.today().strftime("%Y-%m-%d")
126
 
127
 
128
+ def compute(rt, table_key, today=None, tables=None, persist=False):
129
  """Write changed source-backed rollup cells on `table_key`.
130
 
131
  Returns ``{field_key: cells_changed}``. A store-backed call first applies the already-resolved
 
137
 
138
  `tables` (a live `user_tables` dict) is the gate's injection point β€” the same shape
139
  `automation_engine.compute_relation_cells` takes, so this can be proven against a fixture
140
+ without a store. A scheduler that already paid for one full snapshot passes ``persist=True``:
141
+ the preflight then reuses that snapshot, while a changed result still takes the ordinary locked
142
+ update against fresh store state. That saves the otherwise N+1 document reads without ever
143
+ replacing a concurrent human edit with the stale snapshot.
144
  """
145
  ut = _ut()
146
  owned = tables is not None
 
186
 
187
  if owned:
188
  _apply(blob)
189
+ changed = {k: n for k, n in written.items() if n}
190
+ if not changed or not persist:
191
+ return changed
192
+ # The caller's snapshot remains useful to its sibling derived passes, but it is never
193
+ # uploaded wholesale. Reapply the already-resolved plan inside the normal FOR UPDATE
194
+ # transaction, exactly as the ordinary path below does.
195
+ written.clear()
196
+ rt.update(ut.STORE_KEY, _apply, flush="async")
197
  return {k: n for k, n in written.items() if n}
198
 
199
  # A snapshot preflight costs one full read only on a due source-rollup pass. It avoids the far