Skip to content

perf(transformer): copy file content outside the FileTransformer lock#178

Merged
marevol merged 3 commits into
masterfrom
perf/crawler-concurrency-fixes
Jul 9, 2026
Merged

perf(transformer): copy file content outside the FileTransformer lock#178
marevol merged 3 commits into
masterfrom
perf/crawler-concurrency-fixes

Conversation

@marevol

@marevol marevol commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

FileTransformer.storeData held one synchronized (this) monitor across both the target-name selection and the full content copy. Because the transformer is a shared singleton, every crawler thread serialized on that single lock for the entire I/O-bound file write, so file-mirroring crawls had essentially no write concurrency. The name selection was also a check-then-act (exists() then pick) with a TOCTOU race.

Changes

  • createFile is now synchronized and reserves the leaf file atomically via File.createNewFile() (retrying name, name_0, name_1, … on collision). This both closes the TOCTOU race and keeps the critical section short (directory bookkeeping + one atomic create per candidate).
  • storeData performs CopyUtil.copy(is, os) outside any lock, so concurrent threads write their files in parallel.

Single-threaded behavior is unchanged: the same file path is chosen (name, then name_0, …) and the same content is written, with the same exception handling. The only edge-case change is the effectively-unreachable name-exhaustion path (maxDuplicatedPath collisions), which now throws a clear CrawlerSystemException from createFile instead of silently returning the parent directory and failing later when the stream is opened (still a CrawlerSystemException, just earlier and clearer).

Testing

  • test_storeData_concurrent_sameUrl — 20 threads store the same URL concurrently; asserts 20 distinct files (name, name_0name_18), each with its correct content and no extra file.
  • test_storeData_concurrent_copyRunsOutsideLock — a barrier-gated input stream across 6 threads deterministically proves the copy is no longer serialized (this test times out/fails against the original locked-copy code and passes in ~0.1s with the fix).
  • Existing test_createFile / test_transform unchanged and passing; full fess-crawler module suite passes (1980 tests); mvn formatter:format / license:format / javadoc:jar clean.

marevol added 3 commits July 9, 2026 12:50
FileTransformer.storeData held a single synchronized(this) monitor across
both the target-name selection and the full content copy, so every crawler
thread serialized on one lock for the entire I/O-bound file write.

Reserve the target file atomically inside the lock (File.createNewFile with
_N suffixes on collision, which also closes the previous check-then-act
race) and perform CopyUtil.copy outside the lock, so concurrent threads
write their files in parallel. Single-threaded naming and output are
unchanged; the unreachable name-exhaustion case now throws a clearer
CrawlerSystemException instead of failing later at write time.
…throws

Add a test for the CrawlerSystemException that reserveFile throws when all
maxDuplicatedPath candidate names are exhausted (previously uncovered), and
correct storeData's @throws javadoc: I/O errors while copying the response
body surface as IORuntimeException from CopyUtil, not wrapped here as
CrawlerSystemException.
@marevol marevol force-pushed the perf/crawler-concurrency-fixes branch from 81e7828 to 5b864f5 Compare July 9, 2026 03:53
@marevol marevol merged commit 64c6bb2 into master Jul 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant