@@ -11,6 +11,7 @@ import (
1111 "github.com/stretchr/testify/require"
1212 "go.uber.org/fx/fxtest"
1313
14+ "github.com/uber/cadence/common/clock"
1415 "github.com/uber/cadence/common/log/testlogger"
1516 "github.com/uber/cadence/common/types"
1617 "github.com/uber/cadence/service/sharddistributor/store"
@@ -178,6 +179,66 @@ func TestRecordHeartbeatSkipsShardStatisticsWithNilReport(t *testing.T) {
178179 assert .NotContains (t , nsState .ShardStats , skippedShardID )
179180}
180181
182+ func TestRecordHeartbeatShardStatisticsThrottlesWrites (t * testing.T ) {
183+ tc := testhelper .SetupStoreTestCluster (t )
184+ tc .LeaderCfg .Process .HeartbeatTTL = 10 * time .Second
185+ mockTS := clock .NewMockedTimeSourceAt (time .Unix (1000 , 0 ))
186+ executorStore := createStoreWithTimeSource (t , tc , mockTS )
187+ esImpl , ok := executorStore .(* executorStoreImpl )
188+ require .True (t , ok , "unexpected store implementation" )
189+
190+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
191+ defer cancel ()
192+
193+ executorID := "executor-shard-stats-throttle"
194+ shardID := "shard-stats-throttle"
195+
196+ baseLoad := 0.40
197+ smallDelta := shardStatsEpsilon / 2
198+ intervalSeconds := esImpl .maxStatsPersistIntervalSeconds
199+ halfIntervalSeconds := intervalSeconds / 2
200+ if halfIntervalSeconds == 0 {
201+ halfIntervalSeconds = 1
202+ }
203+
204+ // First heartbeat should always persist stats.
205+ require .NoError (t , executorStore .RecordHeartbeat (ctx , tc .Namespace , executorID , store.HeartbeatState {
206+ LastHeartbeat : mockTS .Now ().Unix (),
207+ Status : types .ExecutorStatusACTIVE ,
208+ ReportedShards : map [string ]* types.ShardStatusReport {
209+ shardID : {Status : types .ShardStatusREADY , ShardLoad : baseLoad },
210+ },
211+ }))
212+ statsAfterFirst := getShardStats (t , executorStore , ctx , tc .Namespace , shardID )
213+ require .NotNil (t , statsAfterFirst )
214+
215+ // Advance time by less than the persist interval and provide a small delta: should skip the write.
216+ mockTS .Advance (time .Duration (halfIntervalSeconds ) * time .Second )
217+ require .NoError (t , executorStore .RecordHeartbeat (ctx , tc .Namespace , executorID , store.HeartbeatState {
218+ LastHeartbeat : mockTS .Now ().Unix (),
219+ Status : types .ExecutorStatusACTIVE ,
220+ ReportedShards : map [string ]* types.ShardStatusReport {
221+ shardID : {Status : types .ShardStatusREADY , ShardLoad : baseLoad + smallDelta },
222+ },
223+ }))
224+ statsAfterSkip := getShardStats (t , executorStore , ctx , tc .Namespace , shardID )
225+ require .NotNil (t , statsAfterSkip )
226+ assert .Equal (t , statsAfterFirst .LastUpdateTime , statsAfterSkip .LastUpdateTime , "small recent deltas should not trigger a persist" )
227+
228+ // Advance time beyond the max persist interval, even small deltas should now persist.
229+ mockTS .Advance (time .Duration (intervalSeconds ) * time .Second )
230+ require .NoError (t , executorStore .RecordHeartbeat (ctx , tc .Namespace , executorID , store.HeartbeatState {
231+ LastHeartbeat : mockTS .Now ().Unix (),
232+ Status : types .ExecutorStatusACTIVE ,
233+ ReportedShards : map [string ]* types.ShardStatusReport {
234+ shardID : {Status : types .ShardStatusREADY , ShardLoad : baseLoad + smallDelta / 2 },
235+ },
236+ }))
237+ statsAfterForce := getShardStats (t , executorStore , ctx , tc .Namespace , shardID )
238+ require .NotNil (t , statsAfterForce )
239+ assert .Greater (t , statsAfterForce .LastUpdateTime , statsAfterSkip .LastUpdateTime , "stale stats must be refreshed even if delta is small" )
240+ }
241+
181242func TestGetHeartbeat (t * testing.T ) {
182243 tc := testhelper .SetupStoreTestCluster (t )
183244 executorStore := createStore (t , tc )
@@ -695,3 +756,27 @@ func createStore(t *testing.T, tc *testhelper.StoreTestCluster) store.Store {
695756 require .NoError (t , err )
696757 return store
697758}
759+
760+ func createStoreWithTimeSource (t * testing.T , tc * testhelper.StoreTestCluster , ts clock.TimeSource ) store.Store {
761+ t .Helper ()
762+ store , err := NewStore (ExecutorStoreParams {
763+ Client : tc .Client ,
764+ Cfg : tc .LeaderCfg ,
765+ Lifecycle : fxtest .NewLifecycle (t ),
766+ Logger : testlogger .New (t ),
767+ TimeSource : ts ,
768+ })
769+ require .NoError (t , err )
770+ return store
771+ }
772+
773+ func getShardStats (t * testing.T , s store.Store , ctx context.Context , namespace , shardID string ) * store.ShardStatistics {
774+ t .Helper ()
775+ nsState , err := s .GetState (ctx , namespace )
776+ require .NoError (t , err )
777+ stats , ok := nsState .ShardStats [shardID ]
778+ if ! ok {
779+ return nil
780+ }
781+ return & stats
782+ }
0 commit comments