Skip to content

[#174] Enforce syncFileRetentionCount, reuse an existing CSV snapshot of the same mtime and fail sync() on a token without a snapshot - #200

Open
maximthomas wants to merge 14 commits into
OpenIdentityPlatform:masterfrom
maximthomas:issues/174-csv-sync-snapshot-retention
Open

maximthomas wants to merge 14 commits into
OpenIdentityPlatform:masterfrom
maximthomas:issues/174-csv-sync-snapshot-retention

Conversation

@maximthomas

@maximthomas maximthomas commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #174.

CSVFileConnector.sync() now deletes the snapshots beyond syncFileRetentionCount, reuses an existing snapshot of the CSV file's current mtime instead of failing, advances the token to such a snapshot when it holds the bytes the sync read, matches snapshot names exactly, and fails on a token whose snapshot is gone instead of silently taking a new baseline. getLatestSyncToken takes a snapshot when none exists, so the first sync from its token reports every change made after the call.

  • Snapshots <csv>.<13-digit mtime> are listed by one helper, listSnapshots(), with Pattern.quote(name) + "\\.([0-9]{13})" and Matcher.matches(). The old pattern, "(" + name + ")(\\.[0-9]{13})$" with find(), also matched the snapshots of old_data.csv and dataXcsv, never matched those of data(1).csv, and threw PatternSyntaxException for data[1.csv. A directory that cannot be listed now fails with ConnectorIOException "Unable to list the files of " instead of a NullPointerException. The listing uses the directory of the absolute CSV file, and one helper, snapshotFile(), names every snapshot from the absolute CSV file. So a CSV file named without a directory (csvFile=data.csv) no longer fails every sync and getLatestSyncToken with a NullPointerException, and the scrub still recognises the snapshot the sync reads.
  • scrubSyncFiles now deletes files. At the start of every sync, under the write lock, it deletes the snapshots taken first until syncFileRetentionCount remain, but never the snapshot the sync is about to read. Snapshots are ordered by their own mtime, the time of their copy: each copy is given that mtime before it is renamed into place, because Files.copy gives it only on Linux and macOS, while on Windows its CopyFileEx keeps the mtime of the CSV file. A copy whose mtime cannot be set is logged as WARN "Could not set the mtime of copy of to the time of the copy" and used as it is. Snapshots of one own mtime keep the order of their names. The mtime in a name gives that order only while the mtime of the CSV file never moves back: after a restore from a backup, cp -p or rsync -t, the next scrub of another consumer deleted the snapshot just handed out as a token. A snapshot that cannot be deleted is logged as WARN "Could not delete " and kept. A count of 0 or below keeps only the snapshot the sync reads; a negative count used to throw ArrayIndexOutOfBoundsException. The syncFileRetentionCount help text now says what is kept.
  • A snapshot is taken as a complete copy renamed into place (takeSnapshot). If a snapshot of that mtime already exists, or another process takes it during the copy, it is reused as the baseline of a sync without a token. At the end of a sync, an existing snapshot of the copied mtime becomes the new token only if it holds the same bytes as the sync's copy. Every snapshot is named after the mtime read before its copy, so one taken in another JVM may also hold a later write that this sync has not reported. Then the token stays, and the next sync reports these changes again instead of never reporting that write. A snapshot that cannot be read, or that is gone by the time of the comparison, is logged as WARN "Could not compare with " and is not adopted. Before, the start copy failed with ConnectorException "Unable to copy CSV file for sync operation", and the end always kept the old token, so a second consumer of the same changes got them again on every sync. A failed sync deletes only the snapshots it made, never one it reused.
  • A sync from a token whose snapshot does not exist now fails with ConnectorException "Snapshot of sync token does not exist, so the changes since that token cannot be reported; reset the sync token". Before, it copied the live file as the baseline and so silently skipped every delete since the token.
  • getLatestSyncToken without any snapshot takes one of the CSV file and returns its mtime. Before, it returned the current time without a snapshot, so the first sync from that token copied the live file as its baseline and lost every change made in between. With snapshots, it returns the token of the one taken last, which the scrub keeps longest, instead of the token of the newest name.
  • A failed sync now deletes the snapshots it made before it releases the read lock, not afterwards under the write lock. The read lock keeps every other sync in this JVM out just as well, because each sync starts with the write lock. Between the two locks, a sync queued on the write lock could reuse such a snapshot and hand out its token; the delete then left that token without a snapshot. The read lock is released in a finally block of its own, so a delete that throws, such as with an Error, does not leave it held.
  • Unchanged:

Trade-offs:

  • Upgrade: the first sync after the upgrade deletes every snapshot accumulated beyond syncFileRetentionCount, under the write lock, so searches and writes on that CSV file in this JVM wait for it.
  • Upgrade: a stored token without a snapshot now fails once and needs a reset. That covers tokens the old getLatestSyncToken returned when no snapshot existed (the current time) that were never synced, and tokens whose snapshot a cleanup job deleted. Such cleanup jobs should be dropped: retention now does their work.
  • A consumer's stored token also loses its snapshot when other consumers of the same CSV file take more than syncFileRetentionCount snapshots after it, or when the count is 0 or below and the file has several consumers. Its next sync then fails instead of silently skipping deletes.
  • Up to syncFileRetentionCount + 1 snapshots stay on disk between syncs, or + 2 when the snapshot a sync read was taken before the kept ones.
  • Other JVMs are not excluded. A scrub can delete a snapshot that a sync in another JVM is reading. On POSIX, Files.move without options checks the target and then renames, so two JVMs can replace each other's snapshot of the same mtime. An existing snapshot of the copied mtime becomes the token only if its bytes match, so a later write in it is reported by the next sync instead of lost. In one JVM the bytes differ only when the file changed without its mtime changing, such as a write within the same mtime tick as an earlier sync's copy; that consumer then gets these changes again until the mtime moves.
  • getLatestSyncToken takes only the read lock, so it can return the snapshot of a sync that is failing at the same moment, which that sync then deletes. That needs a second consumer of the CSV file in this JVM; its next sync then fails and its token has to be reset.
  • A sync that finds the snapshot name of its copy taken reads that snapshot and its copy once more to compare them.
  • A snapshot reused by its name, as the baseline of a sync without a token or as the new token at the end of a sync, keeps the time of its first copy. Only when the mtime of the CSV file moves back to that of an existing snapshot does that time differ from when its token is handed out; the scrub can then delete that snapshot before the ones taken after its first copy.
  • On Windows, snapshots taken before this change carry the mtime the CSV file had when they were copied, so among themselves they keep the order of their names. That differs from the order they were taken only if the CSV file's mtime moved back before the upgrade.
  • The scrub and getLatestSyncToken read the mtime of every snapshot of the CSV file to order them, and every snapshot copy sets its own.
  • Snapshot names keep the unpadded mtime, while the listing matches exactly 13 digits, as before. A CSV file with an mtime before 2001-09-09, such as an epoch-dated file from a reproducible-build image, gets snapshots that retention never lists or deletes, one per such mtime. Sync stays correct, because a token's snapshot is looked up by name. Matching any number of digits would let the scrub delete unrelated files such as a data.csv.1 backup, and zero-padded names would break the stored tokens of such mtimes.
  • <csv>.<digits>.tmp files left by a killed process are still not removed automatically.
  • SyncOpTest#syncTestSuccessive now syncs from the snapshot that getLatestSyncToken takes, no longer through the re-copy for a missing snapshot.
  • SyncOpDiffTest#syncOfAFileNamedWithoutADirectoryKeepsTheSnapshotItReads creates a temporary CSV file in the surefire working directory, the module directory, and deletes it and its snapshots in a finally block. A killed test run leaves csv-sync-*.csv* files there.

Test strength

All cases are in SyncOpDiffTest unless named. "Red at " means that the case fails when this PR's tests run against the CSVFileConnector of that commit: 95c8375 is master, and 2c0760a, b542f0d, a2acb12 and 2ada596 are earlier commits of this PR.

arm case mutant killed red at
CSVFileConnector.java:1073, :1077 snapshot names are quoted and matched whole latestSyncTokenIgnoresSnapshotsOfOtherFiles (old_data.csv, dataXcsv), latestSyncTokenFindsTheSnapshotsOfAFileWithRegexMetacharactersInItsName (data(1).csv, data[1.csv), syncOfAFileWhoseNameIsAnInvalidRegexReportsItsChanges the old pattern with find() 95c8375
:1071 an unlistable directory fails with ConnectorIOException latestSyncTokenOfAnUnlistableDirectoryFails (POSIX, non-root) no null check 95c8375 (NullPointerException)
:1054-:1055, :1068 snapshot paths and the listing use the absolute CSV file syncOfAFileNamedWithoutADirectoryKeepsTheSnapshotItReads (a temporary CSV file in the working directory, count 1) the relative parent; an absolute listing with relative snapshot paths, whose scrub deletes the snapshot the sync reads (the sync then fails with "File … does not exist") 95c8375, b542f0d (NullPointerException)
:739 the snapshot taken last is the latest token SyncOpTest#syncTestSuccessive; latestSyncTokenIsThatOfTheSnapshotTakenLast (a snapshot of an older mtime taken after one of a newer mtime) the first listed snapshot; the newest name, as at a2acb12, also with the scrub in the order taken 95c8375, a2acb12 (the newest name)
:1384 an existing snapshot is reused, not copied again secondSyncWithoutATokenOfAnUnchangedFileReusesItsSnapshot no exists() check (4 copies instead of 3) 95c8375
:493 only a snapshot this call made goes into copies failedSyncKeepsTheSnapshotItReused always added 95c8375
:1392, :1395 a snapshot taken meanwhile by another process is kept, and the losing copy is deleted snapshotTakenMeanwhileByAnotherProcessIsKept (overrides copyCsvFile() to write the snapshot) the losing copy kept 95c8375 (the other process's snapshot is deleted)
:694-:695 an existing snapshot at the end with the same bytes advances the token existingSnapshotOfTheCopiedMtimeAdvancesTheToken, secondConsumerOfTheSameChangesGetsTheNewTokenToo the token kept, as before 95c8375
:694, :1407-:1422 an existing snapshot at the end with other bytes keeps the token and is not replaced existingSnapshotOfTheCopiedMtimeWithOtherBytesKeepsTheToken (a row added; a change of the same length past the first 8 KiB) always adopted, as at b542f0d; only the lengths compared; only the first 8 KiB compared b542f0d only (green at 95c8375)
:1425 an existing snapshot that cannot be read is logged and keeps the token existingSnapshotOfTheCopiedMtimeThatCannotBeReadKeepsTheToken (POSIX, non-root) adopted on a failed read b542f0d; 95c8375 (no WARN)
:689 an existing snapshot at the end is not added to copies failedSyncKeepsTheExistingSnapshotOfTheCopiedMtime copies.add(syncOrigin) in the arm that mutant only (green at 95c8375)
:1081-:1087 the scrub deletes snapshots in the order they were taken, by their own mtime syncKeepsTheTokenOfAnotherConsumerWhenTheCsvFileMtimeMovesBack (count 2, the CSV file's mtime set back below both tokens) the name order, as at a2acb12; the order reversed a2acb12 only (green at 95c8375, whose scrub deletes nothing)
:522, :1387, :1369 a snapshot copy gets the time of its copy as its mtime, also where Files.copy keeps the CSV file's mtime, as CopyFileEx does on Windows [true] of syncKeepsTheTokenOfAnotherConsumerWhenTheCsvFileMtimeMovesBack (the copy for the next token) and of latestSyncTokenIsThatOfABaselineTakenAfterTheCsvFileMtimeMovesBack (the baseline of a sync without a token, then the latest token); [true] overrides copyCsvFile() to keep the CSV file's mtime copyCsvFile() instead at :522, and at :1387, each killed by its own case 2ada596 ([true] of both); 95c8375 (both variants of the second case)
:1370-:1372 a copy whose mtime cannot be set is logged and used as it is copyDeletedBeforeItsMtimeIsSetLogsAWarning, copyDeletedBeforeTheIndexPassFailsSyncNamingTheCsvFile (the copy deleted at once) rethrown, which fails the sync as if the CSV file were gone; no WARN 2ada596, 95c8375 (the first case: no WARN)
:1087 snapshots of one own mtime keep the order of their names snapshotsTakenInOneTickAreScrubbedInTheOrderOfTheirNames (three snapshots of one own mtime, count 2) ties broken by descending name 95c8375 (nothing deleted)
:1098 the configured count is used syncDeletesTheSnapshotsBeyondTheRetentionCount (count 2) constant 3 95c8375
:1098, :1103 a count of 0 or below keeps only the snapshot the sync reads syncFromATokenKeepsOnlyTheSnapshotItReads, syncWithoutATokenKeepsOnlyItsBaseline (counts 0 and -1) an early return for a count below 1; scrubSyncFiles(copies.isEmpty() ? syncOrigin : null), which spares only a reused origin 95c8375 (nothing deleted at 0, ArrayIndexOutOfBoundsException at -1)
:1103 the snapshot the sync reads is spared syncKeepsTheSnapshotItReadsWhenItIsTheOldest (count 1) no check 95c8375
:1097 the scrub lists only this file's snapshots syncKeepsTheSnapshotsOfOtherFiles (count 1) the old pattern with find() that mutant only (green at 95c8375)
:1104 a snapshot that cannot be deleted is logged and kept snapshotThatCannotBeScrubbedIsLogged (POSIX, non-root) nothing deleted, as before 95c8375
:481 a token without a snapshot fails syncFromATokenWhoseSnapshotIsGoneFails the old re-copy 95c8375
:744 getLatestSyncToken takes a snapshot when there is none latestSyncTokenWithoutASnapshotTakesOne no snapshot taken 95c8375
:713, :717 a failed sync deletes its snapshots before it releases the read lock syncQueuedBehindAFailingSyncGetsATokenWithASnapshot (swaps a lock into fileNameToLockMap by reflection whose read-lock release waits until the queued sync has taken its baseline) the delete after the release with no lock held; the delete after the release under the write lock, as at 2c0760a; with the case repeated 20 times, each failed all 20 invocations (the first in 3 such runs), and a2acb12 passed all 20 (2 runs); with the release in its own finally block, the delete with no lock held still fails it 2c0760a (the token's snapshot is gone); 95c8375 (the queued sync fails with "Unable to copy…")
:717 the read lock is released even if a delete of a failed sync throws failedSyncReleasesTheReadLockWhenItsCleanupFails (POSIX, non-root: a read-only directory, and System.out swapped for a stream that throws, so the WARN "Could not delete" throws) the release after the delete loop outside a finally block, as at a2acb12 a2acb12 only (green at 95c8375, which released the lock first)
:745-:746 a failed copy in getLatestSyncToken fails the call — not pinned: catch (IOException e) { } survives, and the next sync would then fail with "Snapshot … does not exist" —
:1408-:1410 a snapshot gone by the time of the comparison is logged existingSnapshotOfTheCopiedMtimeDeletedMeanwhileIsLogged (a symbolic link to no file holds the snapshot name; skipped where links cannot be created) the File.length() check before the try, as at a2acb12, which returns 0 for a missing file 95c8375, a2acb12 (no WARN)
:1410 the size check before the byte comparison — not pinned: without it the comparison gives the same result, so it is only a shortcut —
:1077 the name must end after the 13 digits — not pinned: matches() → lookingAt() survives, and would list <csv>.<digits>.tmp files whose random part has 13 or more digits —

mvn -pl OpenICF-csvfile-connector test: 167 run, 0 failures, 0 skipped (132 at 95c8375). Of the 79 SyncOpDiffTest cases, 29 are red at 95c8375.

… releases the read lock

A sync queued on the write lock could take it between the failing sync's
read-lock release and its write-locked delete, reuse the failing sync's
snapshot and hand out its token, which the delete then left without a
snapshot. The read lock keeps other syncs out just as the write lock did.
@maximthomas maximthomas added bug Something isn't working java Pull requests that update java code tests Test additions or fixes connector:csvfile CSV file connector concurrency Races, locking and thread-safety fixes data-loss Fixes or risks loss or corruption of stored data resource-leak Memory, class-loader, thread or handle leaks labels Oct 21, 2026
A sync keeps only the snapshot it reads, for a token and for its own
baseline. Kills an early return for a count below 1 and a scrub that
spares only a reused origin.
… in its help text

The scrub runs when a sync starts and never deletes the snapshot that
sync reads, so "number of sync history files to retain" undercounts
what stays on disk.
…e absolute CSV file

A CSV file named without a directory has no parent file, so listing its
snapshots failed with a NullPointerException in every sync and in
getLatestSyncToken. The listing now uses the absolute directory, and so
does the one helper that names a snapshot: a relative origin would never
equal its listed file, and the scrub would delete the snapshot the sync
is about to read.
…snapshot only if it holds the same bytes

Every snapshot is named after the mtime read before its copy, so one a
sync in another JVM took may also hold a write made after that mtime was
read. Adopting it as the token hid that write from this consumer for
good. The end of a sync now adopts an existing snapshot only when it
holds the same bytes as the sync's copy; otherwise the token stays, and
the next sync reports these changes again. A snapshot that cannot be
read is logged and not adopted.
…c for a delete with no lock held

The fair lock let the queued sync take the write lock first, which pins
a delete taken back under the write lock, but a delete with no lock held
raced with the queued sync and survived most runs. The test now swaps in
a lock whose read-lock release waits until the queued sync has taken its
baseline, so both orders fail on every run.
@maximthomas
maximthomas marked this pull request as ready for review October 21, 2026 09:41
@maximthomas
maximthomas requested a review from vharseko October 21, 2026 09:41

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. The scrub orders snapshots by the mtime in their names, so when the CSV file's mtime moves back, it deletes the snapshot that another consumer just got as its token (CSVFileConnector.java:1079-1091)

scrubSyncFiles deletes the lowest keys first. These are the oldest snapshots only while the CSV file's mtime keeps moving forward. Suppose the file is replaced by one with an older mtime (cp -p, rsync -t, a restore from backup, touch -d). The sync that reports that change then names its snapshot with the smallest key, and the next sync of another consumer deletes that snapshot first. That consumer made no new snapshot. The trade-offs list covers tokens lost to "more than syncFileRetentionCount newer snapshots" made by other consumers, and here there are none.

Repro: a same-package TestNG case at a2acb12, with retention 2 and two consumers.

  • Given: data.csv.1300000000000 holds k (consumer B's token). data.csv.1400000000000 holds k, d (consumer A's token). The live file holds k, with its mtime set to 1200000000000.
  • A syncs from 1400000000000 and gets DELETE d and token 1200000000000. The snapshots are now .1200…, .1300…, .1400….
  • B syncs from 1300000000000 and gets no deltas. Its scrub deletes data.csv.1200000000000.
  • A syncs from 1200000000000 and fails: ConnectorException: Snapshot …/data.csv.1200000000000 of sync token 1200000000000 does not exist, so the changes since that token cannot be reported; reset the sync token.

With the live mtime at 1500000000000 instead, the same steps pass, because the scrub stops at B's origin, the oldest snapshot. On master the scrub deletes nothing, so A's token survives in both cases.

A possible fix is to delete snapshots in the order they were taken and use the name only as the token. The snapshot file's own mtime gives that order: copyCsvFile copies without COPY_ATTRIBUTES, and Files.move keeps the copy's mtime, so it is the time of the copy. If that is out of scope, the case could go into the trade-offs instead.

2. Nit: the read lock is now released after the delete loop, outside a finally (:707-:713)

Before, rwLock.readLock().unlock() was the first statement of the finally. Now it comes after for (File copy : copies) deleteRewriteFile(copy). If that loop throws, the unlock is skipped. In practice that means an Error, or a SecurityException from File.delete(). Every later write and sync on that CSV file in this JVM then blocks on the write lock for good. A nested try { … } finally { rwLock.readLock().unlock(); } keeps the new order and still guarantees the unlock.

3. Nit: a snapshot deleted meanwhile is rejected without the WARN (:1374)

The length check runs before the try, and File.length() returns 0 for a missing file. The javadoc's own example, "one deleted meanwhile in another JVM", therefore returns false at the length check, and "Could not compare …" is never logged. The result is right; only the WARN that the description promises is missing. Comparing Files.size(...) inside the try would log it, because Files.size throws NoSuchFileException.

…st token in the order they were taken

The scrub deleted the snapshots with the lowest mtime in their names
first. Those are the oldest only while the CSV file's mtime moves
forward. After a restore from a backup, cp -p or rsync -t, the sync that
reports the change names its snapshot with the lowest mtime, and the
next scrub of another consumer deleted it, so the next sync from that
token failed.

A snapshot is copied without the attributes of the CSV file and renamed
into place, so its own mtime is the time of its copy. listSnapshots()
now orders snapshots by that mtime, with a stable sort over the name
order, so snapshots taken in one tick of a coarse clock keep the order of
their names. getLatestSyncToken returns the token of the snapshot taken
last, which the scrub keeps longest, instead of the newest name, which
the scrub could now delete first.
… even if deleting its snapshots throws

The read lock was released after the loop that deletes the snapshots of
a failed sync, outside a finally block. An Error or a SecurityException
from that loop left it held, and every later write and sync of that CSV
file in this JVM then waited for the write lock for good. The release
now runs in a finally block of its own, still after the loop, and the
copy for the diff is deleted after it as before.
… byte comparison

The length check ran before the try block, and File.length() returns 0
for a missing file, so a snapshot deleted meanwhile in another JVM was
rejected without the "Could not compare" WARN. The sizes are now
compared after both files are opened, where a missing snapshot throws
NoSuchFileException and is logged.
@maximthomas

Copy link
Copy Markdown
Contributor Author

Answering review 5614427012 (at a2acb12). New head: 2ada596. All three points held; the module runs 163 tests, 0 failures, and the PR body (Trade-offs, Test strength) is updated.

1. Scrub order when the CSV file's mtime moves back (ce1ca89)

Your case, as syncKeepsTheTokenOfAnotherConsumerWhenTheCsvFileMtimeMovesBack (count 2, live mtime set to 1200000000000), failed at a2acb12 as you traced it: B's scrub deleted data.csv.1200000000000, and A's next sync could not run.

listSnapshots() now orders snapshots by their own mtime. It uses a stable sort over the name order, so snapshots of one own mtime, such as those taken in one tick of a coarse file system clock, keep the order of their names (snapshotsTakenInOneTickAreScrubbedInTheOrderOfTheirNames). I checked the premise that a snapshot's own mtime is the time of its copy. copyCsvFile() followed by Files.move gave the copy time, not the CSV file's mtime, on JDK 8, 11, 17, 21, 25 and 26 (macOS). Every earlier version of the connector also took its snapshots with Files.copy without COPY_ATTRIBUTES, so snapshots left by an older version sort the same way.

getLatestSyncToken uses the same order and now returns the token of the snapshot taken last, not the newest name. If only the scrub changed, getLatestSyncToken would hand out the token of the newest name after the mtime moves back, and the new scrub would delete that snapshot first. That is the same loss, only for a new consumer. latestSyncTokenIsThatOfTheSnapshotTakenLast pins this (red at 95c8375 and a2acb12).

One case is left, and it is listed under Trade-offs. A snapshot reused by its name (the baseline of a sync without a token, or an existing snapshot adopted at the end) keeps the time of its first copy. That only matters when the mtime moves back to the mtime of an existing snapshot.

2. Read lock released outside a finally (3cefff4)

Fixed with the nested try/finally you suggested. The delete of next moved into the inner finally as well, after the release. failedSyncReleasesTheReadLockWhenItsCleanupFails pins it. In a read-only directory the delete fails, and System.out is swapped for a stream that throws, so the WARN "Could not delete" throws from inside the loop. At a2acb12 the read lock stayed held (getReadLockCount() was 1). With the new shape, syncQueuedBehindAFailingSyncGetsATokenWithASnapshot still fails the mutant that releases the lock before the delete.

3. No WARN for a snapshot deleted meanwhile (2ada596)

The size check now runs inside the try, after both streams are opened. A missing snapshot therefore throws NoSuchFileException and is logged. existingSnapshotOfTheCopiedMtimeDeletedMeanwhileIsLogged puts a symbolic link to no file at the snapshot name: Files.move finds the name taken, and the comparison then finds no file. The test is skipped where links cannot be created. It logged nothing at a2acb12. Without the size check the comparison still returns the same result, so the check itself stays an unpinned shortcut, as listed.

@maximthomas
maximthomas requested a review from vharseko October 23, 2026 08:25
for (String name : names) {
final Matcher matcher = snapshotName.matcher(name);
if (matcher.matches()) {
byName.put(Long.valueOf(matcher.group(1)), new File(dataDir, name));
…y as its mtime on Windows too

The scrub and getLatestSyncToken order snapshots by their own mtime,
taken to be the time of their copy. Files.copy gives a copy that mtime
on Linux and macOS only: on Windows it calls CopyFileEx, which keeps the
last-write time of the CSV file, so the order fell back to the mtime in
the names. Both Windows CI jobs failed
syncKeepsTheTokenOfAnotherConsumerWhenTheCsvFileMtimeMovesBack: after
the CSV file's mtime moved back, the scrub of another consumer deleted
the snapshot just handed out as a token.

Each copy for a snapshot now gets the current time as its mtime before
it is renamed into place. A copy whose mtime cannot be set is logged as
a WARN and used as it is, so a copy deleted meanwhile still fails the
read that follows, as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working concurrency Races, locking and thread-safety fixes connector:csvfile CSV file connector data-loss Fixes or risks loss or corruption of stored data java Pull requests that update java code resource-leak Memory, class-loader, thread or handle leaks tests Test additions or fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSVFile connector: sync() never deletes old snapshots, and fails or keeps the old token when the current file already has a snapshot

3 participants