mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-27 02:23:23 +00:00
* admin: let waiting shell clients win the admin lock The admin lock manager re-acquired the cluster shell lock immediately after releasing it, while a waiting weed shell client only polls the master once a second, so an operator could lose the race indefinitely. Leave the lock free for slightly more than one poll interval before re-acquiring, and once overlapping reference-counted holds have kept the lock continuously held for over a minute, make new admin-side acquires wait for a full release before piggybacking. * admin/plugin: take the shell lock per detection and per job, not per batch The default-lane scheduler acquired the cluster shell lock once and held it across the whole pass: every due job type's detection plus all of its dispatched jobs, each drained to completion. A manual weed shell operation sharing that lock could stall behind the batch for up to the extended execution window. Hold the lock only while it protects something: around each detection scan and around each dispatched job. Manual shell operations now wait for at most one in-flight job. The admin UI detect+execute path stops wrapping dispatch in an outer hold, since a nested acquire would deadlock against the lock manager's fairness window; detection and per-job dispatch take the lock themselves. * admin/plugin: stop extending the dispatch window to the largest job estimate When any proposal's estimated runtime exceeded the remaining JobTypeMaxRuntime, the whole dispatch context was replaced with a fresh one capped at eight hours, so a single balance backlog could hold the default lane (and with it erasure_coding and vacuum detection) for that long. Keep the dispatch window at JobTypeMaxRuntime and instead detach each started attempt onto its own estimated-runtime deadline. Large jobs still get their full time once started; jobs not yet started when the window closes are canceled and re-proposed by a later detection, so sibling job types get a turn every window. * worker/balance: re-check each planned move against the master before executing A balance plan is computed at detection time, but the admin lock is released between detection and execution, so a manual shell operation can rearrange the volume in the gap. The task's own guards catch a vanished source, but a target that gained a replica in the meantime would be silently overwritten by VolumeCopy and the source delete would then reduce the volume to a single copy. Before executing each move, ask the master for the volume's current locations (uncached) and skip the move if the volume has left the source or the target already holds a replica. Skipped moves fail with a stale-move error and the next detection replans them. Without master addresses in the cluster context the check is skipped, preserving the old behavior with older admins. * admin: block re-acquire while the final lock release is in flight Release dropped the manager mutex before calling ReleaseLock, so a concurrent Acquire could see hold count zero and call RequestLock while the locker still considered itself locked. That request no-ops, leaving a hold with no live master lease. Track the in-flight release and make Acquire wait for it. Also normalize a nil release function from lock manager implementations, and make the yield and fairness windows per-instance fields so tests stop mutating globals.
131 lines
4.0 KiB
Go
131 lines
4.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SchedulerLane identifies an independent scheduling track. Each lane runs
|
|
// its own goroutine and maintains its own detection timing so that
|
|
// workloads in different lanes never block each other.
|
|
type SchedulerLane string
|
|
|
|
const (
|
|
// LaneDefault handles volume management operations (vacuum, balance,
|
|
// erasure coding) and admin scripts. It is the fallback lane for any
|
|
// job type that is not explicitly mapped elsewhere.
|
|
LaneDefault SchedulerLane = "default"
|
|
|
|
// LaneIceberg handles table-bucket Iceberg compaction and maintenance.
|
|
LaneIceberg SchedulerLane = "iceberg"
|
|
|
|
// LaneLifecycle handles S3 object store lifecycle management
|
|
// (expiration, transition, abort incomplete multipart uploads).
|
|
LaneLifecycle SchedulerLane = "lifecycle"
|
|
)
|
|
|
|
// AllLanes returns every defined scheduler lane in a stable order.
|
|
func AllLanes() []SchedulerLane {
|
|
return []SchedulerLane{LaneDefault, LaneIceberg, LaneLifecycle}
|
|
}
|
|
|
|
// laneIdleSleep maps each lane to its default idle sleep duration.
|
|
// Each lane can sleep for a different amount when no work is detected,
|
|
// independent of the per-job-type DetectionInterval.
|
|
var laneIdleSleep = map[SchedulerLane]time.Duration{
|
|
LaneDefault: 61 * time.Second,
|
|
LaneIceberg: 61 * time.Second,
|
|
LaneLifecycle: 5 * time.Minute,
|
|
}
|
|
|
|
// laneRequiresLock maps each lane to whether its job types must be
|
|
// serialised and run under the shared cluster admin lock. The default
|
|
// lane needs this because volume-management operations share global
|
|
// state; the lock is taken around each detection and each job so manual
|
|
// shell operations can interleave. Other lanes run each job type
|
|
// independently without the lock.
|
|
var laneRequiresLock = map[SchedulerLane]bool{
|
|
LaneDefault: true,
|
|
LaneIceberg: false,
|
|
LaneLifecycle: false,
|
|
}
|
|
|
|
// LaneRequiresLock returns true if the given lane serialises its job types
|
|
// and runs them under the cluster admin lock. Unknown lanes default to true.
|
|
func LaneRequiresLock(lane SchedulerLane) bool {
|
|
if v, ok := laneRequiresLock[lane]; ok {
|
|
return v
|
|
}
|
|
return true
|
|
}
|
|
|
|
// LaneIdleSleep returns the idle sleep duration for the given lane,
|
|
// falling back to defaultSchedulerIdleSleep if the lane is unknown.
|
|
func LaneIdleSleep(lane SchedulerLane) time.Duration {
|
|
if d, ok := laneIdleSleep[lane]; ok {
|
|
return d
|
|
}
|
|
return defaultSchedulerIdleSleep
|
|
}
|
|
|
|
// jobTypeLaneMap is the hardcoded mapping from job type to scheduler lane.
|
|
// Job types not present here are assigned to LaneDefault.
|
|
var jobTypeLaneMap = map[string]SchedulerLane{
|
|
// Volume management (default lane)
|
|
"vacuum": LaneDefault,
|
|
"volume_balance": LaneDefault,
|
|
"ec_balance": LaneDefault,
|
|
"erasure_coding": LaneDefault,
|
|
"admin_script": LaneDefault,
|
|
|
|
// Iceberg table maintenance
|
|
"iceberg_maintenance": LaneIceberg,
|
|
|
|
// S3 lifecycle management
|
|
"s3_lifecycle": LaneLifecycle,
|
|
}
|
|
|
|
// JobTypeLane returns the scheduler lane for the given job type.
|
|
// Unknown job types are assigned to LaneDefault.
|
|
func JobTypeLane(jobType string) SchedulerLane {
|
|
if lane, ok := jobTypeLaneMap[jobType]; ok {
|
|
return lane
|
|
}
|
|
return LaneDefault
|
|
}
|
|
|
|
// LaneJobTypes returns the set of known job types assigned to the given lane.
|
|
func LaneJobTypes(lane SchedulerLane) []string {
|
|
var result []string
|
|
for jobType, l := range jobTypeLaneMap {
|
|
if l == lane {
|
|
result = append(result, jobType)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// schedulerLaneState holds the per-lane runtime state used by the scheduler.
|
|
type schedulerLaneState struct {
|
|
lane SchedulerLane
|
|
wakeCh chan struct{}
|
|
|
|
loopMu sync.Mutex
|
|
loop schedulerLoopState
|
|
|
|
// Per-lane execution reservation pool. Each lane tracks how many
|
|
// execution slots it has reserved on each worker independently,
|
|
// so lanes cannot starve each other.
|
|
execMu sync.Mutex
|
|
execRes map[string]int
|
|
}
|
|
|
|
// newLaneState creates a schedulerLaneState for the given lane.
|
|
func newLaneState(lane SchedulerLane) *schedulerLaneState {
|
|
return &schedulerLaneState{
|
|
lane: lane,
|
|
wakeCh: make(chan struct{}, 1),
|
|
execRes: make(map[string]int),
|
|
}
|
|
}
|