fix: a crawl cancel waits for its job, and a stale crawl is ended

Cancelling a running crawl records cancel_requested_at and leaves the
row running until its job stops and writes cancelled, so no second
crawl starts and the row cannot be deleted or pruned meanwhile. The job
writes heartbeat_at every minute; a running crawl without a heartbeat
for 10 minutes is ended by the next start or delete.
This commit is contained in:
Jakub Zych
2026-11-04 22:51:34 +01:00
parent d7597bb1a5
commit cd7182395f
12 changed files with 351 additions and 30 deletions

View File

@@ -186,7 +186,7 @@ func crawlMap(ctx context.Context, svc *classes.Service, c models.Crawl) map[str
"id": v.ID, "status": v.Status, "trigger": v.Trigger, "trigger_name": v.TriggerName, "site": v.Site,
"max_pages": v.MaxPages, "pages": v.Pages, "limit_hit": v.LimitHit, "error": v.Error,
"started_at": v.StartedAt, "finished_at": v.FinishedAt, "created_at": v.CreatedAt,
"cancelled_by": c.CancelledBy, "render": renderView(ctx, svc, c),
"cancelled_by": c.CancelledBy, "cancel_requested": c.CancelRequestedAt != nil, "render": renderView(ctx, svc, c),
}
}
@@ -230,9 +230,10 @@ func ShowCrawl(service func() *classes.Service) http.HandlerFunc {
})
}
// CancelCrawl serves POST /_seo/api/v1/crawls/{id}/cancel: a queued or
// running crawl stops and keeps what it fetched; another answers 409
// crawls.not_running.
// CancelCrawl serves POST /_seo/api/v1/crawls/{id}/cancel: a queued crawl
// ends cancelled at once; a running one answers with cancel_requested true
// and stays running until its job stops, then ends cancelled and keeps
// what it fetched; another answers 409 crawls.not_running.
func CancelCrawl(service func() *classes.Service) http.HandlerFunc {
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
id, err := pathID(r, "id")

View File

@@ -81,7 +81,7 @@ func (c crawlsAdminController) AdminRecordActions() []pact.AdminRecordAction {
Permissions: []string{PermissionManage},
Applies: func(_ context.Context, record any) (bool, error) {
row, ok := record.(*models.Crawl)
return ok && row != nil && row.Active(), nil
return ok && row != nil && row.Active() && !row.Cancelling(), nil
},
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
row, ok := in.Record.(*models.Crawl)
@@ -194,7 +194,9 @@ func (c crawlsAdminController) ListExtendRecords(ctx context.Context, db *gorm.D
row.IssuesCell = strconv.Itoa(counts[row.ID])
row.DurationCell = Duration(*row)
row.TriggerCell = TriggerLabel(ctx, svc, *row)
if key, ok := statusKeys[row.Status]; ok {
if row.Cancelling() {
row.Status = svc.PhraseCtx(ctx, "golem15.seo::lang.crawls.st_cancelling", nil)
} else if key, ok := statusKeys[row.Status]; ok {
row.Status = svc.PhraseCtx(ctx, key, nil)
}
}