Skip to content

Capture Put/Delete/Increment into StorageOpCollector#254

Merged
em3s merged 1 commit intomainfrom
feature/expose-storage-ops-253
Apr 23, 2026
Merged

Capture Put/Delete/Increment into StorageOpCollector#254
em3s merged 1 commit intomainfrom
feature/expose-storage-ops-253

Conversation

@em3s
Copy link
Copy Markdown
Contributor

@em3s em3s commented Apr 21, 2026

Summary

Closes #253. StorageOpCollector.collect(...) now decodes HBase Put / Delete / Increment into base64-encoded StorageOp. No call-site changes.

Set the AB-Include-Mutation-Context: true header on a mutation request to receive the captured ops under context["storageOps"] in the response. When the captured set exceeds the DEFAULT_MAX_OPS cap, context["storageOpsTruncated"] = true is added so clients can tell a truncated snapshot from a complete one. Without the header the wire format is unchanged.

Changes

  • StorageOp — adds Put / Delete / Increment / Unknown data classes; Cell carries type (Put, DeleteColumn, DeleteFamily, DeleteFamilyVersion, Delete) so delete subtypes are observable
  • StorageOpCollector.collectwhen on Put / Delete / Increment; base64-encode row/family/qualifier/value via ByteBuffer.wrap to avoid an extra cell clone; unknown request types become StorageOp.Unknown with a one-shot per-type warn log
  • StorageOpCollector.toContextMap() — single source of the storageOps / storageOpsTruncated shape, consumed by V3 MutationService and V2 Graph
  • CdcContext.storageOpsTruncated — carries the flag across the V2 pipeline; @JsonIgnore keeps it out of CDC

How to Test

./gradlew build

  • StorageOpCollectorTest — round-trip per op type, multi-CF Put, multi-byte value, truncation, toContextMap flag, AppendUnknown, non-Mutation fallthrough
  • CdcContextSerializationTeststorageOps and storageOpsTruncated excluded from CDC
  • V2EdgeStorageOpsSpec — V2 /graph/v2/edge with header
  • EdgeMutationStorageOpsSpec — V3 edge + multi-edge, context.storageOps[0] assertions

@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 2 times, most recently from 0218576 to ea7fea9 Compare April 22, 2026 09:35
@em3s em3s changed the base branch from main to refactor/mutation-response-context April 22, 2026 09:35
@em3s em3s changed the title Expose StorageBackend operation details in mutation response (#253) Expose StorageBackend operation details in mutation response Apr 22, 2026
em3s added a commit that referenced this pull request Apr 22, 2026
Flip the newly-added `context` field on mutation responses from
non-null `emptyMap()` to nullable + `@JsonInclude(NON_NULL)` so the
wire format is byte-identical for existing callers. The key only
appears once a caller actually populates the map, see #254.

- `core.MutationResult.context: Map<String, Any?>? = null`
- V3 `EdgeMutationResponse.Item` / `MultiEdgeMutationResponse.Item`:
  nullable + `@field:JsonInclude(Include.NON_NULL)`
- V2 `engine.edge.MutationResultItem`: same
- Revert the `"context":{}` additions to `MutationServiceAsyncSpec`,
  `MutationServiceSystemAsyncSpec`, `MultiEdgeSpec` — existing JSON
  expectations no longer need to change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 22, 2026
Add opt-in control over the `context` slot via an `AB-Include-Mutation-Context: true` request header. When set, the server attaches an empty `context` map to each mutation response item; without the header nothing changes on the wire. #254 can later populate the map with storage-op details.

- `ServerWebExchangeContextFilter` writes the current `ServerWebExchange` into the Reactor Context (Spring Boot 3.5 no longer does this automatically) so downstream operators can read request headers without threading `exchange` through controller signatures
- `mapToResponseEntity()` reads the header via `Mono.deferContextual`; when the flag is set it enriches V2 `MutationResult`, V3 `EdgeMutationResponse`, and V3 `MultiEdgeMutationResponse` items with `context = emptyMap()`
- V3 `EdgeMutationController` / `MultiEdgeMutationController` routed through `mapToResponseEntity()` for consistency with V2 (previously they wrapped `ResponseEntity.ok(...)` directly and bypassed the helper)
- New tests:
  - `MutationContextOptInE2ETest` — V3 edge + multi-edge sync endpoints, header on/off via `WebTestClient`, case-insensitive header values
  - `ResponseEntityExtensionsTest` — unit coverage for V2 and V3 response bodies, pass-through for non-mutation bodies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s force-pushed the refactor/mutation-response-context branch 2 times, most recently from e2a8caf to 95ccab3 Compare April 23, 2026 01:15
em3s added a commit that referenced this pull request Apr 23, 2026
Query payloads (`EdgePayload`, `EdgeCountPayload`, `EdgeAggPayload`)
already carry a `context: Map<String, Any?>` slot for per-item
metadata; add the same slot to mutation payloads. Nullable with
`@JsonInclude(NON_NULL)` — existing callers see byte-identical
responses. The key is only emitted once a caller populates it
(see #259, #254).

- `core.MutationResult.context: Map<String, Any?>? = null`
- V3 `EdgeMutationResponse.Item` and `MultiEdgeMutationResponse.Item`:
  nullable + `@field:JsonInclude(Include.NON_NULL)`
- V2 `engine.edge.MutationResultItem`: same
- `from(...)` factories thread `MutationResult.context` through

Tests (ObjectSource-driven, one class per Item type):
- `EdgeMutationResponseItemContextTest` (core) — V3 edge item
- `MultiEdgeMutationResponseItemContextTest` (core) — V3 multi-edge item
- `MutationResultItemContextTest` (engine) — V2 mutation result item

Each class has two parameterized methods (`Item serializes`,
`Item deserializes`) whose YAML cases list input and expected
JSON/context — makes intent readable from the test data alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s force-pushed the refactor/mutation-response-context branch from 95ccab3 to f3389bc Compare April 23, 2026 01:43
em3s added a commit that referenced this pull request Apr 23, 2026
Add opt-in control over the `context` slot via an `AB-Include-Mutation-Context: true` request header. When set, the server attaches an empty `context` map to each mutation response item; without the header nothing changes on the wire. #254 can later populate the map with storage-op details.

- `ServerWebExchangeContextFilter` writes the current `ServerWebExchange` into the Reactor Context (Spring Boot 3.5 no longer does this automatically) so downstream operators can read request headers without threading `exchange` through controller signatures
- `mapToResponseEntity()` reads the header via `Mono.deferContextual`; when the flag is set it enriches V2 `MutationResult`, V3 `EdgeMutationResponse`, and V3 `MultiEdgeMutationResponse` items with `context = emptyMap()`
- V3 `EdgeMutationController` / `MultiEdgeMutationController` routed through `mapToResponseEntity()` for consistency with V2 (previously they wrapped `ResponseEntity.ok(...)` directly and bypassed the helper)
- New tests:
  - `MutationContextOptInE2ETest` — V3 edge + multi-edge sync endpoints, header on/off via `WebTestClient`, case-insensitive header values
  - `ResponseEntityExtensionsTest` — unit coverage for V2 and V3 response bodies, pass-through for non-mutation bodies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s changed the base branch from refactor/mutation-response-context to main April 23, 2026 04:13
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 2 times, most recently from 5a50dd7 to e0bcd22 Compare April 23, 2026 04:46
@em3s em3s changed the base branch from main to feature/storage-op-collector-scaffold April 23, 2026 06:18
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch from 914f2b8 to 00e89fa Compare April 23, 2026 06:18
@em3s em3s changed the title Expose StorageBackend operation details in mutation response Capture Put/Delete/Increment into StorageOpCollector Apr 23, 2026
@em3s em3s force-pushed the feature/storage-op-collector-scaffold branch from c0a533c to b69f3e5 Compare April 23, 2026 06:57
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 5 times, most recently from c511f2e to fbbb51f Compare April 23, 2026 09:10
@em3s em3s changed the base branch from feature/storage-op-collector-scaffold to main April 23, 2026 09:19
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 2 times, most recently from 73d76ca to fa01dec Compare April 23, 2026 09:24
@em3s em3s added this to the v0.2.1 milestone Apr 23, 2026
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 6 times, most recently from bac87c2 to 00c5a22 Compare April 23, 2026 13:29
@em3s em3s marked this pull request as ready for review April 23, 2026 13:42
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Apr 23, 2026
@em3s
Copy link
Copy Markdown
Contributor Author

em3s commented Apr 23, 2026

@em3s em3s force-pushed the feature/expose-storage-ops-253 branch 2 times, most recently from 2c2994a to 628e549 Compare April 23, 2026 13:59
Closes #253. Follows #258, #259, #261. Implements
`StorageOpCollector.collect` so it decodes raw HBase requests into the
`StorageOp` sealed type. The scaffolded marker is extended with the
backend-facing shape: `Put` / `Delete` / `Increment` data classes with
`Cell` / `Delta` nested types, base64-encoded
row / family / qualifier / value. No call-site changes — the plumbing
was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation responses
include the captured ops under `context["storageOps"]`. When more than
`DEFAULT_MAX_OPS` (1024) ops would be collected, the collector sets
`context["storageOpsTruncated"] = true` so clients can tell a complete
snapshot from a silently truncated one.

`StorageOp.Cell.type` captures the HBase `Cell.Type` name (`Put`,
`DeleteColumn`, `DeleteFamily`, `DeleteFamilyVersion`, `Delete`) so
shadow tests can distinguish delete subtypes.

Unknown request types become `StorageOp.Unknown(type = qualified class
name)` with a one-shot `warn`-level log per type, so new backends can
join without silent drops.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put / Delete-full-row / Delete-columns / Delete-family / Increment /
  multi-byte) + `@Test` for multi-CF Put, non-Mutation fallthrough,
  `Append` → `Unknown`, truncation cap, `toContextMap` flag
- `CdcContextSerializationTest` — `storageOps` and
  `storageOpsTruncated` excluded from CDC
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with header
  on, asserts `context.storageOps[0].row` / cells shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s force-pushed the feature/expose-storage-ops-253 branch from 628e549 to 2031cd6 Compare April 23, 2026 14:01
@em3s
Copy link
Copy Markdown
Contributor Author

em3s commented Apr 23, 2026

Optimistic Merge.

@em3s em3s merged commit 9bfa878 into main Apr 23, 2026
7 checks passed
em3s added a commit that referenced this pull request Apr 23, 2026
@em3s em3s assigned em3s and unassigned em3s Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose StorageBackend operation details in mutation response

1 participant