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 }