github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/ctrlloop/impl_test.go (about)

     1  /*
     2  * Copyright (c) 2023-present unTill Pro, Ltd.
     3  * @author Alisher Nurmanov
     4   */
     5  
     6  package ctrlloop
     7  
     8  import (
     9  	"container/list"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	"github.com/voedger/voedger/pkg/goutils/logger"
    17  )
    18  
    19  func Test_BasicUsage(t *testing.T) {
    20  	logger.SetLogLevel(logger.LogLevelVerbose)
    21  
    22  	mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time {
    23  		return nowTime
    24  	}
    25  
    26  	nextStartTimeFunc = mockGetNextTimeFunc
    27  
    28  	tests := []struct {
    29  		name                string
    30  		numReportedMessages int
    31  		controller          ControllerFunction[string, int, string, int]
    32  		messages            []ControlMessage[string, int]
    33  	}{
    34  		{
    35  			name:                "3 messages:A->B->C",
    36  			numReportedMessages: 3,
    37  			controller: func(key string, sp int, state string) (newState *string, pv *int, startTime *time.Time) {
    38  				logger.Verbose("controllerFunc")
    39  				v := 1
    40  				pv = &v
    41  				return nil, pv, nil
    42  			},
    43  			messages: []ControlMessage[string, int]{
    44  				{
    45  					Key:                `A`,
    46  					SP:                 0,
    47  					CronSchedule:       `*/1 * * * *`,
    48  					StartTimeTolerance: 5 * time.Second,
    49  				},
    50  				{
    51  					Key:                `B`,
    52  					SP:                 1,
    53  					CronSchedule:       `now`,
    54  					StartTimeTolerance: 5 * time.Second,
    55  				},
    56  				{
    57  					Key:                `C`,
    58  					SP:                 2,
    59  					CronSchedule:       `*/1 * * * *`,
    60  					StartTimeTolerance: 5 * time.Second,
    61  				},
    62  			},
    63  		},
    64  	}
    65  
    66  	for _, test := range tests {
    67  		t.Run(test.name, func(t *testing.T) {
    68  			wg := sync.WaitGroup{}
    69  
    70  			mtx := sync.Mutex{}
    71  			reportDB := make([]struct {
    72  				Key string
    73  				PV  *int
    74  			}, 0)
    75  
    76  			reporterFunc := func(key string, pv *int) (err error) {
    77  				mtx.Lock()
    78  				defer mtx.Unlock()
    79  
    80  				logger.Verbose("reporterFunc")
    81  
    82  				defer wg.Done()
    83  				reportDB = append(reportDB, struct {
    84  					Key string
    85  					PV  *int
    86  				}{Key: key, PV: pv})
    87  				return nil
    88  			}
    89  
    90  			inCh := make(chan ControlMessage[string, int])
    91  
    92  			waitFunc := New(test.controller, reporterFunc, 5, inCh, time.Now)
    93  
    94  			wg.Add(test.numReportedMessages)
    95  
    96  			for _, m := range test.messages {
    97  				inCh <- ControlMessage[string, int]{
    98  					Key:                m.Key,
    99  					SP:                 m.SP,
   100  					CronSchedule:       m.CronSchedule,
   101  					StartTimeTolerance: m.StartTimeTolerance,
   102  				}
   103  			}
   104  
   105  			wg.Wait()
   106  
   107  			close(inCh)
   108  
   109  			waitFunc()
   110  
   111  			assert.GreaterOrEqual(t, test.numReportedMessages, len(reportDB))
   112  		})
   113  	}
   114  }
   115  
   116  // nolint
   117  func Test_SchedulerOnIn(t *testing.T) {
   118  	alwaysNowFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time {
   119  		return nowTime
   120  	}
   121  
   122  	var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location())
   123  
   124  	tests := []struct {
   125  		name                    string
   126  		originalMessages        []ControlMessage[string, int]
   127  		scheduledItems          []scheduledMessage[string, int, struct{}]
   128  		nextStartTimeFunc       nextStartTimeFunction
   129  		nowTime                 time.Time
   130  		expectedResultKeys      []string
   131  		expectedMaxSerialNumber uint64
   132  		expectedTopStartTime    time.Time
   133  	}{
   134  		{
   135  			name: `2 keys`,
   136  			originalMessages: []ControlMessage[string, int]{
   137  				{
   138  					Key: `A`,
   139  					SP:  0,
   140  				},
   141  				{
   142  					Key: `B`,
   143  					SP:  4,
   144  				},
   145  			},
   146  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   147  			nextStartTimeFunc:       alwaysNowFunc,
   148  			nowTime:                 testNowTime,
   149  			expectedResultKeys:      []string{`A`, `B`},
   150  			expectedMaxSerialNumber: 1,
   151  			expectedTopStartTime:    testNowTime,
   152  		},
   153  		{
   154  			name: `2 keys, 1 key in ScheduledItems`,
   155  			originalMessages: []ControlMessage[string, int]{
   156  				{
   157  					Key: `A`,
   158  					SP:  0,
   159  				},
   160  				{
   161  					Key: `B`,
   162  					SP:  4,
   163  				},
   164  			},
   165  			scheduledItems: []scheduledMessage[string, int, struct{}]{
   166  				{
   167  					Key:          `A`,
   168  					SP:           0,
   169  					serialNumber: 10,
   170  					StartTime:    testNowTime,
   171  				},
   172  			},
   173  			nextStartTimeFunc:       alwaysNowFunc,
   174  			nowTime:                 testNowTime,
   175  			expectedResultKeys:      []string{`A`, `B`},
   176  			expectedMaxSerialNumber: 10,
   177  			expectedTopStartTime:    testNowTime,
   178  		},
   179  		{
   180  			name: `invalid CronSchedule`,
   181  			originalMessages: []ControlMessage[string, int]{
   182  				{
   183  					Key:          `A`,
   184  					SP:           0,
   185  					CronSchedule: `QWERTY`,
   186  				},
   187  			},
   188  			nowTime:                 testNowTime,
   189  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   190  			nextStartTimeFunc:       getNextStartTime,
   191  			expectedResultKeys:      []string{`A`},
   192  			expectedMaxSerialNumber: 0,
   193  			expectedTopStartTime:    testNowTime,
   194  		},
   195  		{
   196  			name: `CronSchedule * 1 * * *, tolerance zero`,
   197  			originalMessages: []ControlMessage[string, int]{
   198  				{
   199  					Key:          `A`,
   200  					SP:           0,
   201  					CronSchedule: `* 1 * * *`,
   202  				},
   203  			},
   204  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   205  			nextStartTimeFunc:       getNextStartTime,
   206  			nowTime:                 testNowTime,
   207  			expectedResultKeys:      []string{`A`},
   208  			expectedMaxSerialNumber: 0,
   209  			expectedTopStartTime:    testNowTime.Add(1 * time.Hour),
   210  		},
   211  		{
   212  			name: `CronSchedule 0 0 * * *, tolerance 5 min`,
   213  			originalMessages: []ControlMessage[string, int]{
   214  				{
   215  					Key:                `A`,
   216  					SP:                 0,
   217  					CronSchedule:       `0 0 * * *`,
   218  					StartTimeTolerance: 5 * time.Minute,
   219  				},
   220  			},
   221  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   222  			nextStartTimeFunc:       getNextStartTime,
   223  			nowTime:                 testNowTime.Add(299 * time.Second),
   224  			expectedResultKeys:      []string{`A`},
   225  			expectedMaxSerialNumber: 0,
   226  			expectedTopStartTime:    testNowTime,
   227  		},
   228  		{
   229  			name: `CronSchedule 0 0 * * *, tolerance 5 min, 1 second delay`,
   230  			originalMessages: []ControlMessage[string, int]{
   231  				{
   232  					Key:                `A`,
   233  					SP:                 0,
   234  					CronSchedule:       `0 0 * * *`,
   235  					StartTimeTolerance: 5 * time.Minute,
   236  				},
   237  			},
   238  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   239  			nextStartTimeFunc:       getNextStartTime,
   240  			nowTime:                 testNowTime.Add(301 * time.Second),
   241  			expectedResultKeys:      []string{`A`},
   242  			expectedMaxSerialNumber: 0,
   243  			expectedTopStartTime:    testNowTime.Add(24 * time.Hour),
   244  		},
   245  		{
   246  			name: `the second message scheduled before the first one`,
   247  			originalMessages: []ControlMessage[string, int]{
   248  				{
   249  					Key:          `A`,
   250  					SP:           0,
   251  					CronSchedule: `0 11 * * *`,
   252  				},
   253  				{
   254  					Key:          `B`,
   255  					SP:           4,
   256  					CronSchedule: `0 10 * * *`,
   257  				},
   258  			},
   259  			scheduledItems:          []scheduledMessage[string, int, struct{}]{},
   260  			nextStartTimeFunc:       getNextStartTime,
   261  			nowTime:                 testNowTime,
   262  			expectedResultKeys:      []string{`B`, `A`},
   263  			expectedMaxSerialNumber: 1,
   264  			expectedTopStartTime:    testNowTime.Add(10 * time.Hour),
   265  		},
   266  	}
   267  
   268  	for _, test := range tests {
   269  		t.Run(test.name, func(t *testing.T) {
   270  			nextStartTimeFunc = test.nextStartTimeFunc
   271  
   272  			initList := list.New()
   273  			for _, i := range test.scheduledItems {
   274  				initList.PushBack(i)
   275  			}
   276  
   277  			schedulerObj := newScheduler[string, int, struct{}](initList)
   278  
   279  			for i, m := range test.originalMessages {
   280  				schedulerObj.OnIn(uint64(i), m, test.nowTime)
   281  			}
   282  
   283  			maxSerialNumber := uint64(0)
   284  			resultKeys := make([]string, 0)
   285  			for element := schedulerObj.scheduledItems.Front(); element != nil; element = element.Next() {
   286  				m := element.Value.(scheduledMessage[string, int, struct{}])
   287  				if m.serialNumber > maxSerialNumber {
   288  					maxSerialNumber = m.serialNumber
   289  				}
   290  				resultKeys = append(resultKeys, m.Key)
   291  			}
   292  
   293  			top := schedulerObj.scheduledItems.Front().Value.(scheduledMessage[string, int, struct{}])
   294  			require.Equal(t, test.expectedTopStartTime, top.StartTime)
   295  			require.Equal(t, test.expectedResultKeys, resultKeys)
   296  			require.Equal(t, test.expectedMaxSerialNumber, maxSerialNumber)
   297  		})
   298  	}
   299  }
   300  
   301  func Test_SchedulerOnRepeat(t *testing.T) {
   302  	var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location())
   303  
   304  	tests := []struct {
   305  		name                    string
   306  		messagesToRepeat        []scheduledMessage[string, int, struct{}]
   307  		scheduledItems          []scheduledMessage[string, int, struct{}]
   308  		expectedScheduledKeys   []string
   309  		expectedMaxSerialNumber uint64
   310  	}{
   311  		{
   312  			name: `fresh serial number`,
   313  			messagesToRepeat: []scheduledMessage[string, int, struct{}]{
   314  				{
   315  					Key:          `A`,
   316  					SP:           0,
   317  					serialNumber: 1,
   318  					StartTime:    testNowTime,
   319  				},
   320  				{
   321  					Key:          `A`,
   322  					SP:           1,
   323  					serialNumber: 2,
   324  					StartTime:    testNowTime.Add(5 * time.Second),
   325  				},
   326  			},
   327  			expectedScheduledKeys:   []string{`A`},
   328  			expectedMaxSerialNumber: 2,
   329  		},
   330  		{
   331  			name: `obsoleted serial number`,
   332  			messagesToRepeat: []scheduledMessage[string, int, struct{}]{
   333  				{
   334  					Key:          `A`,
   335  					SP:           0,
   336  					serialNumber: 2,
   337  					StartTime:    testNowTime,
   338  				},
   339  				{
   340  					Key:          `A`,
   341  					SP:           1,
   342  					serialNumber: 1,
   343  					StartTime:    testNowTime.Add(5 * time.Second),
   344  				},
   345  			},
   346  			expectedScheduledKeys:   []string{`A`},
   347  			expectedMaxSerialNumber: 2,
   348  		},
   349  	}
   350  
   351  	for _, test := range tests {
   352  		t.Run(test.name, func(t *testing.T) {
   353  			initList := list.New()
   354  			schedulerObj := newScheduler[string, int, struct{}](initList)
   355  
   356  			for _, m := range test.messagesToRepeat {
   357  				schedulerObj.OnRepeat(m, time.Now())
   358  			}
   359  
   360  			maxSerialNumber := uint64(0)
   361  			resultKeys := make([]string, 0)
   362  			for element := schedulerObj.scheduledItems.Front(); element != nil; element = element.Next() {
   363  				m := element.Value.(scheduledMessage[string, int, struct{}])
   364  				if m.serialNumber > maxSerialNumber {
   365  					maxSerialNumber = m.serialNumber
   366  				}
   367  				resultKeys = append(resultKeys, m.Key)
   368  			}
   369  
   370  			require.Equal(t, test.expectedScheduledKeys, resultKeys)
   371  			require.Equal(t, test.expectedMaxSerialNumber, maxSerialNumber)
   372  		})
   373  	}
   374  }
   375  
   376  func Test_SchedulerOnTimer(t *testing.T) {
   377  	var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location())
   378  
   379  	t.Run(`empty scheduledItems`, func(t *testing.T) {
   380  		dedupInCh := make(chan statefulMessage[string, int, struct{}], 10)
   381  
   382  		schedulerObj := newScheduler[string, int, struct{}](nil)
   383  		schedulerObj.OnTimer(dedupInCh, time.Now())
   384  		schedulerObj.OnTimer(dedupInCh, time.Now())
   385  
   386  		messagesToDedupIn := testMessagesReader(dedupInCh)
   387  
   388  		// closing channels
   389  		close(dedupInCh)
   390  
   391  		require.Empty(t, messagesToDedupIn)
   392  	})
   393  
   394  	t.Run(`2 scheduled items`, func(t *testing.T) {
   395  		dedupInCh := make(chan statefulMessage[string, int, struct{}], 10)
   396  
   397  		scheduledItems := []scheduledMessage[string, int, struct{}]{
   398  			{
   399  				Key:          `A`,
   400  				SP:           0,
   401  				serialNumber: 1,
   402  				StartTime:    testNowTime.Add(5 * time.Second),
   403  			},
   404  			{
   405  				Key:          `B`,
   406  				SP:           1,
   407  				serialNumber: 2,
   408  				StartTime:    testNowTime.Add(3 * time.Second),
   409  			},
   410  		}
   411  
   412  		schedulerObj := newScheduler[string, int, struct{}](nil)
   413  		// fulfilling ScheduledItems storage
   414  		for _, m := range scheduledItems {
   415  			schedulerObj.OnRepeat(m, testNowTime)
   416  		}
   417  		require.Equal(t, 3*time.Second, schedulerObj.lastDuration)
   418  
   419  		schedulerObj.OnTimer(dedupInCh, testNowTime)
   420  		require.Equal(t, 5*time.Second, schedulerObj.lastDuration)
   421  
   422  		// closing channels
   423  		close(dedupInCh)
   424  	})
   425  
   426  	t.Run(`dedupIn channel is busy`, func(t *testing.T) {
   427  		dedupInCh := make(chan statefulMessage[string, int, struct{}], 1)
   428  
   429  		scheduledItems := []scheduledMessage[string, int, struct{}]{
   430  			{
   431  				Key:          `A`,
   432  				SP:           0,
   433  				serialNumber: 1,
   434  				StartTime:    testNowTime.Add(5 * time.Second),
   435  			},
   436  		}
   437  
   438  		schedulerObj := newScheduler[string, int, struct{}](nil)
   439  		// fulfilling ScheduledItems storage
   440  		for _, m := range scheduledItems {
   441  			schedulerObj.OnRepeat(m, testNowTime)
   442  		}
   443  		require.Equal(t, 5*time.Second, schedulerObj.lastDuration)
   444  
   445  		// make dedupInCh busy
   446  		dedupInCh <- statefulMessage[string, int, struct{}]{
   447  			Key:          `B`,
   448  			SP:           1,
   449  			serialNumber: 2,
   450  		}
   451  		schedulerObj.OnTimer(dedupInCh, testNowTime)
   452  		require.Equal(t, DedupInRetryInterval, schedulerObj.lastDuration)
   453  
   454  		// closing channels
   455  		close(dedupInCh)
   456  	})
   457  }
   458  
   459  func Test_Dedupin(t *testing.T) {
   460  	mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time {
   461  		return nowTime
   462  	}
   463  
   464  	nextStartTimeFunc = mockGetNextTimeFunc
   465  
   466  	tests := []struct {
   467  		name          string
   468  		inProcessKeys []string
   469  		messages      []statefulMessage[string, int, struct{}]
   470  	}{
   471  		{
   472  			name:          `2 keys are duplicated in 4 messages`,
   473  			inProcessKeys: []string{``},
   474  			messages: []statefulMessage[string, int, struct{}]{
   475  				{
   476  					Key:          `A`,
   477  					SP:           0,
   478  					serialNumber: 1,
   479  				},
   480  				{
   481  					Key:          `B`,
   482  					SP:           1,
   483  					serialNumber: 1,
   484  				},
   485  				{
   486  					Key:          `B`,
   487  					SP:           2,
   488  					serialNumber: 1,
   489  				},
   490  				{
   491  					Key:          `C`,
   492  					SP:           2,
   493  					serialNumber: 1,
   494  				},
   495  			},
   496  		},
   497  	}
   498  
   499  	for _, test := range tests {
   500  		t.Run(test.name, func(t *testing.T) {
   501  			InProcess := sync.Map{}
   502  			dedupInCh := make(chan statefulMessage[string, int, struct{}])
   503  			callerCh := make(chan statefulMessage[string, int, struct{}], 10)
   504  			repeatCh := make(chan scheduledMessage[string, int, struct{}], 10)
   505  
   506  			var messagesToCall []statefulMessage[string, int, struct{}]
   507  			var messagesToRepeat []scheduledMessage[string, int, struct{}]
   508  			var inProcessKeyCounter int
   509  			go func() {
   510  				testMessagesWriter(dedupInCh, test.messages)
   511  
   512  				// closing channels
   513  				close(dedupInCh)
   514  				close(repeatCh)
   515  			}()
   516  
   517  			dedupIn(dedupInCh, callerCh, repeatCh, &InProcess, time.Now)
   518  
   519  			messagesToCall = testMessagesReader(callerCh)
   520  			messagesToRepeat = testMessagesReader(repeatCh)
   521  
   522  			inProcessKeyCounter = 0
   523  			InProcess.Range(func(_, _ any) bool {
   524  				inProcessKeyCounter++
   525  				return true
   526  			})
   527  
   528  			require.Len(t, messagesToCall, inProcessKeyCounter)
   529  			require.Len(t, messagesToRepeat, 1)
   530  		})
   531  	}
   532  }
   533  
   534  func Test_Repeater(t *testing.T) {
   535  	mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time {
   536  		return nowTime
   537  	}
   538  
   539  	nextStartTimeFunc = mockGetNextTimeFunc
   540  
   541  	now := time.Now()
   542  	pv := 1
   543  
   544  	tests := []struct {
   545  		name          string
   546  		inProcessKeys []string
   547  		messages      []answer[string, int, int, struct{}]
   548  	}{
   549  		{
   550  			name:          `2 messages to report, 2 messages to repeat`,
   551  			inProcessKeys: []string{``},
   552  			messages: []answer[string, int, int, struct{}]{
   553  				{
   554  					Key:          `A`,
   555  					SP:           0,
   556  					serialNumber: 1,
   557  					StartTime:    &now,
   558  					PV:           nil,
   559  				},
   560  				{
   561  					Key:          `B`,
   562  					SP:           1,
   563  					serialNumber: 1,
   564  					StartTime:    &now,
   565  					PV:           nil,
   566  				},
   567  				{
   568  					Key:          `C`,
   569  					SP:           2,
   570  					serialNumber: 1,
   571  					StartTime:    nil,
   572  					PV:           &pv,
   573  				},
   574  				{
   575  					Key:          `D`,
   576  					SP:           2,
   577  					serialNumber: 1,
   578  					StartTime:    nil,
   579  					PV:           &pv,
   580  				},
   581  			},
   582  		},
   583  	}
   584  
   585  	for _, test := range tests {
   586  		t.Run(test.name, func(t *testing.T) {
   587  			repeaterCh := make(chan answer[string, int, int, struct{}])
   588  			repeatCh := make(chan scheduledMessage[string, int, struct{}], 10)
   589  			reporterCh := make(chan reportInfo[string, int], 10)
   590  
   591  			var messagesToReport []reportInfo[string, int]
   592  			var messagesToRepeat []scheduledMessage[string, int, struct{}]
   593  			go func() {
   594  				testMessagesWriter(repeaterCh, test.messages)
   595  
   596  				close(repeaterCh)
   597  			}()
   598  
   599  			repeater(repeaterCh, repeatCh, reporterCh)
   600  
   601  			messagesToReport = testMessagesReader(reporterCh)
   602  			messagesToRepeat = testMessagesReader(repeatCh)
   603  
   604  			require.Len(t, messagesToReport, 2)
   605  			require.Len(t, messagesToRepeat, 2)
   606  		})
   607  	}
   608  }
   609  
   610  func testMessagesWriter[T any](ch chan<- T, arr []T) {
   611  	for _, m := range arr {
   612  		ch <- m
   613  	}
   614  }
   615  
   616  func testMessagesReader[T any](ch <-chan T) []T {
   617  	results := make([]T, 0)
   618  
   619  	var val T
   620  	ok := true
   621  	for ok {
   622  		select {
   623  		case val, ok = <-ch:
   624  			if ok {
   625  				results = append(results, val)
   626  			} else {
   627  				return results
   628  			}
   629  		default:
   630  			return results
   631  		}
   632  	}
   633  	return results
   634  }