fix(intent): trigger ProcessId write-back via targeted single-column update — kill the stale-snapshot race#6226
Merged
Merged
Conversation
…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>
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.
The problem — the ProcessId write-back race
For an
onCreate-triggered process the generated flow was:repository.save()inserts the header → the DAO publishes the create event.<Process>Trigger(async) loads a full snapshot of the row (findById) at time T1.Process.start(...)(BPM work — tens to hundreds of ms), setsentity.ProcessIdon the T1 snapshot, and persists viaupdateWithoutEvent(entity)at time T2 — which issession.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:
recalculate()updating the header totals — the trigger's stale merge then reset them (observed live; an IT needed a "settle sleep");init:, never a start step" caveat;The minted
businessKeyStrategy: timestampfield 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 (theCriteriarule), so nothing is injectable; the id property name comes from the existingRegisteredEntity.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 ofrepository.updateProperty(id, "ProcessId", processId), plus a second targeted write for the minted business-key field whenbusinessKeyStrategyminted 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; convertingSetFieldto the same primitive is the natural follow-up.Verified live
On a ledger fixture: create a document header → the trigger fires, the process starts,
ProcessIdis written back, andReason/Status(theinit:DB default) /EntryDateare untouched — the generated trigger contains only the targeted updates, noupdateWithoutEvent.Tests / docs
IntentEngineITtrigger assertions updated (targetedupdatePropertycalls asserted,updateWithoutEventasserted absent).CLAUDE.mdtrigger gotcha + engine-intentCLAUDE.mdinit:bullet updated (the race is fixed;init:stays the right modeling for an initial status).🤖 Generated with Claude Code