mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 03:27:42 +01:00
* admin/plugin: fail a dispatch its restarted worker no longer lists A job dispatched to a worker that then restarts dies with the old process. The worker re-registers under the same id on a new stream and its heartbeats stop listing the job, but nothing ended the pending execution: the dispatcher kept waiting and held the executor slot until the attempt's execution timeout. Each pending execution now records the stream it went out on. An itemized heartbeat from a newer stream of that worker that leaves the job out twice in a row fails it through the expire path, so the dispatch returns, the slot is released and the retry machinery takes over. Jobs on the heartbeat's own stream, jobs the new stream still lists, and heartbeats that count slots without itemizing them are left alone. Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi * admin/plugin: let a partly itemized listing clear a dispatch's misses A heartbeat that counted more slots than it itemized returned before the listed jobs had their misses reset, so an omission, a partial heartbeat listing the job, and another omission failed a job the worker was still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: look up the worker's stream before tracking the attempt A dispatch to a worker with no stream returned after the job was already recorded as assigned, and that record kept new proposals with the same dedupe key out until the stale-job sweep expired it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: fail the attempt whose request could not be sent The dispatch now sends on the stream it looked up before tracking the attempt, so a worker that reconnects in between makes the send fail. That returned with the job still tracked as assigned, and with the worker back nothing closed the record for the 24h stale timeout while its dedupe key kept new proposals out. The attempt is now completed as failed, after it is unregistered so the completion does not wait on a channel nobody reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: leave a send failure to the expiry that already claimed it The send-error path and an orphan expiry could both complete the same attempt and record two failed runs for it. The send-error path now completes the attempt only if no expiry has marked it failed, decided under the same lock that unregisters it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: record a send failure whose entry Shutdown already removed Shutdown closes shutdownCh before it unregisters pending dispatches, so a blocked send returns while its map entry is gone. Read the expiry claim from the dispatch's own state instead of the map lookup, so the attempt is still failed and does not stay assigned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: match a padded job id against its trimmed listing Heartbeat work ids are trimmed before the orphan check, so compare the dispatch's job id trimmed too; otherwise a job id with surrounding spaces never matches its own listing and the running job is failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin/plugin: skip orphan misses once a completion was handed over The buffer length only shows a completion the dispatcher has not taken yet; after it takes one, the attempt stays registered until the dispatch returns. Mark the hand-off under the pending lock and check that flag instead, so heartbeats in that gap cannot fail a finished attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
|
)
|
|
|
|
// orphanedExecutionMisses is how many itemized heartbeats in a row from a
|
|
// newer stream of the worker must leave a dispatched job out before the
|
|
// admin fails it. The second rides out a heartbeat the worker built while
|
|
// it was still picking the job back up.
|
|
const orphanedExecutionMisses = 2
|
|
|
|
// pendingExecutionState is an execution request waiting for its JobCompleted.
|
|
type pendingExecutionState struct {
|
|
completed chan *plugin_pb.JobCompleted
|
|
jobID string
|
|
jobType string
|
|
// session is the stream the request went out on.
|
|
session *streamSession
|
|
misses int
|
|
failed bool
|
|
// completionSent is set once a completion is handed to the dispatcher,
|
|
// which may already have taken it from the buffer.
|
|
completionSent bool
|
|
}
|
|
|
|
// failExecutionsMissingFromHeartbeat fails the executions sent on an earlier
|
|
// stream of the heartbeat's worker that the worker no longer lists. A worker
|
|
// that restarted comes back under the same id without the old process's
|
|
// jobs, and nothing else ends such a dispatch before its execution timeout,
|
|
// so its slot stays reserved all that time. Jobs sent on the heartbeat's own
|
|
// stream are left alone, since a worker counts a job before it lists it, and
|
|
// so is everything while the heartbeat counts slots it does not itemize,
|
|
// since a missing id then proves nothing.
|
|
func (r *Plugin) failExecutionsMissingFromHeartbeat(session *streamSession, heartbeat *plugin_pb.WorkerHeartbeat) {
|
|
if session == nil || heartbeat == nil {
|
|
return
|
|
}
|
|
itemized := 0
|
|
listed := make(map[string]struct{}, len(heartbeat.GetRunningWork()))
|
|
for _, work := range heartbeat.GetRunningWork() {
|
|
if work.GetKind() == plugin_pb.WorkKind_WORK_KIND_EXECUTION {
|
|
listed[strings.TrimSpace(work.GetWorkId())] = struct{}{}
|
|
itemized++
|
|
}
|
|
}
|
|
itemizesAll := int(heartbeat.GetExecutionSlotsUsed()) <= itemized
|
|
|
|
var orphaned []activeJobSnapshot
|
|
r.pendingExecutionMu.Lock()
|
|
for requestID, pending := range r.pendingExecution {
|
|
if pending.failed || pending.session == nil || pending.session == session || pending.session.workerID != session.workerID {
|
|
continue
|
|
}
|
|
if _, ok := listed[strings.TrimSpace(pending.jobID)]; ok {
|
|
pending.misses = 0
|
|
continue
|
|
}
|
|
if !itemizesAll {
|
|
continue
|
|
}
|
|
if pending.completionSent {
|
|
continue
|
|
}
|
|
pending.misses++
|
|
if pending.misses < orphanedExecutionMisses {
|
|
continue
|
|
}
|
|
pending.failed = true
|
|
orphaned = append(orphaned, activeJobSnapshot{
|
|
jobID: pending.jobID,
|
|
jobType: pending.jobType,
|
|
workerID: session.workerID,
|
|
requestID: requestID,
|
|
})
|
|
}
|
|
r.pendingExecutionMu.Unlock()
|
|
|
|
for _, snap := range orphaned {
|
|
reason := fmt.Sprintf("worker %s re-registered without job %s", snap.workerID, snap.jobID)
|
|
glog.Warningf("Plugin failing %s request %s: %s", snap.jobType, snap.requestID, reason)
|
|
// off the stream's receive loop: the cancel may wait on a full buffer
|
|
go r.expireActiveJob(snap, reason, time.Now().UTC())
|
|
}
|
|
}
|