Skip to content

A stopped job never comes back, and a replayed argv would re-download the mirror - #218

Merged
xroche merged 4 commits into
masterfrom
phase3-stage4-retry-resumes
Sep 14, 2026
Merged

xroche merged 4 commits into
masterfrom
phase3-stage4-retry-resumes

Conversation

@xroche

@xroche xroche commented Sep 14, 2026

Copy link
Copy Markdown
Owner

A stopped job did not come back. onStopJob returned false, so a crawl the system stopped waited for the user instead of continuing.

It now returns JobStopPolicy's verdict. A user stop from the Task Manager, an app cancel and a background restriction do not retry. Everything else does, timeouts included.

The retry has to resume rather than start again, and that is the whole of this change. A rescheduled job replays its argv, and the engine's own resume detection loses to it. hts-in_progress.lock sets HTS_CACHE_PRIORITY before the command line is parsed, and the parse then overrides it, so a replayed -C0 or -iC2 re-downloads the mirror. ResumeArgv drops those tokens and forces -iC1 at index 1, always ahead of the first URL. Position matters: an -iC* past the first URL leaves argv_url at 0 and the engine splices hts-cache/doit.log over the whole command line.

Being interrupted is not enough to know a retry happened. A first attempt on a project holding a stale lock may be a deliberate Update, or a deliberate unticked cache. Forcing the continue mode there would override the user, while the pre-34 path would not. So the job stamps its own attempt marker, and clears it on every start the user asks for.

A retry in a fresh process used to crash before it resumed anything. onStartJob built a CrawlRun, whose engine field calls native init() at once, while the libraries loaded later on the crawl thread. Only a process the system starts for the job alone was affected, which is why nothing caught it. origin/master has the same ordering, so this fixes an existing bug rather than one this stage introduced.

493 tests pass, up from 467, and 19 mutations each turned a test red. On a device a killed crawl restarts once and reaches the engine in 270ms. It then fetches 234 paths, none of them fetched before the kill. 68 plus 234 is every path on the site, each fetched exactly once.

Part of #74, phase 3 stage 4 of 5.

Signed-off-by: Xavier Roche xroche@gmail.com
Co-authored-by: Claude Opus 5 (1M context) noreply@anthropic.com

xroche and others added 4 commits September 14, 2026 08:16
onStopJob returned false, so a crawl the system stopped waited for the user.
It now returns JobStopPolicy's verdict, and the argv a retry replays is
rewritten so the engine resumes instead of downloading the mirror again.

The engine raises HTS_CACHE_PRIORITY by itself when it finds
hts-cache/hts-in_progress.lock, then lets the command line override it.  Two
Android options reach that field: -iC2 from the Update radio and -C0 from an
unticked cache box.  ResumeArgv.forStart drops both and puts one -iC1 at index
1, which is ahead of every URL.  Position is the part that is not negotiable:
an -iC* past the first URL sets argv_url to -1, and the engine then loads
hts-cache/doit.log over the whole command line.

Only a retry rewrites.  A first attempt over an already-interrupted project is
the user picking Continue or Update in the setup pane, and that choice has to
stand, so the job stamps hts-cache/job-attempt.lock before it reaches the
engine and every start the user asks for clears it.  The engine's own lock
cannot answer this: it says the mirror is resumable, not that this execution is
the second one.  No marker is added at crawl start, because the engine's lock
plus our interrupted.lock already cover every window.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
forStart filters -iC* and -C* out and then inserts at index 1, so an argv whose
every token was dropped ({"-C0"}) leaves an empty list and out.add(1, ...)
throws IndexOutOfBoundsException.  CrawlArgv.build always writes "httrack"
first, so no caller reaches it today, but a total function costs one Math.min.

ResumeArgv is also position-blind, which the filter loop now says: an engine
dashvalue_opt field whose value the user typed as -C0 is dropped as if it were
the option.  Restructuring for that would need the engine's own argument
pre-pass, so it is recorded rather than fixed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Three defects the reviewers found in the retry path.

Pressing Stop left the job armed.  stopCrawl() reached MirrorJobService.cancel
only where no owner answered, so a crawl the user stopped kept its scheduled
job while the engine wound down.  A system stop in that window returns
JobStopPolicy.reschedules(), CrawlRun's finally has already stamped
interrupted.lock, and the retry restarts a mirror the user cancelled.  The
cancel now sits outside the owner branches, under
JobStopPolicy.cancelsScheduledJob(force, ownerAnswered).  It is not literally
unconditional, because a soft interrupt keeps the job.  cancel() makes the
system call onStopJob, which stops the engine hard, and that would cut the
pending transfers the first press exists to let finish.  The finished pane and
the offline abandon both pass force, so both reach the cancel.

A retry could be consumed by the wind-down of the crawl it was retrying.  The
next execution hits ProfileLockPolicy.alreadyInProgress, reports "already in
progress", and its finally called jobFinished(params, false), abandoning the
mirror.  CrawlRun now records that refusal, and runCrawl reschedules on it.  The
reschedule fires only where the previous execution of this job was still running
when this one started.  That is what separates a wind-down from a genuine second
crawl of the same project, which must stay refused rather than loop.  An
activity-owned crawl, or one the user started again, leaves the job's own slot
empty or ended.  The flag is read before the slot is overwritten, so it answers
for the predecessor rather than for the new run.  A wrong guess costs one extra
retry, since the execution it names is the refused one, which ends at once.

alreadyAttempted had no behavioural test.  Returning false when the stamp exists
and true after creating it kept everyUserStartClearsTheAttemptStamp green,
because that guard counts occurrences and reads assignments.  The inverted
method overrides the user's own Continue or Update on a first attempt and
re-downloads the mirror on a retry.  It is pure java.io.File work, so it is now
package-private and table-tested.  The rows: no stamp, stamp present, stamp
created then read, and a stamp that cannot be written at all.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
onStartJob builds a CrawlRun, and CrawlRun's engine field calls native
init() as it is constructed. The libraries were only loaded later, on the
crawl thread. A process that had already run HTTrackActivity survived,
because onCreate had loaded them. A process the system started for the job
alone died with UnsatisfiedLinkError. The system restarted the service,
it crashed again, and after the third try the job was killed. That is the
case stage 4 exists for, so the stage did nothing in its own scenario.

Load the libraries in onStartJob, after setNotification so the ten-second
deadline is untouched, and before the construction. A load that fails logs
and returns false. That ends the job without a reschedule and leaves the
crawl to the activity, the same answer schedule() gives when the system
refuses the job.

The freshProcess flag was dead code, because loadedSuccessfully() answers
true before any load. So initRootPath never ran from the job and a fresh
process had no emergency-log path. The flag now reads the new
HTTrackLib.loadAttempted(), which is what the comment beside it always
described.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit 5c47f48 into master Sep 14, 2026
6 of 7 checks passed
@xroche
xroche deleted the phase3-stage4-retry-resumes branch September 14, 2026 07:52
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