Skip to content

fix(intent): trigger ProcessId write-back via targeted single-column update — kill the stale-snapshot race#6226

Merged
delchev merged 2 commits into
masterfrom
fix/processid-targeted-writeback
Jul 10, 2026
Merged

fix(intent): trigger ProcessId write-back via targeted single-column update — kill the stale-snapshot race#6226
delchev merged 2 commits into
masterfrom
fix/processid-targeted-writeback

Conversation

@delchev

@delchev delchev commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The problem — the ProcessId write-back race

For an onCreate-triggered process the generated flow was:

  1. repository.save() inserts the header → the DAO publishes the create event.
  2. The generated <Process>Trigger (async) loads a full snapshot of the row (findById) at time T1.
  3. It calls Process.start(...) (BPM work — tens to hundreds of ms), sets entity.ProcessId on the T1 snapshot, and persists via updateWithoutEvent(entity) at time T2 — which is session.merge(entityName, fullMap): a full-row UPDATE writing every column with its T1 value.

Any write landing on the same row between T1 and T2 was silently reverted:

  • the canonical case: the client creates the header and immediately POSTs line items; each item create runs the parent's recalculate() updating the header totals — the trigger's stale merge then reset them (observed live; an IT needed a "settle sleep");
  • a start-step status write was clobbered back to null — the incident behind the "init:, never a start step" caveat;
  • any fast user edit right after create.

The minted businessKeyStrategy: timestamp field rode the same full-row write.

The fix — persist only the columns the trigger owns

  • JavaEntityStore.updateProperty(Class<T>, Object id, String property, Object value) (data-store-java): an HQL mutation — update <EntityName> set <property> = :value where <idProperty> = :id — in its own transaction. Only the named column is in the statement, so there is nothing to clobber; no audit stamping, no events (a system column write, not a user edit). The property name is validated as a plain identifier (the Criteria rule), so nothing is injectable; the id property name comes from the existing RegisteredEntity.idField().
  • JavaRepository.updateProperty(...): the SDK pass-through, documented as the workflow/system write-back primitive — reserve for system columns; user data keeps going through the generated repository's normal write path (validations, events, i18n).
  • Trigger.java.template: drops the snapshot mutation + updateWithoutEvent(entity) in favour of repository.updateProperty(id, "ProcessId", processId), plus a second targeted write for the minted business-key field when businessKeyStrategy minted one. The trigger now carries no full row at all — consistent with its own id-only clear-D context.

Deliberately not in scope: the SetField/Writer delegates share the load-merge shape but run at task boundaries where the row is workflow-owned; converting SetField to the same primitive is the natural follow-up.

Verified live

On a ledger fixture: create a document header → the trigger fires, the process starts, ProcessId is written back, and Reason / Status (the init: DB default) / EntryDate are untouched — the generated trigger contains only the targeted updates, no updateWithoutEvent.

Tests / docs

  • data-store-java unit tests green (13); IntentEngineIT trigger assertions updated (targeted updateProperty calls asserted, updateWithoutEvent asserted absent).
  • Root CLAUDE.md trigger gotcha + engine-intent CLAUDE.md init: bullet updated (the race is fixed; init: stays the right modeling for an initial status).

🤖 Generated with Claude Code

delchev and others added 2 commits July 10, 2026 10:41
…update - kill the stale-snapshot race

The generated <Process>Trigger loaded a full entity snapshot, called
Process.start (tens to hundreds of ms), then persisted ProcessId via
updateWithoutEvent - a full-row Hibernate merge of the stale snapshot
that silently reverted any concurrent write on the same row: a
document's recalculated totals when line items are added right after
create (observed live; an IT needed a settle sleep), or a start-step
status set (the incident behind the 'use init:, not a start step'
caveat).

New primitive: JavaEntityStore.updateProperty(type, id, property,
value) - an HQL mutation updating ONLY the named column in its own
transaction (property validated as a plain identifier; no audit
stamping, no events), surfaced as JavaRepository.updateProperty - the
sanctioned workflow/system write-back. The trigger now persists
ProcessId (and a minted businessKeyStrategy field) through it and
carries no full row at all, consistent with its id-only clear-D
context.

Verified live: create -> process starts, ProcessId written back,
Reason/Status(init default)/EntryDate untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev delchev merged commit 16fa6e8 into master Jul 10, 2026
9 checks passed
@delchev delchev deleted the fix/processid-targeted-writeback branch July 10, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant