fix(#5036): report partial-commit counts and partial-write drops on batch/time-series write endpoints#5167
Conversation
…atch/time-series write endpoints
POST /api/v1/batch now returns verticesCreated/edgesCreated and a partialCommit
flag on mid-stream failure so clients can reconcile instead of duplicating data on
retry; the non-atomic contract is documented in the handler Javadoc.
POST /api/v1/ts/{db}/write now returns a 400 partial-write payload naming the
dropped measurements (with written/dropped counts) whenever any sample is
discarded, instead of silently returning 204 when at least one sample inserted.
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 6 |
🟢 Coverage 95.00% diff coverage
Metric Results Coverage variation Report missing for ab707c81 Diff coverage ✅ 95.00% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (ab707c8) Report Missing Report Missing Report Missing Head commit (e0f6828) 168675 114304 67.77% Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5167) 40 38 95.00% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request addresses issue #5036 by improving error reporting for partial commits in batch and time-series write endpoints. In PostBatchHandler, streaming ingest failures are caught to return a structured JSON response containing verticesCreated, edgesCreated, and a partialCommit flag. In PostTimeSeriesWriteHandler, any dropped samples now trigger a 400 partial-write response with details of the dropped types and counts instead of silently returning a 204 success. The feedback suggests two improvements: first, in PostBatchHandler, we should unwrap wrapped exceptions and map specific exception types to their appropriate HTTP status codes (e.g., 409, 503) instead of defaulting all non-IllegalArgumentException errors to 500; second, in PostTimeSeriesWriteHandler, the dropped count should represent the number of dropped samples (samples.size() - inserted) rather than the count of unique dropped types.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| final int status = e instanceof IllegalArgumentException ? 400 : 500; | ||
| final String message = e.getMessage() != null ? e.getMessage() : e.toString(); |
There was a problem hiding this comment.
The current exception handling maps all non-IllegalArgumentException errors to HTTP status 500. However, this intercepts and masks other specific exceptions that should return different status codes (e.g., DuplicatedKeyException should return 409 Conflict, NeedRetryException should return 503 Service Unavailable, etc.). Masking these as 500 can mislead clients and disrupt retry or failover logic.
Additionally, exceptions might be wrapped in TransactionException or CommandExecutionException, so we should unwrap them to find the root cause before mapping the status code and extracting the error message.
Throwable cause = e;
while (cause.getCause() != null && (cause instanceof com.arcadedb.exception.TransactionException || cause instanceof com.arcadedb.exception.CommandExecutionException)) {
cause = cause.getCause();
}
int status = 500;
if (cause instanceof IllegalArgumentException || cause instanceof com.arcadedb.exception.QueryNotIdempotentException || cause instanceof com.arcadedb.network.binary.ServerIsNotTheLeaderException) {
status = 400;
} else if (cause instanceof SecurityException || cause instanceof com.arcadedb.server.security.ServerSecurityException) {
status = 403;
} else if (cause instanceof com.arcadedb.exception.RecordNotFoundException) {
status = 404;
} else if (cause instanceof com.arcadedb.exception.DuplicatedKeyException || cause instanceof com.arcadedb.exception.TransactionCommittedRemotelyException) {
status = 409;
} else if (cause instanceof com.arcadedb.exception.NeedRetryException) {
status = 503;
}
final String message = cause.getMessage() != null ? cause.getMessage() : cause.toString();| final int dropped = unknownTypes.size() + nonTimeSeriesTypes.size(); | ||
| if (dropped > 0) { |
There was a problem hiding this comment.
The dropped count is currently calculated as unknownTypes.size() + nonTimeSeriesTypes.size(). Since unknownTypes and nonTimeSeriesTypes are Set<String> containing unique measurement type names, this represents the number of unique types dropped, not the number of samples dropped.
Since written (i.e., inserted) counts individual samples, dropped should also count individual samples to be consistent and accurate. We can calculate the exact number of dropped samples as samples.size() - inserted.
| final int dropped = unknownTypes.size() + nonTimeSeriesTypes.size(); | |
| if (dropped > 0) { | |
| final int dropped = samples.size() - inserted; | |
| if (dropped > 0) { |
|
Review: partial-commit / partial-write reporting on batch & time-series endpoints Thanks for tackling this - the silent-data-loss problem is real and the direction (surface counts plus a partialCommit / partial-write signal so clients can reconcile rather than blindly retry) is the right one. Documenting the non-atomic contract in the Javadoc and matching InfluxDB partial-write semantics are both nice touches, and both new paths have regression tests. A few concerns, roughly by severity. 1. PostBatchHandler broad catch(Exception) collapses the base handler status mapping (high) The new try/catch in execute() maps everything to IllegalArgumentException -> 400, else 500. Previously the exception propagated to AbstractServerHttpHandler, which has a carefully-built mapping this now bypasses:
For the batch endpoint these are not hypothetical: GraphBatch.createVertices commits internally and can throw DuplicatedKeyException (a batch vertex hitting a unique index) or NeedRetryException (Raft hiccup). Both now return 500 instead of 409/503. The 409 arms exist specifically because a 5xx invites HTTP clients and load balancers to retry, and a retry of an already-committed chunk duplicates data - the exact hazard this PR is trying to eliminate. So collapsing to 500 here works against the PR own goal. Consider re-throwing the exceptions the base handler already maps (or letting them propagate) and only wrapping the cases where you want to attach the partial-commit counts. At minimum, the "do not retry" cases should not degrade to a retry-inviting 500. 2. Time-series dropped count mixes units with written (medium) dropped = unknownTypes.size() + nonTimeSeriesTypes.size() counts distinct dropped measurement NAMES (both are Set), while written = inserted counts SAMPLES. If 100 samples for one unknown measurement arrive, the client sees dropped:1 next to written:N and will reasonably read it as "1 sample dropped". The new test only sends one sample per measurement, so the mismatch is invisible. Either count actual dropped samples (increment a counter in the continue branches) or rename the field to droppedMeasurements so the unit is unambiguous. 3. Reported batch counts are optimistic vs what is durable (low) verticesCreated / edgesCreated track records submitted to the batch, but GraphBatch only commits every commitEvery (default 50000). On mid-stream failure the records since the last commit boundary are rolled back, so the reported count can overstate what actually survived on disk. The test uses commitEvery=1 so it lines up exactly, but with defaults a client reconciling against these numbers could over- or under-delete. Worth a Javadoc note that the counts are "attempted" (upper bound). 4. 500 path drops the stack trace (low) The catch logs only Level.WARNING with e.getMessage(). For the genuine-server-error (500) case the previous path logged via the base handler with the full throwable. Losing the stack trace makes diagnosing real ingest failures harder - consider logging the throwable (with stack) when the status is 500. Minor / nits
Overall a solid, well-tested fix. The main items I would want addressed before merge are point 1 (do not let 409/503 outcomes degrade to a retry-inviting 500 on the batch endpoint) and point 2 (the dropped unit mismatch). |
…d samples - PostBatchHandler: only enrich IllegalArgumentException (400) with partial-commit counts; let engine/cluster exceptions (409/503/403/404) propagate to AbstractServerHttpHandler so 'do not retry' outcomes are not downgraded to a retry-inviting 500 and the full stack trace is still logged. Javadoc notes the counts are attempted (an upper bound on what is durable). - PostTimeSeriesWriteHandler: report dropped as samples.size() - inserted so it is in the same unit (samples) as written, not a count of distinct measurement names. - Test: drop the unused contentEncoding helper parameter.
|
Code Review - PR #5167 Thanks for this fix - it closes a genuine data-loss-visibility gap, and the change is careful and well-reasoned. The inline documentation explaining why only Should fix 1. Stray Worth considering 2. Partial-commit error body diverges from the standard error envelope. Elsewhere the server emits errors via 3. Non- Nits 4. CSV path isn't exercised for partial commit. Things I checked and liked
Overall: solid, well-tested change. Just the stray |
… CSV partial-commit test - Remove stray </content> line from the tracking doc (claude must-fix). - Add exception class name to the batch 400 partial-commit body so it matches the standard error envelope; Javadoc notes non-400 paths are best-effort for reporting. - Add partialCommitErrorReportsPersistedCountsCsv to lock the partial-commit contract for the CSV stream path as well as JSONL.
Code Review - PR #5167Solid, well-scoped fix. The core behavior change is correct: both endpoints previously reported success (or a bare error) while data was partially written, and this makes the partial state explicit and reconcilable. The Javadoc/inline comments are unusually thorough and the reasoning for only enriching the I verified the logic against the surrounding code. A few points, none blocking: 1. Error-envelope inconsistency with
|
…acking doc - resolveRef now rethrows Integer/Long parse failures as a clear 'Malformed RID ...' IllegalArgumentException instead of leaking the raw JDK NumberFormatException text. - Add partialCommitFalseWhenFirstRecordFails to lock the zero-committed boundary (partialCommit=false, zero counts) for the batch 400 body. - Trim the tracking doc's review-process log to keep it about behavior; add an operator note about the time-series 204->400 change.
Code Review - PR #5167Thanks for tackling this - the silent partial-write data loss is a real correctness/observability gap, and the fix is well-scoped. The Javadoc and inline comments are genuinely excellent: they document the non-atomic contract, the rationale for leaving non-retryable engine/cluster exceptions to propagate, and the "upper bound" caveat on the counts. Tests cover the important cases (JSONL, CSV, first-record-fails, mixed TS ingest). A few observations below, mostly around consistency and one behavioral note. Design is sound (verified)I traced 1. Error body diverges from the standard error envelope (medium)
2. InfluxDB 204 -> 400 is a real behavioral change for Telegraf-style clients (informational)You call this out in the PR body and the doc file, which is the right thing to do. Just amplifying: any deployment that continuously ships a mix where one measurement type simply hasn't been created yet will now get a 400 on every write batch instead of 204, and Telegraf treats 400 as non-retryable (drops the buffered batch). That matches InfluxDB's partial-write semantics and is defensible, but it deserves a prominent CHANGELOG/release-note entry (the doc mentions this - please make sure it lands in the actual release notes, not just 3. Broad
|
… bodies - Add reusable AbstractServerHttpHandler.getCorrelationId(exchange) and use it in the batch and time-series handlers so their bespoke 400 bodies include the same requestId the standard error envelope emits, keeping client-facing errors cross-referenceable with the server log. - Document why the batch body stays bespoke and exposes the message in production (the message echoes client input; nothing engine-internal to conceal; the client needs the offending location to reconcile). - Add allDroppedReportsPartialWritePayloadShape to pin the written/dropped/unknownTypes payload on the all-dropped time-series path.
Review: PR #5167 - partial-commit / partial-write reportingThanks for tackling #5036. This is a well-scoped, well-documented change that closes a real data-loss-visibility gap on two bulk-write endpoints. The Javadoc and inline comments are unusually thorough, the "attempted vs. durable" semantics are stated honestly rather than oversold, and the new tests cover the important axes (mixed drop, all-dropped, first-record-fails, JSONL and CSV paths, plus a persistence assertion on the time-series side). Nice work. A few observations, mostly minor. Correctness
Scope limitation worth surfacingThe core goal - "let the client detect a partial write instead of blindly retrying" - is only met for the Security / consistency nuanceThe standard Minor
Overall: a solid, defensible change with honest documentation. The main thing I would push on is whether the non-IAE failure paths (503/409/500) - the ones most likely to actually strand committed data - deserve the same treatment in a follow-up. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5167 +/- ##
============================================
+ Coverage 65.97% 65.99% +0.02%
+ Complexity 916 915 -1
============================================
Files 1693 1693
Lines 136805 136837 +32
Branches 29253 29259 +6
============================================
+ Hits 90251 90308 +57
+ Misses 34308 34284 -24
+ Partials 12246 12245 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Closes #5036
Summary
Both bulk write endpoints could report success (or a bare error) while data was partially written, hiding data loss from the client.
POST /api/v1/batch- a batch is not atomic:GraphBatchcommits everycommitEveryrecords, so a mid-stream failure (malformed line, unknown temp id, dropped connection) left earlier chunks durably committed while the error body reported nothing about them. The handler now catches the failure and returns an explicit response (400for client-input errors,500otherwise) whose JSON body carriesverticesCreated,edgesCreated, and apartialCommitflag alongside the originalerrormessage. The non-atomic contract is documented in the class Javadoc so clients know a blind whole-file retry duplicates the already-committed vertices (temporary@ids are not keys).POST /api/v1/ts/{db}/write(InfluxDB line protocol) - samples for unknown / non-timeseries measurements were dropped with only a server-side WARNING, and the response was204as long as at least one sample inserted. Matching InfluxDB semantics, the handler now returns a400partial-write payload naming the dropped measurements (withwritten/droppedcounts) whenever any sample is discarded. A clean ingest still returns204, and the all-dropped case keeps its existing400.Test plan
mvn -pl server test -Dtest=PostBatchHandlerIT -DskipITs=false- 16/16 pass, including newpartialCommitErrorReportsPersistedCounts(mid-stream edge failure returns 400 withverticesCreated=2,partialCommit=true, offending ref in message).mvn -pl server test -Dtest=PostTimeSeriesWriteHandlerIT -DskipITs=false- 7/7 pass, including newpartialWriteReportsDroppedMeasurements(mixed known/unknown ingest returns 400 naming the dropped measurement while the valid sample is persisted).204/200) and all-dropped error paths (400) keep their previous status codes.