github.com/benz9527/toy-box/algo@v0.0.0-20240221120937-66c0c6bd5abd/timer/x_timing_wheels_test.go (about)

     1  package timer
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"github.com/stretchr/testify/assert"
     7  	"log/slog"
     8  	"sort"
     9  	"sync/atomic"
    10  	"testing"
    11  	"time"
    12  	"unsafe"
    13  )
    14  
    15  func TestTimingWheel_AlignmentAndSize(t *testing.T) {
    16  	tw := &timingWheel{}
    17  	t.Logf("tw alignment: %d\n", unsafe.Alignof(tw))
    18  	t.Logf("tw ctx alignment: %d\n", unsafe.Alignof(tw.ctx))
    19  	t.Logf("tw slot alignment: %d\n", unsafe.Alignof(tw.slots))
    20  	t.Logf("tw tickMs alignment: %d\n", unsafe.Alignof(tw.tickMs))
    21  	t.Logf("tw startMs alignment: %d\n", unsafe.Alignof(tw.startMs))
    22  	t.Logf("tw slotSize alignment: %d\n", unsafe.Alignof(tw.slotSize))
    23  	t.Logf("tw globalSlotCounterRef alignment: %d\n", unsafe.Alignof(tw.globalSlotCounterRef))
    24  	t.Logf("tw overflowWheelRef alignment: %d\n", unsafe.Alignof(tw.overflowWheelRef))
    25  	t.Logf("tw globalDqRef alignment: %d\n", unsafe.Alignof(tw.globalDqRef))
    26  
    27  	t.Logf("tw size: %d\n", unsafe.Sizeof(*tw))
    28  	t.Logf("tw ctx size: %d\n", unsafe.Sizeof(tw.ctx))
    29  	t.Logf("tw slot size: %d\n", unsafe.Sizeof(tw.slots))
    30  	t.Logf("tw tickMs size: %d\n", unsafe.Sizeof(tw.tickMs))
    31  	t.Logf("tw startMs size: %d\n", unsafe.Sizeof(tw.startMs))
    32  	t.Logf("tw slotSize size: %d\n", unsafe.Sizeof(tw.slotSize))
    33  	t.Logf("tw globalSlotCounterRef size: %d\n", unsafe.Sizeof(tw.globalSlotCounterRef))
    34  	t.Logf("tw overflowWheelRef size: %d\n", unsafe.Sizeof(tw.overflowWheelRef))
    35  	t.Logf("tw globalDqRef size: %d\n", unsafe.Sizeof(tw.globalDqRef))
    36  }
    37  
    38  func TestXTimingWheels_AlignmentAndSize(t *testing.T) {
    39  	tw := &xTimingWheels{}
    40  	t.Logf("tw alignment: %d\n", unsafe.Alignof(tw))
    41  	t.Logf("tw ctx alignment: %d\n", unsafe.Alignof(tw.ctx))
    42  	t.Logf("tw tw alignment: %d\n", unsafe.Alignof(tw.tw))
    43  	t.Logf("tw stopC alignment: %d\n", unsafe.Alignof(tw.stopC))
    44  	t.Logf("tw twEventC alignment: %d\n", unsafe.Alignof(tw.twEventC))
    45  	t.Logf("tw twEventPool alignment: %d\n", unsafe.Alignof(tw.twEventPool))
    46  	t.Logf("tw expiredSlotC alignment: %d\n", unsafe.Alignof(tw.expiredSlotC))
    47  	t.Logf("tw taskCounter alignment: %d\n", unsafe.Alignof(tw.taskCounter))
    48  	t.Logf("tw slotCounter alignment: %d\n", unsafe.Alignof(tw.slotCounter))
    49  	t.Logf("tw isRunning alignment: %d\n", unsafe.Alignof(tw.isRunning))
    50  	t.Logf("tw dq alignment: %d\n", unsafe.Alignof(tw.dq))
    51  	t.Logf("tw tasksMap alignment: %d\n", unsafe.Alignof(tw.tasksMap))
    52  
    53  	t.Logf("tw size: %d\n", unsafe.Sizeof(*tw))
    54  	t.Logf("tw ctx size: %d\n", unsafe.Sizeof(tw.ctx))
    55  	t.Logf("tw tw size: %d\n", unsafe.Sizeof(tw.tw))
    56  	t.Logf("tw stopC size: %d\n", unsafe.Sizeof(tw.stopC))
    57  	t.Logf("tw twEventC size: %d\n", unsafe.Sizeof(tw.twEventC))
    58  	t.Logf("tw twEventPool size: %d\n", unsafe.Sizeof(tw.twEventPool))
    59  	t.Logf("tw expiredSlotC size: %d\n", unsafe.Sizeof(tw.expiredSlotC))
    60  	t.Logf("tw taskCounter size: %d\n", unsafe.Sizeof(tw.taskCounter))
    61  	t.Logf("tw slotCounter size: %d\n", unsafe.Sizeof(tw.slotCounter))
    62  	t.Logf("tw isRunning size: %d\n", unsafe.Sizeof(tw.isRunning))
    63  	t.Logf("tw dq size: %d\n", unsafe.Sizeof(tw.dq))
    64  	t.Logf("tw tasksMap size: %d\n", unsafe.Sizeof(tw.tasksMap))
    65  }
    66  
    67  func TestNewTimingWheels(t *testing.T) {
    68  	ctx, cancel := context.WithTimeoutCause(context.TODO(), time.Second, errors.New("timeout"))
    69  	defer cancel()
    70  	tw := NewTimingWheels(
    71  		ctx,
    72  		time.Now().UTC().UnixMilli(),
    73  		WithTimingWheelTickMs(100),
    74  		WithTimingWheelSlotSize(32),
    75  	)
    76  	t.Logf("tw tickMs: %d\n", tw.GetTickMs())
    77  	t.Logf("tw startMs: %d\n", tw.GetStartMs())
    78  	t.Logf("tw slotSize: %d\n", tw.GetSlotSize())
    79  	t.Logf("tw globalTaskCounterRef: %d\n", tw.GetTaskCounter())
    80  	<-ctx.Done()
    81  	time.Sleep(100 * time.Millisecond)
    82  }
    83  
    84  func testAfterFunc(t *testing.T) {
    85  	ctx, cancel := context.WithTimeoutCause(context.Background(), 2100*time.Millisecond, errors.New("timeout"))
    86  	defer cancel()
    87  	tw := NewTimingWheels(
    88  		ctx,
    89  		time.Now().UTC().UnixMilli(),
    90  	)
    91  
    92  	delays := []time.Duration{
    93  		time.Millisecond,
    94  		2 * time.Millisecond,
    95  		5 * time.Millisecond,
    96  		10 * time.Millisecond,
    97  		15 * time.Millisecond,
    98  		18 * time.Millisecond,
    99  		20 * time.Millisecond,
   100  		21 * time.Millisecond,
   101  		50 * time.Millisecond,
   102  		100 * time.Millisecond,
   103  		200 * time.Millisecond,
   104  		500 * time.Millisecond,
   105  		time.Second,
   106  	}
   107  
   108  	expectedExecCount := len(delays)
   109  	actualExecCount := 0
   110  	for i := 0; i < len(delays); i++ {
   111  		//lowResolutionBeginTs := time.Now().UTC().UnixMilli()
   112  		_, err := tw.AfterFunc(delays[i], func(ctx context.Context, md JobMetadata) {
   113  			actualExecCount++
   114  			//execAt := time.Now().UTC().UnixMilli()
   115  			//slog.Info("after func", "expired ms", md.GetExpiredMs(), "exec at", execAt,
   116  			//	"interval", md.GetExpiredMs()-lowResolutionBeginTs, "diff", execAt-md.GetExpiredMs())
   117  		})
   118  		assert.NoError(t, err)
   119  	}
   120  	t.Logf("begin tw tasks: %d\n", tw.GetTaskCounter())
   121  	<-ctx.Done()
   122  	time.Sleep(50 * time.Millisecond)
   123  	t.Logf("final tw tasks: %d\n", tw.GetTaskCounter())
   124  	if actualExecCount != expectedExecCount || tw.GetTaskCounter() > 1 || tw.GetTaskCounter() < 0 {
   125  		t.Logf("actual exec count: %d, expected exec count: %d\n", actualExecCount, expectedExecCount)
   126  		panic("exec count not match")
   127  	}
   128  }
   129  
   130  func TestXTimingWheels_AfterFunc(t *testing.T) {
   131  	loops := 100
   132  	for i := 0; i < loops; i++ {
   133  		t.Logf("loop %d\n", i)
   134  		testAfterFunc(t)
   135  	}
   136  }
   137  
   138  func TestXTimingWheels_ScheduleFunc_ConcurrentFinite(t *testing.T) {
   139  	ctx, cancel := context.WithTimeoutCause(context.Background(), 2100*time.Millisecond, errors.New("timeout"))
   140  	defer cancel()
   141  	tw := NewTimingWheels(
   142  		ctx,
   143  		time.Now().UTC().UnixMilli(),
   144  	)
   145  
   146  	delays := []time.Duration{
   147  		time.Millisecond,
   148  		2 * time.Millisecond,
   149  		5 * time.Millisecond,
   150  		10 * time.Millisecond,
   151  		15 * time.Millisecond,
   152  		18 * time.Millisecond,
   153  		20 * time.Millisecond,
   154  		21 * time.Millisecond,
   155  		50 * time.Millisecond,
   156  		100 * time.Millisecond,
   157  		200 * time.Millisecond,
   158  		500 * time.Millisecond,
   159  		time.Second,
   160  	}
   161  	schedFn := func() Scheduler {
   162  		return NewFiniteScheduler(delays...)
   163  	}
   164  	assert.NotNil(t, schedFn)
   165  	execCounter := &atomic.Int32{}
   166  
   167  	go func() {
   168  		task, err := tw.ScheduleFunc(schedFn, func(ctx context.Context, md JobMetadata) {
   169  			execCounter.Add(1)
   170  		})
   171  		assert.NoError(t, err)
   172  		t.Logf("task1: %s\n", task.GetJobID())
   173  	}()
   174  	go func() {
   175  		task, err := tw.ScheduleFunc(schedFn, func(ctx context.Context, md JobMetadata) {
   176  			execCounter.Add(1)
   177  		})
   178  		assert.NoError(t, err)
   179  		t.Logf("task2: %s\n", task.GetJobID())
   180  	}()
   181  
   182  	t.Logf("tw tickMs: %d\n", tw.GetTickMs())
   183  	t.Logf("tw startMs: %d\n", tw.GetStartMs())
   184  	t.Logf("tw slotSize: %d\n", tw.GetSlotSize())
   185  	t.Logf("tw tasks: %d\n", tw.GetTaskCounter())
   186  	<-ctx.Done()
   187  	time.Sleep(100 * time.Millisecond)
   188  	t.Logf("final tw tasks: %d\n", tw.GetTaskCounter())
   189  	expectedExecCount := len(delays) * 2
   190  	actualExecCount := execCounter.Load()
   191  	assert.Equal(t, expectedExecCount, int(actualExecCount))
   192  	assert.Equal(t, int64(0), tw.GetTaskCounter())
   193  }
   194  
   195  func TestXTimingWheels_ScheduleFunc_1MsInfinite(t *testing.T) {
   196  	ctx, cancel := context.WithTimeoutCause(context.Background(), 1*time.Second, errors.New("timeout"))
   197  	defer cancel()
   198  	tw := NewTimingWheels(
   199  		ctx,
   200  		time.Now().UTC().UnixMilli(),
   201  	)
   202  
   203  	delays := []time.Duration{
   204  		time.Millisecond,
   205  	}
   206  	schedFn := func() Scheduler {
   207  		return NewInfiniteScheduler(delays...)
   208  	}
   209  	assert.NotNil(t, schedFn())
   210  	// FIXME 1ms infinite scheduling will occur critical delay execution error
   211  	task, err := tw.ScheduleFunc(schedFn, func(ctx context.Context, md JobMetadata) {
   212  		execAt := time.Now().UTC().UnixMilli()
   213  		slog.Info("infinite sched1 after func", "expired ms", md.GetExpiredMs(), "exec at", execAt, "diff",
   214  			execAt-md.GetExpiredMs())
   215  	})
   216  	assert.NoError(t, err)
   217  	t.Logf("task1: %s\n", task.GetJobID())
   218  
   219  	t.Logf("tw tickMs: %d\n", tw.GetTickMs())
   220  	t.Logf("tw startMs: %d\n", tw.GetStartMs())
   221  	t.Logf("tw slotSize: %d\n", tw.GetSlotSize())
   222  	t.Logf("tw tasks: %d\n", tw.GetTaskCounter())
   223  	<-ctx.Done()
   224  	time.Sleep(100 * time.Millisecond)
   225  	t.Logf("final tw tasks: %d\n", tw.GetTaskCounter())
   226  }
   227  
   228  func TestXTimingWheels_ScheduleFunc_32MsInfinite(t *testing.T) {
   229  	ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("timeout"))
   230  	defer cancel()
   231  	tw := NewTimingWheels(
   232  		ctx,
   233  		time.Now().UTC().UnixMilli(),
   234  	)
   235  
   236  	delays := []time.Duration{
   237  		18 * time.Millisecond,
   238  	}
   239  	schedFn := func() Scheduler {
   240  		return NewInfiniteScheduler(delays...)
   241  	}
   242  	assert.NotNil(t, schedFn())
   243  	task, err := tw.ScheduleFunc(schedFn, func(ctx context.Context, md JobMetadata) {
   244  		execAt := time.Now().UTC().UnixMilli()
   245  		slog.Info("infinite sched32 after func", "expired ms", md.GetExpiredMs(), "exec at", execAt, "diff",
   246  			execAt-md.GetExpiredMs())
   247  	})
   248  	assert.NoError(t, err)
   249  	t.Logf("task1: %s\n", task.GetJobID())
   250  
   251  	t.Logf("tw tickMs: %d\n", tw.GetTickMs())
   252  	t.Logf("tw startMs: %d\n", tw.GetStartMs())
   253  	t.Logf("tw slotSize: %d\n", tw.GetSlotSize())
   254  	t.Logf("tw tasks: %d\n", tw.GetTaskCounter())
   255  	<-ctx.Done()
   256  	time.Sleep(100 * time.Millisecond)
   257  	t.Logf("final tw tasks: %d\n", tw.GetTaskCounter())
   258  }
   259  
   260  func TestXTimingWheels_AfterFunc_Slots(t *testing.T) {
   261  	ctx, cancel := context.WithTimeoutCause(context.Background(), 500*time.Millisecond, errors.New("timeout"))
   262  	defer cancel()
   263  	ctx = context.WithValue(ctx, disableTimingWheelsScheduleCancelTask, true)
   264  	ctx = context.WithValue(ctx, disableTimingWheelsScheduleExpiredSlot, true)
   265  	ctx = context.WithValue(ctx, disableTimingWheelsSchedulePoll, true)
   266  
   267  	tw := NewTimingWheels(
   268  		ctx,
   269  		time.Now().UTC().UnixMilli(),
   270  	)
   271  
   272  	delays := []time.Duration{
   273  		3 * time.Millisecond,
   274  		4 * time.Millisecond,
   275  		5 * time.Millisecond,
   276  		10 * time.Millisecond,
   277  		15 * time.Millisecond,
   278  		18 * time.Millisecond,
   279  		20 * time.Millisecond,
   280  		21 * time.Millisecond,
   281  		40 * time.Millisecond,
   282  		50 * time.Millisecond,
   283  		100 * time.Millisecond,
   284  		200 * time.Millisecond,
   285  		400 * time.Millisecond,
   286  		500 * time.Millisecond,
   287  		time.Second,
   288  	}
   289  
   290  	for i := 0; i < len(delays); i++ {
   291  		_, err := tw.AfterFunc(delays[i], func(ctx context.Context, md JobMetadata) {})
   292  		assert.NoError(t, err)
   293  	}
   294  	t.Logf("tw tickMs: %d\n", tw.GetTickMs())
   295  	t.Logf("tw startMs: %d\n", tw.GetStartMs())
   296  	t.Logf("tw slotSize: %d\n", tw.GetSlotSize())
   297  	t.Logf("tw tasks: %d\n", tw.GetTaskCounter())
   298  
   299  	<-time.After(100 * time.Millisecond)
   300  	taskIDs := make([]string, 0, len(delays))
   301  	for k := range tw.(*xTimingWheels).tasksMap {
   302  		taskIDs = append(taskIDs, string(k))
   303  	}
   304  	sort.Strings(taskIDs)
   305  	for _, taskID := range taskIDs {
   306  		v := tw.(*xTimingWheels).tasksMap[JobID(taskID)]
   307  		t.Logf("slot level: %d, ID %d, %d\n", v.GetSlot().GetLevel(), v.GetSlot().GetSlotID(), v.GetExpiredMs())
   308  	}
   309  	<-ctx.Done()
   310  }
   311  
   312  func BenchmarkNewTimingWheels_AfterFunc(b *testing.B) {
   313  	ctx := context.Background()
   314  	ctx = context.WithValue(ctx, disableTimingWheelsScheduleCancelTask, true)
   315  	ctx = context.WithValue(ctx, disableTimingWheelsScheduleExpiredSlot, true)
   316  	ctx = context.WithValue(ctx, disableTimingWheelsSchedulePoll, true)
   317  	tw := NewTimingWheels(
   318  		ctx,
   319  		time.Now().UTC().UnixMilli(),
   320  		WithTimingWheelTickMs(1),
   321  		WithTimingWheelSlotSize(20),
   322  	)
   323  	defer tw.Shutdown()
   324  	b.ResetTimer()
   325  	for i := 0; i < b.N; i++ {
   326  		_, err := tw.AfterFunc(time.Duration(i+1)*time.Millisecond, func(ctx context.Context, md JobMetadata) {
   327  		})
   328  		assert.NoError(b, err)
   329  	}
   330  	b.ReportAllocs()
   331  }