mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 11:07:35 +01:00
* admin: count executor slots per job type, deduped against the heartbeat per_worker_execution_concurrency is a per-job-type setting, but the scheduler compared it against the worker's execution_slots_used, which counts every job type. One s3_lifecycle job a worker reported running after a reconnect made admin_script (per-worker 1) undispatchable on a worker with three free slots. A reservation and the heartbeat entry for the same job were also charged as two slots, so a per-worker limit of N let N-1 jobs run. Reservations now carry the job id and job type. The per-type limit is checked against that type's work alone, the worker's slot total against work of every type, and a heartbeat entry covered by a reservation is counted once. Per-lane reservation pools go away: the per-type count is what kept lanes from starving each other. * admin: stop a lane waiting on executor capacity it cannot free A queued job no executor could take was retried every backoff until job_type_max_runtime ran out, up to 30 minutes, while the default lane sat in executing and every other job type in it idled. The dispatch loop now asks what holds the capacity. Slots held by the lane's own jobs release when they complete, so it keeps waiting. Slots held by other lanes or by work this admin never dispatched clear on a schedule the lane has no view of, so the queued jobs are canceled at once with a skipped_no_executor activity naming the worker, its slot count and the job types holding it, and the next detection proposes them again. A missing worker is waited for only the orphan grace. * admin: cancel a job on its worker when the admin expires it Expiring a job, by the stale sweep or by the operator's expire endpoint, only failed the tracked record. The worker never heard about it and kept running the job with its slot taken, so a job stuck in "starting" held a slot for over a day and, with the old slot accounting, blocked a whole lane. Expiring a job now also sends the worker a cancel for it, so the slot comes back on the next heartbeat. * admin: count a completed job's heartbeat echo as its lane's own slot The worker sends JobCompleted before the heartbeat that frees the slot, so right after a dispatch goroutine releases its reservation the job still shows in the worker's running work. That entry was counted as foreign, and a lane with per-worker 1 gave up and canceled the rest of its queue after the first job. A released reservation now stays for completedJobEchoGrace and claims that heartbeat entry as the lane's own load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: let the admin's own jobs claim slots a worker does not itemize The Rust worker reports only a slot count, every 10s, and the Go worker counts a slot one heartbeat before it lists the job. Those slots were charged on top of the reservations behind them, and once a job completed its stale count read as foreign work, so the lane gave up its queue. Unitemized slots now go first to this admin's unlisted reservations and recent completions; the rest are of unknown type and count against every type's per-worker limit, not only the slot total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: give up pinned jobs whose busy home a free worker cannot relieve A job homed on a replica-host worker stays on that home while its queue is shorter than LocalQueuePin, so a free remote worker cannot take it. The outlook treated that free worker as a reason to wait, and a home held by foreign work parked the lane for its whole window. A free worker now only holds the wait for the orphan grace, after which the held homes decide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: let a redispatched job's reservation replace its released one A job the worker turns away for capacity is released, kept as an echo, and queued again under the same id. Once the new attempt ran, both the echo and the new reservation matched its heartbeat entry and it counted as two slots. Reserving a job now drops what an earlier reservation of it left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: credit a count-only slot to a completed job only until the next heartbeat A released reservation claimed an unitemized slot for its full 30s grace, so on a count-only heartbeat other work of another type was charged to the completed job's type. A heartbeat that arrives after the release already reflects the freed slot; from then on the slot is of unknown type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: name unitemized slots in the no-capacity reason holders() listed only the job types the worker itemized, so a worker with named and count-only work read as "2/2 slots used by <type>". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: keep the blocked clock running until a reservation succeeds A popped job whose reservation then failed reset the clock before the failure pushed it back, so pop-then-fail cycles never aged the block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: resend an expiry cancel the worker did not receive Expiring a job sends its worker one cancel. When the worker is disconnected, or its outgoing buffer stays full past the send timeout, that cancel is dropped. The record is already failed, so later sweeps and expire calls skip it, and a worker that kept the job running (a detached job across a reconnect, or a jammed stream) holds the slot until the job ends on its own. The admin now holds an undelivered expiry cancel and sends it again on the worker's next heartbeat that still lists the job. It is forgotten when that heartbeat no longer lists the job, and skipped when the job is active again, since a new attempt reuses the job id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: cancel an expired attempt by its request id The expiry cancel named the job id, and a retry reuses it. The worker keys its cancel funcs by both ids, so a cancel arriving after a retry started on the same worker stopped the retry. The held resend also gave up on any job that was active again, and the expired attempt's own late progress marks the job running, so a worker that buffered progress while disconnected kept the expired job. The cancel now names the attempt's request id, as the other execution cancels do, and the held resend no longer depends on the tracked state. Only a cancel that can name nothing but the job id still skips a job that is active again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: ignore late reports from an attempt the admin already ended Expiring a job now cancels it on its worker, and the worker answers with its own JobCompleted for that attempt. That overwrote the expiry's failure with the cancel error and logged a second run record under the same run id. Late progress from the expired attempt likewise reopened the record as running. A progress update or completion whose request id names an attempt the record already closed is now dropped after the dispatcher's waiter is told. A retry runs under a new request id, so its reports still apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
601 lines
19 KiB
Go
601 lines
19 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
|
)
|
|
|
|
func replicaJob(id, source string) *plugin_pb.JobSpec {
|
|
return &plugin_pb.JobSpec{
|
|
JobId: id,
|
|
JobType: "erasure_coding",
|
|
Summary: id,
|
|
DedupeKey: id,
|
|
Parameters: map[string]*plugin_pb.ConfigValue{
|
|
"source_server": {
|
|
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: source},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func registerECWorkers(t *testing.T, pluginSvc *Plugin, workers map[string]string) {
|
|
t.Helper()
|
|
for id, addr := range workers {
|
|
pluginSvc.registry.UpsertFromHello(&plugin_pb.WorkerHello{
|
|
WorkerId: id,
|
|
Address: addr,
|
|
Capabilities: []*plugin_pb.JobTypeCapability{
|
|
{JobType: "erasure_coding", CanExecute: true, MaxExecutionConcurrency: 8},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssignJobsWaterfillsPreferredWorkers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-a": "10.0.0.1:9334",
|
|
"worker-b": "10.0.0.2:9334",
|
|
"worker-c": "10.0.0.3:9334",
|
|
"worker-d": "10.0.0.4:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("j1", "10.0.0.1:8080"),
|
|
replicaJob("j2", "10.0.0.1:8080"),
|
|
replicaJob("j3", "10.0.0.1:8080"),
|
|
replicaJob("j4", "10.0.0.1:8080"),
|
|
}
|
|
// All four replica IPs listed so every worker is preferred.
|
|
for _, job := range jobs {
|
|
job.Parameters["replica_servers"] = &plugin_pb.ConfigValue{
|
|
Kind: &plugin_pb.ConfigValue_StringList{StringList: &plugin_pb.StringList{
|
|
Values: []string{"10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080", "10.0.0.4:8080"},
|
|
}},
|
|
}
|
|
}
|
|
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, schedulerPolicy{LocalQueuePin: 5})
|
|
if qs.queueLen("worker-a")+qs.queueLen("worker-b")+qs.queueLen("worker-c")+qs.queueLen("worker-d") != 4 {
|
|
t.Fatalf("expected 4 assigned jobs")
|
|
}
|
|
for _, id := range []string{"worker-a", "worker-b", "worker-c", "worker-d"} {
|
|
if qs.queueLen(id) != 1 {
|
|
t.Fatalf("want 1 job on %s, got %d", id, qs.queueLen(id))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssignAndTakeDoesNotStarveOtherHomes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("local-1", "10.0.0.1:8080"),
|
|
replicaJob("local-2", "10.0.0.1:8080"),
|
|
replicaJob("remote-1", "10.0.0.2:8080"),
|
|
}
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 5}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 2 || qs.queueLen("worker-remote") != 1 {
|
|
t.Fatalf("homes local=%d remote=%d", qs.queueLen("worker-local"), qs.queueLen("worker-remote"))
|
|
}
|
|
|
|
_, target1 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
_, release1, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, target1, "")
|
|
if err != nil {
|
|
t.Fatalf("reserve 1: %v", err)
|
|
}
|
|
defer release1()
|
|
|
|
item2, target2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item2 == nil || target2 != "worker-remote" {
|
|
t.Fatalf("second take should start remote home, got job=%v target=%s", item2, target2)
|
|
}
|
|
}
|
|
|
|
func TestTakeStealsOnlyWhenHomeQueueReachesK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := make([]*plugin_pb.JobSpec, 0, 6)
|
|
for i := 0; i < 6; i++ {
|
|
jobs = append(jobs, replicaJob(fmt.Sprintf("j%d", i), "10.0.0.1:8080"))
|
|
}
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 5}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 6 {
|
|
t.Fatalf("all jobs should home on local, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
_, target := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if target != "worker-local" {
|
|
t.Fatalf("first take want local, got %s", target)
|
|
}
|
|
_, release, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, "worker-local", "")
|
|
if err != nil {
|
|
t.Fatalf("reserve local: %v", err)
|
|
}
|
|
defer release()
|
|
|
|
// 5 still queued on local (>=K) and local has no slot → steal to remote.
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-remote" {
|
|
t.Fatalf("expected steal to remote, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
if qs.queueLen("worker-local") != 4 {
|
|
t.Fatalf("after steal want 4 left on local, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
// 4 < K: do not steal again.
|
|
if item, tgt := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy); item != nil {
|
|
t.Fatalf("pin should hold remaining jobs, got target=%s", tgt)
|
|
}
|
|
}
|
|
|
|
func TestTakeStealsImmediatelyWhenPinDisabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("a", "10.0.0.1:8080"),
|
|
replicaJob("b", "10.0.0.1:8080"),
|
|
}
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 0}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
|
|
_, target := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if target != "worker-local" {
|
|
t.Fatalf("first take want local, got %s", target)
|
|
}
|
|
_, release, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, "worker-local", "")
|
|
if err != nil {
|
|
t.Fatalf("reserve local: %v", err)
|
|
}
|
|
defer release()
|
|
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-remote" {
|
|
t.Fatalf("K=0 should steal immediately, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
}
|
|
|
|
func TestTakeStealsOrphanedHomeWithoutK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("o1", "10.0.0.1:8080"),
|
|
replicaJob("o2", "10.0.0.1:8080"),
|
|
}
|
|
policy := schedulerPolicy{
|
|
PerWorkerConcurrency: 1,
|
|
LocalQueuePin: 5,
|
|
OrphanRehomeGrace: 30 * time.Second,
|
|
}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 2 {
|
|
t.Fatalf("want 2 on local, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
pluginSvc.registry.Remove("worker-local")
|
|
|
|
if item, tgt := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy); item != nil {
|
|
t.Fatalf("within grace should hold orphan queue, got target=%s", tgt)
|
|
}
|
|
if qs.queueLen("worker-local") != 2 {
|
|
t.Fatalf("held queue changed: %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
qs.mu.Lock()
|
|
qs.missingSince["worker-local"] = time.Now().Add(-time.Minute)
|
|
qs.mu.Unlock()
|
|
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-remote" {
|
|
t.Fatalf("after grace should steal without K, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
if qs.queueLen("worker-local") != 1 {
|
|
t.Fatalf("want 1 orphan left, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
stolen2, stealTarget2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen2 == nil || stealTarget2 != "worker-remote" {
|
|
t.Fatalf("second orphan should also move, got job=%v target=%s", stolen2, stealTarget2)
|
|
}
|
|
}
|
|
|
|
func TestTakeKeepsOrphanWhenHomeReconnectsDuringGrace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{replicaJob("blink", "10.0.0.1:8080")}
|
|
policy := schedulerPolicy{
|
|
PerWorkerConcurrency: 1,
|
|
LocalQueuePin: 5,
|
|
OrphanRehomeGrace: 30 * time.Second,
|
|
}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
pluginSvc.registry.Remove("worker-local")
|
|
if item, _ := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy); item != nil {
|
|
t.Fatal("expected hold during grace")
|
|
}
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{"worker-local": "10.0.0.1:9334"})
|
|
item, target := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item == nil || target != "worker-local" {
|
|
t.Fatalf("reconnected home should run its queue, got job=%v target=%s", item, target)
|
|
}
|
|
}
|
|
|
|
func TestTakeStealsOrphanImmediatelyWhenGraceDisabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{replicaJob("o1", "10.0.0.1:8080")}
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 5, OrphanRehomeGrace: 0}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
pluginSvc.registry.Remove("worker-local")
|
|
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-remote" {
|
|
t.Fatalf("grace=0 should steal immediately, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
}
|
|
|
|
func TestBindQueuedJobWorkerKeepsPendingState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
job := replicaJob("bind-1", "10.0.0.1:8080")
|
|
pluginSvc.trackExecutionQueued(job)
|
|
pluginSvc.bindQueuedJobWorker(job, "worker-local")
|
|
|
|
pluginSvc.jobsMu.Lock()
|
|
tracked := pluginSvc.jobs[job.JobId]
|
|
pluginSvc.jobsMu.Unlock()
|
|
if tracked == nil {
|
|
t.Fatal("missing tracked job")
|
|
}
|
|
if tracked.State != "job_state_pending" {
|
|
t.Fatalf("state=%s", tracked.State)
|
|
}
|
|
if tracked.Stage != "queued" {
|
|
t.Fatalf("stage=%s", tracked.Stage)
|
|
}
|
|
if tracked.WorkerID != "worker-local" {
|
|
t.Fatalf("worker=%s", tracked.WorkerID)
|
|
}
|
|
}
|
|
|
|
func TestHomelessAllRemoteWorkersUseFIFOWithoutPinOrGrace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
// Workers never registered on the replica hostname — homeless, not orphan.
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-a": "10.0.0.5:9334",
|
|
"worker-b": "10.0.0.6:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("h1", "10.0.0.1:8080"),
|
|
replicaJob("h2", "10.0.0.1:8080"),
|
|
replicaJob("h3", "10.0.0.1:8080"),
|
|
}
|
|
policy := schedulerPolicy{
|
|
PerWorkerConcurrency: 1,
|
|
LocalQueuePin: 5,
|
|
OrphanRehomeGrace: 30 * time.Second,
|
|
}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-a") != 0 || qs.queueLen("worker-b") != 0 {
|
|
t.Fatalf("homeless jobs must not get a fake home, a=%d b=%d", qs.queueLen("worker-a"), qs.queueLen("worker-b"))
|
|
}
|
|
if qs.leftoverLen() != 3 {
|
|
t.Fatalf("want 3 homeless FIFO, got %d", qs.leftoverLen())
|
|
}
|
|
|
|
item1, target1 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item1 == nil || item1.job.JobId != "h1" {
|
|
t.Fatalf("FIFO first want h1, got %v", item1)
|
|
}
|
|
_, release1, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, target1, "")
|
|
if err != nil {
|
|
t.Fatalf("reserve 1: %v", err)
|
|
}
|
|
defer release1()
|
|
|
|
item2, target2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item2 == nil || item2.job.JobId != "h2" {
|
|
t.Fatalf("FIFO second want h2 immediately, got %v", item2)
|
|
}
|
|
if target2 == "" || target2 == target1 {
|
|
t.Fatalf("second take should hit the other idle worker, t1=%s t2=%s", target1, target2)
|
|
}
|
|
if qs.leftoverLen() != 1 {
|
|
t.Fatalf("want 1 homeless left, got %d", qs.leftoverLen())
|
|
}
|
|
}
|
|
|
|
func TestMixedReplicaLocalJobsStayHomeBeforeHomeless(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("local-1", "10.0.0.1:8080"),
|
|
replicaJob("local-2", "10.0.0.1:8080"),
|
|
replicaJob("homeless-1", "10.0.0.9:8080"),
|
|
replicaJob("homeless-2", "10.0.0.9:8080"),
|
|
}
|
|
policy := schedulerPolicy{
|
|
PerWorkerConcurrency: 1,
|
|
LocalQueuePin: 5,
|
|
OrphanRehomeGrace: 30 * time.Second,
|
|
}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 2 || qs.queueLen("worker-remote") != 0 || qs.leftoverLen() != 2 {
|
|
t.Fatalf("homes local=%d remote=%d leftover=%d", qs.queueLen("worker-local"), qs.queueLen("worker-remote"), qs.leftoverLen())
|
|
}
|
|
|
|
item1, target1 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item1 == nil || item1.job.JobId != "local-1" || target1 != "worker-local" {
|
|
t.Fatalf("first take want local-1 on home, got job=%v target=%s", item1, target1)
|
|
}
|
|
_, release, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, "worker-local", "")
|
|
if err != nil {
|
|
t.Fatalf("reserve local: %v", err)
|
|
}
|
|
defer release()
|
|
|
|
item2, target2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item2 == nil || item2.job.JobId != "homeless-1" || target2 != "worker-remote" {
|
|
t.Fatalf("remote should take homeless, not steal pinned home, got job=%v target=%s", item2, target2)
|
|
}
|
|
if qs.queueLen("worker-local") != 1 {
|
|
t.Fatalf("remaining replica-local job must stay home, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
if qs.leftoverLen() != 1 {
|
|
t.Fatalf("one homeless should remain, leftover=%d", qs.leftoverLen())
|
|
}
|
|
}
|
|
|
|
func TestIdleHomeWorkerTakesHomelessAfterEmptyHomeQueue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
})
|
|
|
|
jobs := []*plugin_pb.JobSpec{
|
|
replicaJob("home-1", "10.0.0.1:8080"),
|
|
replicaJob("homeless-1", "10.0.0.9:8080"),
|
|
}
|
|
policy := schedulerPolicy{
|
|
PerWorkerConcurrency: 1,
|
|
LocalQueuePin: 5,
|
|
OrphanRehomeGrace: 30 * time.Second,
|
|
}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 1 || qs.leftoverLen() != 1 {
|
|
t.Fatalf("home=%d leftover=%d", qs.queueLen("worker-local"), qs.leftoverLen())
|
|
}
|
|
|
|
item1, target1 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item1 == nil || item1.job.JobId != "home-1" || target1 != "worker-local" {
|
|
t.Fatalf("first take want home-1, got job=%v target=%s", item1, target1)
|
|
}
|
|
item2, target2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item2 == nil || item2.job.JobId != "homeless-1" || target2 != "worker-local" {
|
|
t.Fatalf("idle home with empty queue may take homeless, got job=%v target=%s", item2, target2)
|
|
}
|
|
if qs.leftoverLen() != 0 {
|
|
t.Fatalf("homeless should be gone, leftover=%d", qs.leftoverLen())
|
|
}
|
|
}
|
|
|
|
func TestHomelessWorkerTakesFIFOBeforeStealableHome(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-remote": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := make([]*plugin_pb.JobSpec, 0, 7)
|
|
for i := 0; i < 6; i++ {
|
|
jobs = append(jobs, replicaJob(fmt.Sprintf("j%d", i), "10.0.0.1:8080"))
|
|
}
|
|
jobs = append(jobs, replicaJob("homeless-1", "10.0.0.9:8080"))
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 5}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 6 || qs.leftoverLen() != 1 {
|
|
t.Fatalf("home=%d leftover=%d", qs.queueLen("worker-local"), qs.leftoverLen())
|
|
}
|
|
if qs.hasHome("worker-remote") {
|
|
t.Fatal("remote must have no home identity")
|
|
}
|
|
|
|
_, target := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if target != "worker-local" {
|
|
t.Fatalf("first take want local, got %s", target)
|
|
}
|
|
_, release, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, "worker-local", "")
|
|
if err != nil {
|
|
t.Fatalf("reserve local: %v", err)
|
|
}
|
|
defer release()
|
|
|
|
item, takeTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item == nil || takeTarget != "worker-remote" || item.job.JobId != "homeless-1" {
|
|
t.Fatalf("remote must take homeless FIFO before K-steal, got job=%v target=%s", item, takeTarget)
|
|
}
|
|
if qs.leftoverLen() != 0 {
|
|
t.Fatalf("homeless should be gone, leftover=%d", qs.leftoverLen())
|
|
}
|
|
if qs.queueLen("worker-local") != 5 {
|
|
t.Fatalf("stealable home must stay until FIFO empty, got %d", qs.queueLen("worker-local"))
|
|
}
|
|
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-remote" || stolen.job.JobId == "homeless-1" {
|
|
t.Fatalf("after FIFO empty remote may steal >=K, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
}
|
|
|
|
func TestHomeWorkerStealsBeforeHomelessAfterOwnQueueEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pluginSvc, err := New(Options{})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
defer pluginSvc.Shutdown()
|
|
|
|
registerECWorkers(t, pluginSvc, map[string]string{
|
|
"worker-local": "10.0.0.1:9334",
|
|
"worker-other": "10.0.0.2:9334",
|
|
})
|
|
|
|
jobs := make([]*plugin_pb.JobSpec, 0, 8)
|
|
for i := 0; i < 6; i++ {
|
|
jobs = append(jobs, replicaJob(fmt.Sprintf("j%d", i), "10.0.0.1:8080"))
|
|
}
|
|
jobs = append(jobs, replicaJob("other-1", "10.0.0.2:8080"))
|
|
jobs = append(jobs, replicaJob("homeless-1", "10.0.0.9:8080"))
|
|
policy := schedulerPolicy{PerWorkerConcurrency: 1, LocalQueuePin: 5}
|
|
qs := pluginSvc.assignJobsToWorkerQueues("erasure_coding", jobs, policy)
|
|
if qs.queueLen("worker-local") != 6 || qs.queueLen("worker-other") != 1 || qs.leftoverLen() != 1 {
|
|
t.Fatalf("local=%d other=%d leftover=%d", qs.queueLen("worker-local"), qs.queueLen("worker-other"), qs.leftoverLen())
|
|
}
|
|
if !qs.hasHome("worker-other") {
|
|
t.Fatal("other must keep home identity")
|
|
}
|
|
|
|
item1, target1 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item1 == nil || target1 != "worker-local" {
|
|
t.Fatalf("first take want local home, got job=%v target=%s", item1, target1)
|
|
}
|
|
_, release, err := pluginSvc.reserveExecutorByID("erasure_coding", policy, "worker-local", "")
|
|
if err != nil {
|
|
t.Fatalf("reserve local: %v", err)
|
|
}
|
|
defer release()
|
|
|
|
item2, target2 := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if item2 == nil || item2.job.JobId != "other-1" || target2 != "worker-other" {
|
|
t.Fatalf("other should drain own home first, got job=%v target=%s", item2, target2)
|
|
}
|
|
|
|
stolen, stealTarget := pluginSvc.takeQueuedJob(qs, "erasure_coding", policy)
|
|
if stolen == nil || stealTarget != "worker-other" || stolen.job.JobId == "homeless-1" {
|
|
t.Fatalf("home worker with empty own queue still steals before homeless, got job=%v target=%s", stolen, stealTarget)
|
|
}
|
|
if qs.leftoverLen() != 1 {
|
|
t.Fatalf("homeless should still wait, leftover=%d", qs.leftoverLen())
|
|
}
|
|
}
|