flamenco/internal/manager/job_deleter/job_deleter_test.go
Sybren A. Stüvel 286d0efa2d Manager: speed up job deletion by skipping the DB integrity check
Speed up the deletion of multiple jobs by skipping the database integrity
check. It is now clear what was causing the integrity issues (disabled
foreign key constraints), and this is now checked for before deleting
anything. This reduces the deletion time from ~500ms per job to ~150ms
(on my computer, with my database, of course).
2024-05-28 16:07:22 +02:00

190 lines
6.2 KiB
Go

package job_deleter
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"projects.blender.org/studio/flamenco/internal/manager/job_deleter/mocks"
"projects.blender.org/studio/flamenco/internal/manager/persistence"
"projects.blender.org/studio/flamenco/pkg/shaman"
)
type JobDeleterMocks struct {
persist *mocks.MockPersistenceService
storage *mocks.MockStorage
broadcaster *mocks.MockChangeBroadcaster
shaman *mocks.MockShaman
ctx context.Context
cancel context.CancelFunc
}
func TestQueueJobDeletion(t *testing.T) {
s, finish, mocks := jobDeleterTestFixtures(t)
defer finish()
mocks.broadcaster.EXPECT().BroadcastJobUpdate(gomock.Any()).Times(3)
job1 := &persistence.Job{UUID: "2f7d910f-08a6-4b0f-8ecb-b3946939ed1b"}
mocks.persist.EXPECT().RequestJobDeletion(mocks.ctx, job1)
require.NoError(t, s.QueueJobDeletion(mocks.ctx, job1))
// Call twice more to overflow the queue.
job2 := &persistence.Job{UUID: "e8fbe41c-ed24-46df-ba63-8d4f5524071b"}
mocks.persist.EXPECT().RequestJobDeletion(mocks.ctx, job2)
require.NoError(t, s.QueueJobDeletion(mocks.ctx, job2))
job3 := &persistence.Job{UUID: "deeab6ba-02cd-42c0-b7bc-2367a2f04c7d"}
mocks.persist.EXPECT().RequestJobDeletion(mocks.ctx, job3)
require.NoError(t, s.QueueJobDeletion(mocks.ctx, job3))
if assert.Len(t, s.queue, 2, "the first two job UUID should be queued") {
assert.Equal(t, job1.UUID, <-s.queue)
assert.Equal(t, job2.UUID, <-s.queue)
}
}
func TestQueuePendingDeletions(t *testing.T) {
s, finish, mocks := jobDeleterTestFixtures(t)
defer finish()
// Queue one more job than fits.
job1 := "aa420164-926a-45d5-ae8b-510ff3d2cd4d"
job2 := "e5feadee-999e-48c2-853d-9db94e7623b0"
job3 := "8516ac60-787c-411e-80a7-026456034da4"
mocks.persist.EXPECT().
FetchJobsDeletionRequested(mocks.ctx).
Return([]string{job1, job2, job3}, nil)
s.queuePendingDeletions(mocks.ctx)
if assert.Len(t, s.queue, 2, "the first two job UUIDs should be queued") {
assert.Equal(t, job1, <-s.queue)
assert.Equal(t, job2, <-s.queue)
}
}
func TestQueuePendingDeletionsUnhappy(t *testing.T) {
s, finish, mocks := jobDeleterTestFixtures(t)
defer finish()
// Any error fetching the deletion-requested jobs should just be logged, and
// not cause any issues.
mocks.persist.EXPECT().
FetchJobsDeletionRequested(mocks.ctx).
Return(nil, errors.New("mocked DB failure"))
s.queuePendingDeletions(mocks.ctx)
assert.Len(t, s.queue, 0)
}
func TestDeleteJobWithoutShaman(t *testing.T) {
s, finish, mocks := jobDeleterTestFixtures(t)
defer finish()
jobUUID := "2f7d910f-08a6-4b0f-8ecb-b3946939ed1b"
mocks.shaman.EXPECT().IsEnabled().Return(false).AnyTimes()
mocks.persist.EXPECT().
FetchJobsDeletionRequested(mocks.ctx).
Return([]string{jobUUID}, nil).
AnyTimes()
// Mock log storage deletion failure. This should prevent the deletion from the database.
mocks.storage.EXPECT().
RemoveJobStorage(mocks.ctx, jobUUID).
Return(errors.New("intended log file deletion failure"))
assert.Error(t, s.deleteJob(mocks.ctx, jobUUID))
// Mock that log storage deletion is ok, but database is not.
mocks.storage.EXPECT().RemoveJobStorage(mocks.ctx, jobUUID)
mocks.persist.EXPECT().DeleteJob(mocks.ctx, jobUUID).
Return(errors.New("mocked DB error"))
assert.Error(t, s.deleteJob(mocks.ctx, jobUUID))
// Mock that everything went OK.
mocks.storage.EXPECT().RemoveJobStorage(mocks.ctx, jobUUID)
mocks.persist.EXPECT().DeleteJob(mocks.ctx, jobUUID)
mocks.broadcaster.EXPECT().BroadcastJobUpdate(gomock.Any())
require.NoError(t, s.deleteJob(mocks.ctx, jobUUID))
}
func TestDeleteJobWithShaman(t *testing.T) {
s, finish, mocks := jobDeleterTestFixtures(t)
defer finish()
jobUUID := "2f7d910f-08a6-4b0f-8ecb-b3946939ed1b"
mocks.shaman.EXPECT().IsEnabled().Return(true).AnyTimes()
mocks.persist.EXPECT().
FetchJobsDeletionRequested(mocks.ctx).
Return([]string{jobUUID}, nil).
AnyTimes()
shamanCheckoutID := "010_0431_lighting"
mocks.persist.EXPECT().FetchJobShamanCheckoutID(mocks.ctx, jobUUID).Return(shamanCheckoutID, nil).AnyTimes()
// Mock that Shaman deletion failed. The rest of the deletion should be
// blocked by this.
mocks.shaman.EXPECT().EraseCheckout(shamanCheckoutID).Return(errors.New("mocked failure"))
assert.Error(t, s.deleteJob(mocks.ctx, jobUUID))
// Mock that Shaman deletion couldn't happen because the checkout dir doesn't
// exist. The rest of the deletion should continue.
mocks.shaman.EXPECT().EraseCheckout(shamanCheckoutID).Return(shaman.ErrDoesNotExist)
// Mock log storage deletion failure. This should prevent the deletion from the database.
mocks.storage.EXPECT().
RemoveJobStorage(mocks.ctx, jobUUID).
Return(errors.New("intended log file deletion failure"))
assert.Error(t, s.deleteJob(mocks.ctx, jobUUID))
// Mock that log storage deletion is ok, but database is not.
mocks.shaman.EXPECT().EraseCheckout(shamanCheckoutID)
mocks.storage.EXPECT().RemoveJobStorage(mocks.ctx, jobUUID)
mocks.persist.EXPECT().DeleteJob(mocks.ctx, jobUUID).
Return(errors.New("mocked DB error"))
assert.Error(t, s.deleteJob(mocks.ctx, jobUUID))
// Mock that everything went OK.
mocks.shaman.EXPECT().EraseCheckout(shamanCheckoutID)
mocks.storage.EXPECT().RemoveJobStorage(mocks.ctx, jobUUID)
mocks.persist.EXPECT().DeleteJob(mocks.ctx, jobUUID)
mocks.broadcaster.EXPECT().BroadcastJobUpdate(gomock.Any())
require.NoError(t, s.deleteJob(mocks.ctx, jobUUID))
}
func jobDeleterTestFixtures(t *testing.T) (*Service, func(), *JobDeleterMocks) {
mockCtrl := gomock.NewController(t)
mocks := &JobDeleterMocks{
persist: mocks.NewMockPersistenceService(mockCtrl),
storage: mocks.NewMockStorage(mockCtrl),
broadcaster: mocks.NewMockChangeBroadcaster(mockCtrl),
shaman: mocks.NewMockShaman(mockCtrl),
}
ctx, cancel := context.WithCancel(context.Background())
mocks.ctx = ctx
mocks.cancel = cancel
// This should be called at the end of each unit test.
finish := func() {
mocks.cancel()
jobDeletionQueueSize = defaultJobDeletionQueueSize
}
jobDeletionQueueSize = 2
s := NewService(
mocks.persist,
mocks.storage,
mocks.broadcaster,
mocks.shaman,
)
return s, finish, mocks
}