Skip to content

Commit 5bc39b0

Browse files
authored
Feature/optimized patching (#15)
* feat: add path-preserving resolution and enhanced node path capabilities Implemented path-preserving resolution methods in `Blue` to maintain specified or matching node paths during resolution, including `resolvePreservingPaths` and `resolvePreservingMatchingPaths`. Added utilities like `NodePathSelector` and `NodePathEditor` for advanced path manipulation, and introduced `ExcludedPathLimits` to restrict merge operations at specific paths. Updated `ProcessorExecutionContext` and `Node` to support new node path-related methods. Added comprehensive test coverage with `MaskedResolutionTest`. * chore: bump version to 2.0.1 * feat: implement batch patching transaction handling Added `BatchPatchTransaction`, `BatchPatchRecord`, and `BatchPatchResult` for processing transactional batch patches. Introduced `ProcessingMetricsSink` for optional metrics instrumentation. Added comprehensive test coverage, including batch patch commit/rollback scenarios and performance metrics validation.
1 parent 7a5193b commit 5bc39b0

27 files changed

Lines changed: 2538 additions & 180 deletions

docs/snapshots-patching-and-generalization.md

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -348,25 +348,61 @@ Changed paths include:
348348

349349
## Processor Transaction Flow
350350

351-
`DocumentProcessingRuntime.applyPatch(...)` now works roughly like this:
351+
`DocumentProcessingRuntime.applyPatches(...)` now works roughly like this:
352352

353353
```text
354-
rollback = current mutable materialized view
355354
baseSnapshot = current snapshot or snapshotManager.fromDocument(...)
356-
canonicalPlan = ImmutablePatchPlanner(baseSnapshot.canonical).plan(...)
357-
resolvedPlan = ImmutablePatchPlanner(baseSnapshot.resolved).plan(...)
358-
conformancePlan = ConformanceEngine.planGeneralization(...)
359-
if generalized:
360-
commit generalized snapshot
361-
else:
362-
commit normal canonical patch snapshot
355+
for each patch:
356+
canonicalPlan = ImmutablePatchPlanner(working canonical root).plan(...)
357+
resolvedPlan = ImmutablePatchPlanner(working resolved root).plan(...)
358+
remember frozen before/after update metadata
359+
conformancePlan = ConformanceEngine.planGeneralization(..., changedPaths)
360+
commit final canonical/resolved roots once
363361
on failure:
364-
restore rollback and previous snapshot
362+
restore previous snapshot if one was active
365363
```
366364

365+
Batch conformance selects changed paths whose final resolved path has typed
366+
metadata at the changed node or one of its ancestors up to the origin scope.
367+
This catches typed descendants below an otherwise untyped root, including list
368+
`itemType` and dictionary `valueType` paths, while leaving unrelated untyped
369+
processor-managed writes out of the conformance planner.
370+
367371
The mutable `Node` view is now a compatibility adapter generated from the
368372
canonical snapshot. Snapshot state is authoritative.
369373

374+
## Batch Patch Application
375+
376+
`ProcessorExecutionContext.applyPatches(List<JsonPatch>)` applies a changeset
377+
atomically.
378+
379+
Semantics:
380+
381+
- patches are applied in order
382+
- duplicate paths are preserved
383+
- if any patch fails, the full batch rolls back
384+
- the mutable materialized root is not deep-copied before a batch; planning runs
385+
on frozen roots and the materialized view changes only at commit
386+
- conformance/generalization is planned over the final working roots
387+
- the runtime commits once
388+
- document update events are returned and routed in patch order after the batch
389+
commit
390+
- update `before` values describe the value at the patch path immediately before
391+
that patch entry was applied
392+
- update `after` values normally describe the committed post-conformance value
393+
at the patch path; if a later patch in the same batch overlaps that path, the
394+
earlier update keeps its patch-time intermediate `after` value so duplicate
395+
and add/remove patch-entry order remains observable
396+
- update before/after values stay frozen-backed and materialize to `Node` only
397+
when a matching `DocumentUpdateChannel` needs an event or a caller explicitly
398+
reads `before()` / `after()`
399+
- batch timing and update materialization counters are exposed package-privately
400+
for tests and performance investigation
401+
- `applyPatch` delegates to `applyPatches(singletonList(...))`
402+
403+
This is the preferred path for workflow steps such as `Conversation/Update
404+
Document` that apply a computed changeset.
405+
370406
## Gas And Caching
371407

372408
Resolved snapshot/type caches affect CPU and provider fetches, not gas.
@@ -405,4 +441,6 @@ Still missing:
405441
- `ConformanceEngineTest`
406442
- `DocumentProcessorSnapshotTransactionTest`
407443
- `DocumentProcessorGeneralizationTest`
444+
- `DocumentProcessingRuntimeBatchPatchTest`
445+
- `DocumentProcessorBatchPatchTest`
408446
- `DocumentProcessorGasTest`

src/main/java/blue/language/Blue.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,23 @@ public Blue registerContractProcessor(String blueId, ContractProcessor<? extends
431431
}
432432

433433
public DocumentProcessingResult processDocument(Node document, Node event) {
434-
return attachProcessingSnapshot(ensureDocumentProcessor().processDocument(document, event));
434+
DocumentProcessor processor = ensureDocumentProcessor();
435+
long start = System.nanoTime();
436+
try {
437+
return attachProcessingSnapshot(processor, processor.processDocument(document, event));
438+
} finally {
439+
processor.processingMetricsSink().addBlueProcessDocumentNanos(System.nanoTime() - start);
440+
}
435441
}
436442

437443
public DocumentProcessingResult processDocument(ResolvedSnapshot snapshot, Node event) {
438-
return ensureDocumentProcessor().processDocument(snapshot, event);
444+
DocumentProcessor processor = ensureDocumentProcessor();
445+
long start = System.nanoTime();
446+
try {
447+
return processor.processDocument(snapshot, event);
448+
} finally {
449+
processor.processingMetricsSink().addBlueProcessDocumentNanos(System.nanoTime() - start);
450+
}
439451
}
440452

441453
public DocumentProcessor getDocumentProcessor() {
@@ -451,7 +463,8 @@ public Blue documentProcessor(DocumentProcessor documentProcessor) {
451463
}
452464

453465
public DocumentProcessingResult initializeDocument(Node document) {
454-
return attachProcessingSnapshot(ensureDocumentProcessor().initializeDocument(document));
466+
DocumentProcessor processor = ensureDocumentProcessor();
467+
return attachProcessingSnapshot(processor, processor.initializeDocument(document));
455468
}
456469

457470
public DocumentProcessingResult initializeDocument(ResolvedSnapshot snapshot) {
@@ -558,11 +571,18 @@ private DocumentProcessor createDefaultDocumentProcessor() {
558571
.build();
559572
}
560573

561-
private DocumentProcessingResult attachProcessingSnapshot(DocumentProcessingResult result) {
574+
private DocumentProcessingResult attachProcessingSnapshot(DocumentProcessor processor, DocumentProcessingResult result) {
562575
if (result == null || result.capabilityFailure() || result.snapshot() != null) {
563576
return result;
564577
}
565-
return result.withSnapshot(resolveProcessingSnapshot(result.document()));
578+
long start = System.nanoTime();
579+
try {
580+
return result.withSnapshot(resolveProcessingSnapshot(result.document()));
581+
} finally {
582+
long nanos = System.nanoTime() - start;
583+
processor.processingMetricsSink().addResultSnapshotAttachNanos(nanos);
584+
processor.processingMetricsSink().addBlueIdCalculationNanos(nanos);
585+
}
566586
}
567587

568588
private void refreshDocumentProcessorConformanceEngine() {
@@ -571,7 +591,8 @@ private void refreshDocumentProcessorConformanceEngine() {
571591
documentProcessor.getContractTypeResolver(),
572592
conformanceEngine(),
573593
processingSnapshotManager(),
574-
new ContractMatchingService(this));
594+
new ContractMatchingService(this),
595+
documentProcessor.processingMetricsSink());
575596
}
576597
}
577598

src/main/java/blue/language/conformance/ConformanceEngine.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import blue.language.utils.NodeProviderWrapper;
1010
import blue.language.utils.limits.Limits;
1111

12+
import java.util.ArrayList;
13+
import java.util.List;
1214
import java.util.Objects;
1315

1416
public final class ConformanceEngine {
@@ -60,4 +62,38 @@ public ConformancePlan planGeneralization(FrozenNode canonicalRoot, FrozenNode r
6062
return new FrozenConformancePlanner(nodeProvider, mergingProcessor, resolvedReferenceCache)
6163
.plan(canonicalRoot, resolvedRoot, changedPath);
6264
}
65+
66+
public ConformancePlan planGeneralization(FrozenNode canonicalRoot,
67+
FrozenNode resolvedRoot,
68+
List<String> changedPaths) {
69+
if (changedPaths == null || changedPaths.isEmpty()) {
70+
return ConformancePlan.unchanged(canonicalRoot, resolvedRoot);
71+
}
72+
FrozenNode nextCanonical = canonicalRoot;
73+
FrozenNode nextResolved = resolvedRoot;
74+
boolean generalized = false;
75+
List<CanonicalGeneralizationPatch> canonicalPatches = new ArrayList<>();
76+
List<String> allChangedPaths = new ArrayList<>();
77+
FrozenConformancePlanner planner = new FrozenConformancePlanner(nodeProvider,
78+
mergingProcessor,
79+
resolvedReferenceCache);
80+
for (String changedPath : changedPaths) {
81+
ConformancePlan plan = planner.plan(nextCanonical, nextResolved, changedPath);
82+
nextCanonical = plan.canonicalRoot() != null ? plan.canonicalRoot() : nextCanonical;
83+
nextResolved = plan.root();
84+
if (plan.generalized()) {
85+
generalized = true;
86+
canonicalPatches.addAll(plan.canonicalPatches());
87+
allChangedPaths.addAll(plan.changedPaths());
88+
}
89+
}
90+
if (!generalized) {
91+
return ConformancePlan.unchanged(nextCanonical, nextResolved);
92+
}
93+
return ConformancePlan.generalized(nextCanonical,
94+
nextResolved,
95+
canonicalPatches,
96+
allChangedPaths,
97+
nextCanonical != null);
98+
}
6399
}

src/main/java/blue/language/conformance/ConformancePlan.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ public static ConformancePlan unchanged(FrozenNode canonicalRoot, FrozenNode roo
5050
canonicalRoot != null);
5151
}
5252

53+
public static ConformancePlan generalized(FrozenNode canonicalRoot,
54+
FrozenNode root,
55+
List<CanonicalGeneralizationPatch> canonicalPatches,
56+
List<String> changedPaths,
57+
boolean fullSnapshotRebuildAvoidable) {
58+
return new ConformancePlan(canonicalRoot,
59+
root,
60+
true,
61+
canonicalPatches,
62+
changedPaths,
63+
fullSnapshotRebuildAvoidable);
64+
}
65+
5366
public FrozenNode canonicalRoot() {
5467
return canonicalRoot;
5568
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package blue.language.processor;
2+
3+
import blue.language.processor.model.JsonPatch;
4+
import blue.language.snapshot.FrozenNode;
5+
6+
import java.util.List;
7+
8+
final class BatchPatchRecord {
9+
10+
private final JsonPatch patch;
11+
private final ImmutablePatchPlanner.PatchPlan canonicalPlan;
12+
private final ImmutablePatchPlanner.PatchPlan resolvedPlan;
13+
private final FrozenNode beforeAtPatchTime;
14+
private final FrozenNode afterAtPatchTime;
15+
private final boolean processorManagedConformanceBypass;
16+
17+
BatchPatchRecord(JsonPatch patch,
18+
ImmutablePatchPlanner.PatchPlan canonicalPlan,
19+
ImmutablePatchPlanner.PatchPlan resolvedPlan,
20+
boolean processorManagedConformanceBypass) {
21+
this.patch = patch;
22+
this.canonicalPlan = canonicalPlan;
23+
this.resolvedPlan = resolvedPlan;
24+
this.beforeAtPatchTime = resolvedPlan.before();
25+
this.afterAtPatchTime = resolvedPlan.after();
26+
this.processorManagedConformanceBypass = processorManagedConformanceBypass;
27+
}
28+
29+
JsonPatch patch() {
30+
return patch;
31+
}
32+
33+
ImmutablePatchPlanner.PatchPlan canonicalPlan() {
34+
return canonicalPlan;
35+
}
36+
37+
ImmutablePatchPlanner.PatchPlan resolvedPlan() {
38+
return resolvedPlan;
39+
}
40+
41+
String path() {
42+
return canonicalPlan.path();
43+
}
44+
45+
JsonPatch.Op op() {
46+
return canonicalPlan.op();
47+
}
48+
49+
String originScope() {
50+
return canonicalPlan.originScope();
51+
}
52+
53+
List<String> cascadeScopes() {
54+
return canonicalPlan.cascadeScopes();
55+
}
56+
57+
FrozenNode beforeAtPatchTime() {
58+
return beforeAtPatchTime;
59+
}
60+
61+
FrozenNode afterAtPatchTime() {
62+
return afterAtPatchTime;
63+
}
64+
65+
boolean processorManagedConformanceBypass() {
66+
return processorManagedConformanceBypass;
67+
}
68+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package blue.language.processor;
2+
3+
import blue.language.snapshot.FrozenNode;
4+
5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.List;
8+
import java.util.Objects;
9+
10+
final class BatchPatchResult {
11+
12+
private final FrozenNode canonicalRoot;
13+
private final FrozenNode resolvedRoot;
14+
private final List<DocumentProcessingRuntime.DocumentUpdateData> updates;
15+
private final long patchPlanningNanos;
16+
private final long conformanceNanos;
17+
private final long buildUpdatesNanos;
18+
19+
BatchPatchResult(FrozenNode canonicalRoot,
20+
FrozenNode resolvedRoot,
21+
List<DocumentProcessingRuntime.DocumentUpdateData> updates) {
22+
this(canonicalRoot, resolvedRoot, updates, 0L, 0L, 0L);
23+
}
24+
25+
BatchPatchResult(FrozenNode canonicalRoot,
26+
FrozenNode resolvedRoot,
27+
List<DocumentProcessingRuntime.DocumentUpdateData> updates,
28+
long patchPlanningNanos,
29+
long conformanceNanos,
30+
long buildUpdatesNanos) {
31+
this.canonicalRoot = Objects.requireNonNull(canonicalRoot, "canonicalRoot");
32+
this.resolvedRoot = Objects.requireNonNull(resolvedRoot, "resolvedRoot");
33+
this.updates = Collections.unmodifiableList(new ArrayList<>(
34+
Objects.requireNonNull(updates, "updates")));
35+
this.patchPlanningNanos = patchPlanningNanos;
36+
this.conformanceNanos = conformanceNanos;
37+
this.buildUpdatesNanos = buildUpdatesNanos;
38+
}
39+
40+
FrozenNode canonicalRoot() {
41+
return canonicalRoot;
42+
}
43+
44+
FrozenNode resolvedRoot() {
45+
return resolvedRoot;
46+
}
47+
48+
List<DocumentProcessingRuntime.DocumentUpdateData> updates() {
49+
return updates;
50+
}
51+
52+
long patchPlanningNanos() {
53+
return patchPlanningNanos;
54+
}
55+
56+
long conformanceNanos() {
57+
return conformanceNanos;
58+
}
59+
60+
long buildUpdatesNanos() {
61+
return buildUpdatesNanos;
62+
}
63+
}

0 commit comments

Comments
 (0)