Skip to content

Skip unnecessary source database refresh for unchanged files#15

Closed
dinkelk wants to merge 3 commits into
masterfrom
fix/source-db-refresh-optimization
Closed

Skip unnecessary source database refresh for unchanged files#15
dinkelk wants to merge 3 commits into
masterfrom
fix/source-db-refresh-optimization

Conversation

@dinkelk
Copy link
Copy Markdown
Owner

@dinkelk dinkelk commented Apr 5, 2026

Summary

  • When redo-ifchange encounters a source file from within a .do file, it previously called initializeSourceDatabase unconditionally — deleting and recreating the entire database directory even if nothing changed.
  • This opens a corruption window: if the process is killed (e.g. Ctrl+C triggering SIGKILL via the process group handler) between the delete and the markSource write, the database is left without a source marker, causing permanent "No rule to build" errors in projects with a catch-all default.do.
  • Now we compare the current file stamp against the cached stamp first. If they match, we skip the refresh entirely — closing the corruption window for the vast majority of source files on incremental builds.

Test plan

  • Existing test suite passes (all 370-done, 998-corner-cases, etc.)
  • New test: unchanged sources skip DB refresh (verified via inode stability)
  • New test: changed sources still trigger refresh and dependent rebuilds
  • New test: new source files get properly initialized
  • Run against real Adamant project build to verify no regressions

🤖 Generated with Claude Code

Gus and others added 3 commits April 5, 2026 12:44
When redo-ifchange encounters a source file from within a .do file, it
previously called initializeSourceDatabase unconditionally — even if the
file hadn't changed. That function deletes and recreates the entire
database directory, which opens a corruption window: if the process is
killed (e.g. Ctrl+C triggering SIGKILL via the process group handler)
between the delete and the markSource write, the database is left without
a source marker. This causes permanent "No rule to build" errors in
projects with a catch-all default.do (like Adamant).

Now we compare the current file stamp against the cached stamp first. If
they match, we skip the refresh entirely — the database is already in the
correct state. This eliminates the corruption window for the vast majority
of source files on incremental builds (only files that actually changed
need the refresh).

Includes tests verifying:
- Unchanged sources skip DB refresh (inode stability check)
- Changed sources still trigger refresh and dependent rebuilds
- New sources get properly initialized

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, initializeSourceDatabase would delete the entire database
directory (refreshDatabase) before writing the source marker. If the
process was killed between the delete and the markSource write (e.g.
Ctrl+C triggering SIGKILL via the process group handler), the database
was left without a source marker — causing permanent corruption.

Now the source marker ("y" entry) is written FIRST, before any
destructive operations. Stale target entries (d, e, r, c, a, p) are
then cleaned up individually. If the process is killed at any point
after markSource, the source marker always exists and the file will
be correctly recognized as a source on the next build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The C-level signal handler that sent SIGKILL to the entire process group
on Ctrl+C was introduced to fix hangs where GHC's RTS couldn't deliver
async exceptions while the main thread was blocked in waitpid. However,
SIGKILL gives processes zero time to finish database operations, which
can corrupt the redo database (e.g. source markers lost mid-write).

This replaces the blocking waits with non-blocking polling loops:
- Build.hs: waitForProcessInterruptible polls getProcessExitCode
  every 50ms instead of blocking in waitForProcess
- JobServer.hs: waitOnJob and runJobs use non-blocking getProcessStatus
  with 50ms polling instead of blocking getProcessStatus

Between polls, GHC's RTS can deliver async exceptions (UserInterrupt
from SIGINT), so Ctrl+C is handled promptly without needing SIGKILL.
The onException handler in runDoFile then terminates the child process
group via interruptProcessGroupOf (children run in their own process
group via create_group=True).

Also removes:
- installKillGroupHandler from Main.hs (no longer needed)
- resetSignalHandlers from JobServer.hs (no SIGKILL handler to reset)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dinkelk dinkelk closed this Apr 8, 2026
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