// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package state import ( "context" "fmt" "testing" "time" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/hashicorp/go-memdb" // "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant/internal/server/proto/vagrant_server" ) func TestJobAssign(t *testing.T) { t.Run("basic assignment with one", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // We should not have an output buffer yet require.Nil(job.OutputBuffer) // Should block if requesting another since none exist ctx, cancel := context.WithCancel(context.Background()) cancel() job, err = s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) require.Error(err) require.Nil(job) require.Equal(ctx.Err(), err) }) t.Run("blocking on any", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build { job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) } // Get the next value in a goroutine { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var job *Job var jerr error doneCh := make(chan struct{}) go func() { defer close(doneCh) job, jerr = s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) }() // We should be blocking select { case <-doneCh: t.Fatal("should wait") case <-time.After(500 * time.Millisecond): } // Insert another job require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "B", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // We should get a result select { case <-doneCh: case <-time.After(500 * time.Millisecond): t.Fatal("should have a result") } require.NoError(jerr) require.NotNil(job) require.Equal("B", job.Id) } }) // t.Run("blocking on matching basis and project", func(t *testing.T) { // require := require.New(t) // s := TestState(t) // defer s.Close() // // Create two builds for the same project // require.NoError(s.JobCreate(serverptypes.TestJobNew(t, &vagrant_server.Job{ // Id: "A", // Project: &vagrant_plugin_sdk.Ref_Project{ // ResourceId: "project1", // }, // Operation: &vagrant_server.Job_Run{ // Run: &vagrant_server.Job_RunOp{}, // }, // }))) // require.NoError(s.JobCreate(serverptypes.TestJobNew(t, &vagrant_server.Job{ // Id: "B", // Project: &vagrant_plugin_sdk.Ref_Project{ // ResourceId: "project1", // }, // Operation: &vagrant_server.Job_Run{ // Run: &vagrant_server.Job_RunOp{}, // }, // }))) // // Assign it, we should get this build // { // job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) // require.NoError(err) // require.NotNil(job) // require.Equal("A", job.Id) // } // // Get the next value in a goroutine // { // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // var job *Job // var jerr error // doneCh := make(chan struct{}) // go func() { // defer close(doneCh) // job, jerr = s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) // }() // // We should be blocking // select { // case <-doneCh: // t.Fatal("should wait") // case <-time.After(500 * time.Millisecond): // } // // Insert another job for a different workspace // require.NoError(s.JobCreate(serverptypes.TestJobNew(t, &vagrant_server.Job{ // Id: "C", // Project: &vagrant_plugin_sdk.Ref_Project{ // ResourceId: "project2", // }, // Operation: &vagrant_server.Job_Run{ // Run: &vagrant_server.Job_RunOp{}, // }, // }))) // // We should get a result // select { // case <-doneCh: // case <-time.After(500 * time.Millisecond): // t.Fatal("should have a result") // } // require.NoError(jerr) // require.NotNil(job) // require.Equal("C", job.Id) // } // }) // t.Run("blocking on matching basis and project (sequential)", func(t *testing.T) { // require := require.New(t) // s := TestState(t) // defer s.Close() // // Create two builds for the same app/workspace // require.NoError(s.JobCreate(serverptypes.TestJobNew(t, &vagrant_server.Job{ // Id: "A", // Project: &vagrant_plugin_sdk.Ref_Project{ // ResourceId: "project1", // }, // Operation: &vagrant_server.Job_Run{ // Run: &vagrant_server.Job_RunOp{}, // }, // }))) // require.NoError(s.JobCreate(serverptypes.TestJobNew(t, &vagrant_server.Job{ // Id: "B", // Project: &vagrant_plugin_sdk.Ref_Project{ // ResourceId: "project1", // }, // Operation: &vagrant_server.Job_Run{ // Run: &vagrant_server.Job_RunOp{}, // }, // }))) // // Assign it, we should get this build // job1, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) // require.NoError(err) // require.NotNil(job1) // require.Equal("A", job1.Id) // // Get the next value in a goroutine // { // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // var job *Job // var jerr error // doneCh := make(chan struct{}) // go func() { // defer close(doneCh) // job, jerr = s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) // }() // // We should be blocking // select { // case <-doneCh: // t.Fatal("should wait") // case <-time.After(500 * time.Millisecond): // } // // Complete the job // _, err = s.JobAck(job1.Id, true) // require.NoError(err) // require.NoError(s.JobComplete(job1.Id, nil, nil)) // // We should get a result // select { // case <-doneCh: // case <-time.After(500 * time.Millisecond): // t.Fatal("should have a result") // } // require.NoError(jerr) // require.NotNil(job) // require.Equal("B", job.Id) // } // }) t.Run("basic assignment with two", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create two builds slightly apart require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) time.Sleep(1 * time.Millisecond) require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "B", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get build A then B { job, err := s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) _, err = s.JobAck(job.Id, true) require.NoError(err) require.NoError(s.JobComplete(job.Id, nil, nil)) } { job, err := s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("B", job.Id) _, err = s.JobAck(job.Id, true) require.NoError(err) require.NoError(s.JobComplete(job.Id, nil, nil)) } }) t.Run("assignment by ID", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_B"}) // Create a build by ID require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: "R_A", }, }, }, }))) time.Sleep(1 * time.Millisecond) require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "B", }))) time.Sleep(1 * time.Millisecond) require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "C", }))) // Assign for R_B, which should get B since it won't match the earlier // assignment target. { job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_B"}) require.NoError(err) require.NotNil(job) require.Equal("B", job.Id) _, err = s.JobAck(job.Id, true) require.NoError(err) require.NoError(s.JobComplete(job.Id, nil, nil)) } // Assign for R_A, which should get A since it matches the target. { job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) _, err = s.JobAck(job.Id, true) require.NoError(err) require.NoError(s.JobComplete(job.Id, nil, nil)) } }) t.Run("assignment by ID no candidates", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_B"}) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build by ID require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: "R_B", }, }, }, }))) // Assign for R_A which should get nothing cause it doesn't match. // NOTE that using "R_A" here is very important. This fixes a bug // where our lower bound was picking up invalid IDs. { ctx, cancel := context.WithCancel(context.Background()) defer cancel() doneCh := make(chan struct{}) go func() { defer close(doneCh) s.JobAssignForRunner(ctx, &vagrant_server.Runner{Id: "R_A"}) }() // We should be blocking select { case <-doneCh: t.Fatal("should wait") case <-time.After(500 * time.Millisecond): } } }) t.Run("any cannot be assigned to ByIdOnly runner", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) r := &vagrant_server.Runner{Id: "R_A", ByIdOnly: true} testRunnerProto(t, s, r) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Should block because none direct assign ctx, cancel := context.WithCancel(context.Background()) cancel() job, err := s.JobAssignForRunner(ctx, r) require.Error(err) require.Nil(job) require.Equal(ctx.Err(), err) // Create a target require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "B", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: "R_A", }, }, }, }))) // Assign it, we should get this build job, err = s.JobAssignForRunner(context.Background(), r) require.NoError(err) require.NotNil(job) require.Equal("B", job.Id) }) } func TestJobAck(t *testing.T) { t.Run("ack", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Verify it is changed job, err = s.JobById(job.Id, nil) require.NoError(err) require.Equal(vagrant_server.Job_RUNNING, job.Job.State) // We should have an output buffer require.NotNil(job.OutputBuffer) }) t.Run("ack negative", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) // Ack it _, err = s.JobAck(job.Id, false) require.NoError(err) // Verify it is changed job, err = s.JobById(job.Id, nil) require.NoError(err) require.Equal(vagrant_server.Job_QUEUED, job.State) // We should not have an output buffer require.Nil(job.OutputBuffer) }) t.Run("timeout before ack should requeue", func(t *testing.T) { require := require.New(t) // Set a short timeout old := jobWaitingTimeout defer func() { jobWaitingTimeout = old }() jobWaitingTimeout = 5 * time.Millisecond s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) // Sleep too long time.Sleep(100 * time.Millisecond) // Verify it is queued job, err = s.JobById(job.Id, nil) require.NoError(err) require.Equal(vagrant_server.Job_QUEUED, job.Job.State) // Ack it _, err = s.JobAck(job.Id, true) require.Error(err) }) } func TestJobComplete(t *testing.T) { t.Run("success", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Complete it require.NoError(s.JobComplete(job.Id, &vagrant_server.Job_Result{ Run: &vagrant_server.Job_CommandResult{}, }, nil)) // Verify it is changed job, err = s.JobById(job.Id, nil) require.NoError(err) require.Equal(vagrant_server.Job_SUCCESS, job.State) require.Nil(job.Error) require.NotNil(job.Result) require.NotNil(job.Result.Run) }) t.Run("error", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Complete it require.NoError(s.JobComplete(job.Id, nil, fmt.Errorf("bad"))) // Verify it is changed job, err = s.JobById(job.Id, nil) require.NoError(err) require.Equal(vagrant_server.Job_ERROR, job.State) require.NotNil(job.Error) st := status.FromProto(job.Error) require.Equal(codes.Unknown, st.Code()) require.Contains(st.Message(), "bad") }) } func TestJobIsAssignable(t *testing.T) { t.Run("no runners", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) // Create a build result, err := s.JobIsAssignable(ctx, TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, })) require.NoError(err) require.False(result) }) t.Run("any target, runners exist", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Should be assignable result, err := s.JobIsAssignable(ctx, TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Any{ Any: &vagrant_server.Ref_RunnerAny{}, }, }, })) require.NoError(err) require.True(result) }) t.Run("any target, runners ByIdOnly", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A", ByIdOnly: true}) // Should be assignable result, err := s.JobIsAssignable(ctx, TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Any{ Any: &vagrant_server.Ref_RunnerAny{}, }, }, })) require.NoError(err) require.False(result) }) t.Run("ID target, no match", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_B"}) // Should be assignable result, err := s.JobIsAssignable(ctx, TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: "R_A", }, }, }, })) require.NoError(err) require.False(result) }) t.Run("ID target, match", func(t *testing.T) { require := require.New(t) ctx := context.Background() s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Should be assignable result, err := s.JobIsAssignable(ctx, TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, TargetRunner: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: "R_A", }, }, }, })) require.NoError(err) require.True(result) }) } func TestJobCancel(t *testing.T) { t.Run("queued", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Cancel it require.NoError(s.JobCancel("A", false)) // Verify it is canceled job, err := s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_ERROR, job.Job.State) require.NotNil(job.Job.Error) require.NotEmpty(job.CancelTime) }) t.Run("assigned", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Cancel it require.NoError(s.JobCancel("A", false)) // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_WAITING, job.Job.State) require.NotEmpty(job.CancelTime) }) t.Run("assigned with force", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Cancel it require.NoError(s.JobCancel("A", true)) // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_ERROR, job.Job.State) require.NotEmpty(job.CancelTime) }) t.Run("assigned with force clears assignedSet", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, Operation: &vagrant_server.Job_Command{}, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Cancel it require.NoError(s.JobCancel("A", true)) // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_ERROR, job.Job.State) require.NotEmpty(job.CancelTime) // Create a another job require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "B", Scope: &vagrant_server.Job_Project{ Project: projRef, }, Operation: &vagrant_server.Job_Command{}, }))) ws := memdb.NewWatchSet() // Read it back to check the blocked status job2, err := s.JobById("B", ws) require.NoError(err) require.NotNil(job2) require.Equal("B", job2.Id) require.Equal(vagrant_server.Job_QUEUED, job2.State) require.False(job2.Blocked) }) t.Run("completed", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Complete it require.NoError(s.JobComplete(job.Id, nil, nil)) // Cancel it require.NoError(s.JobCancel("A", false)) // Verify it is not canceled job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_SUCCESS, job.Job.State) require.Empty(job.CancelTime) }) } func TestJobHeartbeat(t *testing.T) { t.Run("times out after ack", func(t *testing.T) { require := require.New(t) s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Set a short timeout old := jobHeartbeatTimeout defer func() { jobHeartbeatTimeout = old }() jobHeartbeatTimeout = 5 * time.Millisecond // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) time.Sleep(1 * time.Second) // Should time out require.Eventually(func() bool { // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) return job.Job.State == vagrant_server.Job_ERROR }, 1*time.Second, 10*time.Millisecond) }) t.Run("doesn't time out if heartbeating", func(t *testing.T) { require := require.New(t) // Set a short timeout old := jobHeartbeatTimeout defer func() { jobHeartbeatTimeout = old }() jobHeartbeatTimeout = 250 * time.Millisecond s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Start heartbeating ctx, cancel := context.WithCancel(context.Background()) doneCh := make(chan struct{}) defer func() { cancel() <-doneCh }() go func() { defer close(doneCh) tick := time.NewTicker(20 * time.Millisecond) defer tick.Stop() for { select { case <-tick.C: s.JobHeartbeat(job.Id) case <-ctx.Done(): return } } }() // Sleep for a bit time.Sleep(1 * time.Second) // Verify it is running job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_RUNNING, job.Job.State) // Stop it require.NoError(s.JobComplete(job.Id, nil, nil)) }) t.Run("times out if heartbeating stops", func(t *testing.T) { require := require.New(t) // Set a short timeout old := jobHeartbeatTimeout defer func() { jobHeartbeatTimeout = old }() jobHeartbeatTimeout = 250 * time.Millisecond s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Start heartbeating ctx, cancel := context.WithCancel(context.Background()) doneCh := make(chan struct{}) defer func() { cancel() <-doneCh }() go func() { defer close(doneCh) tick := time.NewTicker(20 * time.Millisecond) defer tick.Stop() for { select { case <-tick.C: s.JobHeartbeat(job.Id) case <-ctx.Done(): return } } }() // Sleep for a bit time.Sleep(10 * time.Millisecond) // Verify it is running job, err = s.JobById("A", nil) require.NoError(err) require.Equal(vagrant_server.Job_RUNNING, job.Job.State) // Stop heartbeating cancel() // Pause before check. We encounter the database being // scrubbed otherwise (TODO: fixme) time.Sleep(1 * time.Second) // Should time out require.Eventually(func() bool { // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) return job.Job.State == vagrant_server.Job_ERROR }, 1*time.Second, 10*time.Millisecond) }) t.Run("times out if running state loaded on restart", func(t *testing.T) { require := require.New(t) // Set a short timeout old := jobHeartbeatTimeout defer func() { jobHeartbeatTimeout = old }() jobHeartbeatTimeout = 250 * time.Millisecond s := TestState(t) defer s.Close() projRef := TestProjectProto(t, s) testRunnerProto(t, s, &vagrant_server.Runner{Id: "R_A"}) // Create a build require.NoError(s.JobCreate(TestJobProto(t, &vagrant_server.Job{ Id: "A", Scope: &vagrant_server.Job_Project{ Project: projRef, }, }))) // Assign it, we should get this build job, err := s.JobAssignForRunner(context.Background(), &vagrant_server.Runner{Id: "R_A"}) require.NoError(err) require.NotNil(job) require.Equal("A", job.Id) require.Equal(vagrant_server.Job_WAITING, job.State) // Ack it _, err = s.JobAck(job.Id, true) require.NoError(err) // Start heartbeating ctx, cancel := context.WithCancel(context.Background()) doneCh := make(chan struct{}) defer func() { cancel() <-doneCh }() go func(s *State) { defer close(doneCh) tick := time.NewTicker(20 * time.Millisecond) defer tick.Stop() for { select { case <-tick.C: s.JobHeartbeat(job.Id) case <-ctx.Done(): return } } }(s) // Reinit the state as if we crashed s = TestStateReinit(t, s) defer s.Close() // Verify it exists job, err = s.JobById("A", nil) require.NoError(err) require.NotNil(job) require.Equal(vagrant_server.Job_RUNNING, job.Job.State) // Should time out require.Eventually(func() bool { // Verify it is canceled job, err = s.JobById("A", nil) require.NoError(err) return job.Job.State == vagrant_server.Job_ERROR }, 2*time.Second, 10*time.Millisecond) }) }