Skip to content

Commit 429153a

Browse files
committed
fix(client): batch manifest updates during segment trim
1 parent d5673b3 commit 429153a

4 files changed

Lines changed: 286 additions & 53 deletions

File tree

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentManager.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -962,37 +962,63 @@ private boolean serviceRing0(RingEntry e) {
962962
boolean trimFailed = false;
963963
Throwable trimFailure = null;
964964
int unlinked = 0;
965+
int batchSize = 0;
965966
MmapSegment segment = first;
966967
long coveredAck = durableAck;
967-
while (segment != null && unlinked < MAX_TRIMS_PER_RING_PASS) {
968+
while (segment != null && batchSize < MAX_TRIMS_PER_RING_PASS) {
968969
long lastSeq = segment.baseSeq() + segment.frameCount() - 1L;
969970
if (lastSeq > coveredAck) {
970971
break;
971972
}
972-
MmapSegment next = e.ring.nextSealedAfter(segment);
973-
String path = segment.path();
973+
trimBatch[batchSize++] = segment;
974+
segment = e.ring.nextSealedAfter(segment);
975+
}
976+
if (batchSize > 0) {
974977
try {
975-
// Durably commit the head advance past this segment before
976-
// unlinking it. The ring recomputes the successor under its
977-
// own monitor -- computing it here from `next` would race
978-
// with a concurrent rotation sealing the active, letting the
979-
// head leapfrog a still-unacked sealed segment (whose file a
980-
// later recovery would then discard as "stale below head").
981-
e.ring.advanceManifestHeadPast(segment);
982-
segment.close();
983-
// A retry after a post-unlink directory-sync failure sees an
984-
// already absent path. Treat absence as the prior successful
985-
// unlink and retry the batch barrier rather than wedging.
986-
if (!filesFacade.remove(path) && filesFacade.exists(path)) {
978+
// Durably commit the head advance past the LAST batch member
979+
// before unlinking any of them. One commit covers the whole
980+
// batch: head values are segment boundaries and the batch is
981+
// a contiguous prefix of the sealed chain, so recovery
982+
// discards every batch member as "stale below head" whether
983+
// the crash lands mid-unlink or after -- byte-identical to a
984+
// per-segment commit, minus up to 63 device flushes. The ring
985+
// recomputes the successor under its own monitor --
986+
// computing it here from the walk above would race with a
987+
// concurrent rotation sealing the active, letting the head
988+
// leapfrog a still-unacked sealed segment (whose file a later
989+
// recovery would then discard as "stale below head").
990+
e.ring.advanceManifestHeadPast(trimBatch[batchSize - 1]);
991+
} catch (Throwable t) {
992+
for (int i = 0; i < batchSize; i++) {
993+
trimBatch[i] = null;
994+
}
995+
recordTrimFailure(e, TRIM_RETRY_UNLINK, now, t);
996+
return false;
997+
}
998+
while (unlinked < batchSize) {
999+
MmapSegment trimming = trimBatch[unlinked];
1000+
String path = trimming.path();
1001+
try {
1002+
trimming.close();
1003+
// A retry after a post-unlink directory-sync failure sees
1004+
// an already absent path. Treat absence as the prior
1005+
// successful unlink and retry the batch barrier rather
1006+
// than wedging. A retry after a mid-batch unlink failure
1007+
// re-collects from firstTrimmable(); its head advance
1008+
// clamps to the already-committed boundary (no fsync).
1009+
if (!filesFacade.remove(path) && filesFacade.exists(path)) {
1010+
trimFailed = true;
1011+
break;
1012+
}
1013+
unlinked++;
1014+
} catch (Throwable t) {
9871015
trimFailed = true;
1016+
trimFailure = t;
9881017
break;
9891018
}
990-
trimBatch[unlinked++] = segment;
991-
segment = next;
992-
} catch (Throwable t) {
993-
trimFailed = true;
994-
trimFailure = t;
995-
break;
1019+
}
1020+
for (int i = unlinked; i < batchSize; i++) {
1021+
trimBatch[i] = null;
9961022
}
9971023
}
9981024

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentRing.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -518,13 +518,17 @@ static Recovery recover(
518518
}
519519

520520
/**
521-
* Durably advances the manifest head past {@code trimming} (the sealed
522-
* segment the manager is about to unlink). The successor and the current
523-
* active are both read under the ring monitor, so a concurrent rotation
524-
* (which also mutates the manifest under this monitor) can never make the
525-
* head leapfrog a still-live sealed segment: if rotation sealed the old
526-
* active after the caller's snapshot, {@code trimming.successor()} now
527-
* points at that sealed segment, not at the new active.
521+
* Durably advances the manifest head past {@code trimming} (the LAST
522+
* sealed segment of the bounded batch the manager is about to unlink).
523+
* One durable commit covers every earlier batch member: head values are
524+
* segment boundaries and the batch is a contiguous prefix of the sealed
525+
* chain, so recovery discards each member as "stale below head"
526+
* regardless of how far the unlink loop got. The successor and the
527+
* current active are both read under the ring monitor, so a concurrent
528+
* rotation (which also mutates the manifest under this monitor) can never
529+
* make the head leapfrog a still-live sealed segment: if rotation sealed
530+
* the old active after the caller's snapshot, {@code trimming.successor()}
531+
* now points at that sealed segment, not at the new active.
528532
*/
529533
synchronized void advanceManifestHeadPast(MmapSegment trimming) {
530534
if (manifest == null) {

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SfManifest.java

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ final class SfManifest implements QuietCloseable {
4949
private final int fd;
5050
private final FilesFacade filesFacade;
5151
private final String path;
52+
// Preallocated record scratch for update(): the trim path calls update()
53+
// once per batch and must not malloc/free per call. Guarded by this
54+
// object's monitor (update() and close() are both synchronized).
55+
private final long writeScratch;
5256
private long activeBase;
5357
private boolean closed;
5458
private long generation;
@@ -62,6 +66,7 @@ private SfManifest(FilesFacade filesFacade, String path, int fd,
6266
this.generation = generation;
6367
this.headBase = headBase;
6468
this.activeBase = activeBase;
69+
this.writeScratch = Unsafe.malloc(RECORD_SIZE, MemoryTag.NATIVE_DEFAULT);
6570
}
6671

6772
static SfManifest create(FilesFacade filesFacade, String dir, long headBase, long activeBase) {
@@ -71,11 +76,12 @@ static SfManifest create(FilesFacade filesFacade, String dir, long headBase, lon
7176
throw new MmapSegmentException("exclusive create failed for SF manifest " + path);
7277
}
7378
boolean success = false;
79+
SfManifest manifest = null;
7480
try {
7581
if (!filesFacade.allocate(fd, FILE_SIZE)) {
7682
throw new MmapSegmentException("could not allocate SF manifest " + path);
7783
}
78-
SfManifest manifest = new SfManifest(filesFacade, path, fd, 0, -1, -1);
84+
manifest = new SfManifest(filesFacade, path, fd, 0, -1, -1);
7985
manifest.update(headBase, activeBase);
8086
if (filesFacade.fsyncDir(dir) != 0) {
8187
throw new MmapSegmentException("could not sync SF manifest directory " + dir);
@@ -84,7 +90,13 @@ static SfManifest create(FilesFacade filesFacade, String dir, long headBase, lon
8490
return manifest;
8591
} finally {
8692
if (!success) {
87-
filesFacade.close(fd);
93+
if (manifest != null) {
94+
// close() frees the constructor-owned scratch buffer as
95+
// well as the fd; closing only the raw fd would leak it.
96+
manifest.close();
97+
} else {
98+
filesFacade.close(fd);
99+
}
88100
filesFacade.remove(path);
89101
}
90102
}
@@ -151,9 +163,13 @@ long activeBase() {
151163
}
152164

153165
@Override
154-
public void close() {
166+
public synchronized void close() {
167+
// Synchronized against update() so the scratch buffer can never be
168+
// freed under a concurrent writer; update() checks `closed` inside
169+
// the same monitor.
155170
if (!closed) {
156171
closed = true;
172+
Unsafe.free(writeScratch, RECORD_SIZE, MemoryTag.NATIVE_DEFAULT);
157173
filesFacade.close(fd);
158174
}
159175
}
@@ -202,29 +218,24 @@ synchronized void update(long newHeadBase, long newActiveBase) {
202218
return;
203219
}
204220
long nextGeneration = generation + 1;
205-
long buffer = Unsafe.malloc(RECORD_SIZE, MemoryTag.NATIVE_DEFAULT);
206-
try {
207-
Unsafe.getUnsafe().setMemory(buffer, RECORD_SIZE, (byte) 0);
208-
Unsafe.getUnsafe().putInt(buffer, MAGIC);
209-
Unsafe.getUnsafe().putInt(buffer + 4, VERSION);
210-
Unsafe.getUnsafe().putLong(buffer + 8, nextGeneration);
211-
Unsafe.getUnsafe().putLong(buffer + 16, newHeadBase);
212-
Unsafe.getUnsafe().putLong(buffer + 24, newActiveBase);
213-
int crc = Crc32c.update(Crc32c.INIT, buffer, CRC_OFFSET);
214-
Unsafe.getUnsafe().putInt(buffer + CRC_OFFSET, crc);
215-
long offset = (nextGeneration & 1L) * RECORD_SIZE;
216-
if (filesFacade.write(fd, buffer, RECORD_SIZE, offset) != RECORD_SIZE) {
217-
throw new MmapSegmentException("short write updating SF manifest " + path);
218-
}
219-
if (filesFacade.fsync(fd) != 0) {
220-
throw new MmapSegmentException("could not sync SF manifest " + path);
221-
}
222-
generation = nextGeneration;
223-
headBase = newHeadBase;
224-
activeBase = newActiveBase;
225-
} finally {
226-
Unsafe.free(buffer, RECORD_SIZE, MemoryTag.NATIVE_DEFAULT);
221+
Unsafe.getUnsafe().setMemory(writeScratch, RECORD_SIZE, (byte) 0);
222+
Unsafe.getUnsafe().putInt(writeScratch, MAGIC);
223+
Unsafe.getUnsafe().putInt(writeScratch + 4, VERSION);
224+
Unsafe.getUnsafe().putLong(writeScratch + 8, nextGeneration);
225+
Unsafe.getUnsafe().putLong(writeScratch + 16, newHeadBase);
226+
Unsafe.getUnsafe().putLong(writeScratch + 24, newActiveBase);
227+
int crc = Crc32c.update(Crc32c.INIT, writeScratch, CRC_OFFSET);
228+
Unsafe.getUnsafe().putInt(writeScratch + CRC_OFFSET, crc);
229+
long offset = (nextGeneration & 1L) * RECORD_SIZE;
230+
if (filesFacade.write(fd, writeScratch, RECORD_SIZE, offset) != RECORD_SIZE) {
231+
throw new MmapSegmentException("short write updating SF manifest " + path);
232+
}
233+
if (filesFacade.fsync(fd) != 0) {
234+
throw new MmapSegmentException("could not sync SF manifest " + path);
227235
}
236+
generation = nextGeneration;
237+
headBase = newHeadBase;
238+
activeBase = newActiveBase;
228239
}
229240

230241
/**

0 commit comments

Comments
 (0)