github.com/ngicks/gokugen@v0.0.5/task_storage/single_node_test.go (about) 1 package taskstorage_test 2 3 import ( 4 "context" 5 "runtime" 6 "sync" 7 "sync/atomic" 8 "testing" 9 "time" 10 11 "github.com/ngicks/gokugen" 12 "github.com/ngicks/gokugen/impl/repository" 13 taskstorage "github.com/ngicks/gokugen/task_storage" 14 syncparam "github.com/ngicks/type-param-common/sync-param" 15 ) 16 17 var _ gokugen.Task = &fakeTask{} 18 19 type fakeTask struct { 20 ctx gokugen.SchedulerContext 21 } 22 23 func (t *fakeTask) Cancel() (cancelled bool) { 24 return true 25 } 26 27 func (t *fakeTask) CancelWithReason(err error) (cancelled bool) { 28 return true 29 } 30 31 func (t *fakeTask) GetScheduledTime() time.Time { 32 return t.ctx.ScheduledTime() 33 } 34 func (t *fakeTask) IsCancelled() (cancelled bool) { 35 return 36 } 37 func (t *fakeTask) IsDone() (done bool) { 38 return 39 } 40 41 func buildTaskStorage() ( 42 singleNode *taskstorage.SingleNodeTaskStorage, 43 multiNode *taskstorage.MultiNodeTaskStorage, 44 repo *repository.InMemoryRepo, 45 registry *syncparam.Map[string, gokugen.WorkFnWParam], 46 ) { 47 repo = repository.NewInMemoryRepo() 48 registry = new(syncparam.Map[string, gokugen.WorkFnWParam]) 49 singleNode = taskstorage.NewSingleNodeTaskStorage( 50 repo, 51 func(ti taskstorage.TaskInfo) bool { return true }, 52 registry, 53 nil, 54 ) 55 multiNode = taskstorage.NewMultiNodeTaskStorage( 56 repo, 57 func(ti taskstorage.TaskInfo) bool { return true }, 58 registry, 59 ) 60 return 61 } 62 63 type resultSet struct { 64 retVal any 65 err error 66 } 67 68 func prepare( 69 ts interface { 70 Middleware(freeParam bool) []gokugen.MiddlewareFunc 71 }, 72 freeParam bool, 73 ) ( 74 sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error), 75 doAllTasks func(), 76 getTaskResults func() []resultSet, 77 ) { 78 mws := ts.Middleware(freeParam) 79 80 workMu := sync.Mutex{} 81 works := make([]taskstorage.WorkFn, 0) 82 83 taskResults := make([]resultSet, 0) 84 doAllTasks = func() { 85 workMu.Lock() 86 defer workMu.Unlock() 87 for _, v := range works { 88 ret, err := v(context.TODO(), time.Now()) 89 taskResults = append(taskResults, resultSet{retVal: ret, err: err}) 90 } 91 } 92 getTaskResults = func() []resultSet { 93 workMu.Lock() 94 defer workMu.Unlock() 95 cloned := make([]resultSet, len(taskResults)) 96 copy(cloned, taskResults) 97 return cloned 98 } 99 sched = func(ctx gokugen.SchedulerContext) (gokugen.Task, error) { 100 workMu.Lock() 101 works = append(works, ctx.Work()) 102 workMu.Unlock() 103 return &fakeTask{ 104 ctx: ctx, 105 }, nil 106 } 107 for i := len(mws) - 1; i >= 0; i-- { 108 sched = mws[i](sched) 109 } 110 return 111 } 112 113 // parepare SingleNodeTaskStorage and other instances. 114 func prepareSingle(freeParam bool) ( 115 ts *taskstorage.SingleNodeTaskStorage, 116 repo *repository.InMemoryRepo, 117 registry *syncparam.Map[string, gokugen.WorkFnWParam], 118 sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error), 119 doAllTasks func(), 120 getTaskResults func() []resultSet, 121 ) { 122 ts, _, repo, registry = buildTaskStorage() 123 sched, doAllTasks, getTaskResults = prepare(ts, freeParam) 124 return 125 } 126 127 func TestSingleNode(t *testing.T) { 128 prep := func(paramLoad bool) func() ( 129 repo *repository.InMemoryRepo, 130 registry *syncparam.Map[string, gokugen.WorkFnWParam], 131 sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error), 132 doAllTasks func(), 133 getTaskResults func() []resultSet, 134 ) { 135 return func() ( 136 repo *repository.InMemoryRepo, 137 registry *syncparam.Map[string, gokugen.WorkFnWParam], 138 sched func(ctx gokugen.SchedulerContext) (gokugen.Task, error), 139 doAllTasks func(), 140 getTaskResults func() []resultSet, 141 ) { 142 _, repo, registry, sched, doAllTasks, getTaskResults = prepareSingle(paramLoad) 143 return 144 } 145 } 146 147 t.Run("no param load", func(t *testing.T) { 148 storageTestSet(t, prep(false)) 149 }) 150 151 t.Run("param load", func(t *testing.T) { 152 storageTestSet(t, prep(true)) 153 }) 154 155 t.Run("param is freed after task storage if freeParam is set to true", func(t *testing.T) { 156 _, repo, registry, sched, doAllTasks, _ := prepareSingle(true) 157 158 registry.Store("foobar", func(ctx context.Context, scheduled time.Time, param any) (any, error) { 159 return nil, nil 160 }) 161 162 type exampleParam struct { 163 Foo string 164 Bar int 165 } 166 paramUsedInSched := new(exampleParam) 167 paramStoredInRepo := new(exampleParam) 168 169 var called int64 170 runtime.SetFinalizer(paramUsedInSched, func(*exampleParam) { 171 atomic.AddInt64(&called, 1) 172 }) 173 174 _, _ = sched( 175 gokugen.BuildContext( 176 time.Now(), 177 nil, 178 nil, 179 gokugen.WithParam(paramUsedInSched), 180 gokugen.WithWorkId("foobar"), 181 ), 182 ) 183 stored, _ := repo.GetAll() 184 taskId := stored[0].Id 185 // see comment below. 186 // 187 // paramInRepo := stored[0].Param 188 repo.Update(taskId, taskstorage.UpdateDiff{ 189 UpdateKey: taskstorage.UpdateKey{ 190 Param: true, 191 }, 192 Diff: taskstorage.TaskInfo{ 193 Param: paramStoredInRepo, 194 }, 195 }) 196 197 doAllTasks() 198 199 for i := 0; i < 100; i++ { 200 runtime.GC() 201 if atomic.LoadInt64(&called) == 1 { 202 break 203 } 204 } 205 206 if atomic.LoadInt64(&called) != 1 { 207 t.Fatalf("param is not dropped.") 208 } 209 // Comment-in these lines to see `paramUsedInSched` | `paramInRepo` is now determine to be not reachable. 210 // At least, the case fails if they are kept alive. 211 // 212 // runtime.KeepAlive(paramUsedInSched) 213 // runtime.KeepAlive(paramInRepo) 214 runtime.KeepAlive(paramStoredInRepo) 215 }) 216 }