github.com/ngicks/gokugen@v0.0.5/task_storage/task_storage_test.go (about)

     1  package taskstorage_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sync/atomic"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/ngicks/gokugen"
    11  	"github.com/ngicks/gokugen/impl/repository"
    12  	taskstorage "github.com/ngicks/gokugen/task_storage"
    13  	syncparam "github.com/ngicks/type-param-common/sync-param"
    14  )
    15  
    16  func storageTestSet(
    17  	t *testing.T,
    18  	prepare func() (
    19  		repo *repository.InMemoryRepo,
    20  		registry *syncparam.Map[string, gokugen.WorkFnWParam],
    21  		sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error),
    22  		doAllTasks func(),
    23  		getTaskResults func() []resultSet,
    24  	),
    25  ) {
    26  	t.Run("basic usage", func(t *testing.T) {
    27  		repo, registry, sched, doAllTasks, _ := prepare()
    28  
    29  		registry.Store("foobar", func(taskCtx context.Context, scheduled time.Time, param any) (any, error) {
    30  			return nil, nil
    31  		})
    32  		now := time.Now()
    33  		task, err := sched(
    34  			gokugen.BuildContext(
    35  				now,
    36  				nil,
    37  				nil,
    38  				gokugen.WithParam(nil),
    39  				gokugen.WithWorkId("foobar"),
    40  			),
    41  		)
    42  		if err != nil {
    43  			t.Fatalf("must not be non nil error: %v", err)
    44  		}
    45  		if task.GetScheduledTime() != now {
    46  			t.Fatalf(
    47  				"scheduled time is modified: now=%s, stored in task=%s",
    48  				now.Format(time.RFC3339Nano),
    49  				task.GetScheduledTime().Format(time.RFC3339Nano),
    50  			)
    51  		}
    52  		stored, err := repo.GetAll()
    53  		if err != nil {
    54  			t.Fatalf("must not be non nil error: %v", err)
    55  		}
    56  		if len(stored) == 0 {
    57  			t.Fatalf("stored task must not be zero")
    58  		}
    59  		storedTask := stored[0]
    60  		taskId := storedTask.Id
    61  		if storedTask.WorkId != "foobar" {
    62  			t.Fatalf("unmatched work id: %s", storedTask.WorkId)
    63  		}
    64  		if storedTask.ScheduledTime != now {
    65  			t.Fatalf("unmatched scheduled time: %s", storedTask.ScheduledTime.Format(time.RFC3339Nano))
    66  		}
    67  
    68  		doAllTasks()
    69  
    70  		storedInfoLater, err := repo.GetById(taskId)
    71  		if err != nil {
    72  			t.Fatalf("must not be non nil error: %v", err)
    73  		}
    74  
    75  		if storedInfoLater.State != taskstorage.Done {
    76  			t.Fatalf("incorrect state: %s", storedInfoLater.State)
    77  		}
    78  	})
    79  
    80  	t.Run("cancel marks data as cancelled inside repository", func(t *testing.T) {
    81  		repo, registry, sched, _, _ := prepare()
    82  
    83  		registry.Store("foobar", func(taskCtx context.Context, scheduled time.Time, param any) (any, error) {
    84  			return nil, nil
    85  		})
    86  		now := time.Now()
    87  		task, _ := sched(
    88  			gokugen.BuildContext(
    89  				now,
    90  				nil,
    91  				nil,
    92  				gokugen.WithParam(nil),
    93  				gokugen.WithWorkId("foobar"),
    94  			),
    95  		)
    96  		task.Cancel()
    97  
    98  		stored, _ := repo.GetAll()
    99  		storedTask := stored[0]
   100  
   101  		if storedTask.State != taskstorage.Cancelled {
   102  			t.Fatalf("wrong state: must be cancelled, but is %s", storedTask.State)
   103  		}
   104  	})
   105  
   106  	t.Run("failed marks data as failed inside repository", func(t *testing.T) {
   107  		repo, registry, sched, doAllTasks, _ := prepare()
   108  
   109  		registry.Store("foobar", func(taskCtx context.Context, scheduled time.Time, param any) (any, error) {
   110  			return nil, errors.New("mock error")
   111  		})
   112  		now := time.Now()
   113  		sched(
   114  			gokugen.BuildContext(
   115  				now,
   116  				nil,
   117  				nil,
   118  				gokugen.WithParam(nil),
   119  				gokugen.WithWorkId("foobar"),
   120  			),
   121  		)
   122  
   123  		doAllTasks()
   124  
   125  		stored, _ := repo.GetAll()
   126  		storedTask := stored[0]
   127  
   128  		if storedTask.State != taskstorage.Failed {
   129  			t.Fatalf("wrong state: must be failed, but is %s", storedTask.State)
   130  		}
   131  	})
   132  }
   133  
   134  type testMode int
   135  
   136  const (
   137  	singleNodeMode testMode = iota
   138  	multiNodeMode
   139  )
   140  
   141  type syncer interface {
   142  	Sync(
   143  		schedule func(ctx gokugen.SchedulerContext) (gokugen.Task, error),
   144  	) (rescheduled map[string]gokugen.Task, schedulingErr map[string]error, err error)
   145  }
   146  
   147  func testSync(t *testing.T, mode testMode) {
   148  	var ts syncer
   149  	var repo *repository.InMemoryRepo
   150  	var registry *syncparam.Map[string, gokugen.WorkFnWParam]
   151  	var sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error)
   152  	var doAllTasks func()
   153  
   154  	switch mode {
   155  	case singleNodeMode:
   156  		ts, repo, registry, sched, doAllTasks, _ = prepareSingle(true)
   157  	case multiNodeMode:
   158  		ts, repo, registry, sched, doAllTasks, _ = prepareMulti(true)
   159  	}
   160  
   161  	rescheduled, schedulingErr, err := ts.Sync(sched)
   162  	if len(rescheduled) != 0 || len(schedulingErr) != 0 || err != nil {
   163  		t.Fatalf(
   164  			"len of rescheduled = %d, len of schedulingErr = %d, err = %v",
   165  			len(rescheduled),
   166  			len(schedulingErr),
   167  			err,
   168  		)
   169  	}
   170  
   171  	var called int64
   172  	registry.Store("foobar", func(taskCtx context.Context, scheduled time.Time, param any) (any, error) {
   173  		atomic.AddInt64(&called, 1)
   174  		return nil, nil
   175  	})
   176  	registry.Store("external", func(taskCtx context.Context, scheduled time.Time, param any) (any, error) {
   177  		atomic.AddInt64(&called, 1)
   178  		return nil, nil
   179  	})
   180  
   181  	sched(
   182  		gokugen.BuildContext(time.Now(), nil, nil, gokugen.WithParam(nil), gokugen.WithWorkId("foobar")),
   183  	)
   184  	sched(
   185  		gokugen.BuildContext(time.Now(), nil, nil, gokugen.WithParam(nil), gokugen.WithWorkId("foobar")),
   186  	)
   187  	sched(
   188  		gokugen.BuildContext(time.Now(), nil, nil, gokugen.WithParam(nil), gokugen.WithWorkId("foobar")),
   189  	)
   190  
   191  	task1, task2, task3 := func() (taskstorage.TaskInfo, taskstorage.TaskInfo, taskstorage.TaskInfo) {
   192  		tasks, err := repo.GetAll()
   193  		if err != nil {
   194  			t.Fatalf("should not be error: %v", err)
   195  		}
   196  		return tasks[0], tasks[1], tasks[2]
   197  	}()
   198  
   199  	// task1 is known, no one changed it.
   200  
   201  	// task2 is known but changed externally.
   202  	repo.Update(task2.Id, taskstorage.UpdateDiff{
   203  		UpdateKey: taskstorage.UpdateKey{
   204  			State: true,
   205  		},
   206  		Diff: taskstorage.TaskInfo{
   207  			State: taskstorage.Working,
   208  		},
   209  	})
   210  
   211  	// task3 will be changed later.
   212  
   213  	// unknown and must be rescheduled.
   214  	repo.Insert(taskstorage.TaskInfo{
   215  		WorkId:        "external",
   216  		ScheduledTime: time.Now(),
   217  		State:         taskstorage.Initialized,
   218  	})
   219  
   220  	// unknown and must **NOT** be rescheduled.
   221  	repo.Insert(taskstorage.TaskInfo{
   222  		WorkId:        "external",
   223  		ScheduledTime: time.Now(),
   224  		State:         taskstorage.Working,
   225  	})
   226  
   227  	// unknown work id
   228  	repo.Insert(taskstorage.TaskInfo{
   229  		WorkId:        "baz?",
   230  		ScheduledTime: time.Now(),
   231  		State:         taskstorage.Initialized,
   232  	})
   233  
   234  	rescheduled, schedErr, err := ts.Sync(sched)
   235  	if err != nil {
   236  		t.Fatalf("must not be err: %v", err)
   237  	}
   238  
   239  	if len(rescheduled) != 1 {
   240  		t.Fatalf("rescheduled must be 1: %v", rescheduled)
   241  	}
   242  	if len(schedErr) != 1 {
   243  		t.Fatalf("schedErr must be 1: %d", len(schedErr))
   244  	}
   245  
   246  	repo.Update(task3.Id, taskstorage.UpdateDiff{
   247  		UpdateKey: taskstorage.UpdateKey{
   248  			State: true,
   249  		},
   250  		Diff: taskstorage.TaskInfo{
   251  			State: taskstorage.Working,
   252  		},
   253  	})
   254  
   255  	rescheduled, schedErr, err = ts.Sync(sched)
   256  	if err != nil {
   257  		t.Fatalf("must not be err: %v", err)
   258  	}
   259  
   260  	if len(rescheduled) != 0 {
   261  		t.Fatalf("rescheduled must be 0: %d", len(rescheduled))
   262  	}
   263  	if len(schedErr) != 0 {
   264  		t.Fatalf("schedErr must be 0: %d", len(schedErr))
   265  	}
   266  
   267  	doAllTasks()
   268  
   269  	currentCallCount := atomic.LoadInt64(&called)
   270  	var violated bool
   271  	switch mode {
   272  	case singleNodeMode:
   273  		// In single node mode, it ignores Working state. So call count is 4.
   274  		violated = currentCallCount != 4
   275  	case multiNodeMode:
   276  		// In multi node mode, it respects Working state. So call count is 2.
   277  		violated = currentCallCount != 2
   278  	}
   279  	if violated {
   280  		t.Fatalf("call count is %d", currentCallCount)
   281  	}
   282  
   283  	info, err := repo.GetById(task1.Id)
   284  	if err != nil {
   285  		t.Fatalf("must not be err: %v", err)
   286  	}
   287  	if info.State != taskstorage.Done {
   288  		t.Fatalf("work is not done correctly: %s", info.State)
   289  	}
   290  }
   291  
   292  func TestTaskStorageSync(t *testing.T) {
   293  	t.Run("Sync: single node", func(t *testing.T) {
   294  		testSync(t, singleNodeMode)
   295  	})
   296  
   297  	t.Run("Sync: multi node", func(t *testing.T) {
   298  		testSync(t, multiNodeMode)
   299  	})
   300  }