It appears that events for proxy check requests are not updated each interval. Rather, the splayed executions are not scheduled until _after_ the whole interval.
60 second check interval, 2 proxy entities, 50% splay:
60 second check interval, 2 proxy entities, 50% splay:
Splayed executions should be scheduled immediately, not after the original interval.
{
"type": "Entity",
"api_version": "core/v2",
"metadata": {
"name": "proxy1",
"namespace": "default"
},
"spec": {
"deregister": false,
"deregistration": {},
"entity_class": "proxy",
"last_seen": 0,
"subscriptions": null,
"system": {
"network": {
"interfaces": null
}
}
}
}
{
"type": "Entity",
"api_version": "core/v2",
"metadata": {
"name": "proxy2",
"namespace": "default"
},
"spec": {
"deregister": false,
"deregistration": {},
"entity_class": "proxy",
"last_seen": 0,
"subscriptions": null,
"system": {
"network": {
"interfaces": null
}
}
}
}
{
"type": "CheckConfig",
"api_version": "core/v2",
"metadata": {
"name": "check-proxy",
"namespace": "default"
},
"spec": {
"check_hooks": null,
"command": "echo hi",
"env_vars": null,
"handlers": [],
"high_flap_threshold": 0,
"interval": 60,
"low_flap_threshold": 0,
"output_metric_format": "",
"output_metric_handlers": null,
"proxy_entity_name": "",
"proxy_requests": {
"entity_attributes": [
"entity.entity_class == 'proxy'"
],
"splay": true,
"splay_coverage": 50
},
"publish": true,
"round_robin": false,
"runtime_assets": null,
"stdin": false,
"subdue": null,
"subscriptions": [
"proxy"
],
"timeout": 0,
"ttl": 0
}
}
--subscriptions proxy)sensuctl event info proxy1 check-proxy --format json | jq .check.historysensuctl event info proxy2 check-proxy --format json | jq .check.historyWith the check splay and proxy entity configurations, each check should execute every 60 seconds (and 15 seconds apart from each other: 60 second interval * 50% / 2 entities = 15 second splay). The current behavior is not at parity with 1.x or docs.
Reported by AdamskiEM/@adammcdonagh in the community slack.
We should try first to reproduce and detect this issue in the our automated testing!
@palourde The automated test is flaky at best. But it only validates the splay between two different checks, rather than additionally validating the proper interval in the check history. I'll add a note to https://github.com/sensu/sensu-go-qa-crucible/issues/41 about this.
I noticed this recently, and did some digging. I believe I have found the root cause of this, and figured I should provide details to make it easier for whoever ends up fixing this.
https://github.com/sensu/sensu-go/blob/27ad8a3db009abfee339de2ad649a7f74266291f/backend/schedulerd/interval_scheduler.go#L47-L60
This is where the logic controlling the scheduling and the resulting wait occur. The defer means that the timer isn't reset until the check has been published, which is probably a bug in general, but I suspect the time taken to publish a single check isn't long enough to notice this in the non-proxy case.
Basically, what the defer results in (as described in the issue above) is the check schedule time drifting by approximately the amount of time it takes for executor.processCheck on line 57 to complete.
This executor.processCheck call ultimately ends up here:
https://github.com/sensu/sensu-go/blob/27ad8a3db009abfee339de2ad649a7f74266291f/backend/schedulerd/executor.go#L248-L257
Where you can see that we sleep for splay * numEntities number of seconds, and since splay is calculated as splay_coverage / 100.0 * interval / num_entities this results in check scheduling drifting by approximately interval * splay_percentage every time the timer fires.
A simple solution is to remove the defer so that the timer is reset immediately, though I'm not sure whether that creates a condition where the a timer could fire for a check that is still being scheduled (say with a splay_coverage of 100%). Perhaps a more robust solution would be to time the function internally and reset the timer to interval - func_time, eg:
func (s *IntervalScheduler) schedule(timer CheckTimer, executor *CheckExecutor) {
start := time.Now()
defer func() {
elapsed := time.Since(start)
s.resetTimerLess(timer, elapsed)
}
...
}
Where resetTimerLess resets the timer to min(0, s.check.Interval - elapsed). resetTimer doesn't appear to be used anywhere else though, so perhaps it's ok to break that existing interface to add the elapsed arg.
I'm happy to draft a PR if we can agree on an approach.
I like the idea of resetting the timer immediately. Since this function is executed in the same goroutine as the select that consumes the timer, it should be safe, and produce a more accurate result. Thanks @roganartu
Most helpful comment
I noticed this recently, and did some digging. I believe I have found the root cause of this, and figured I should provide details to make it easier for whoever ends up fixing this.
https://github.com/sensu/sensu-go/blob/27ad8a3db009abfee339de2ad649a7f74266291f/backend/schedulerd/interval_scheduler.go#L47-L60
This is where the logic controlling the scheduling and the resulting wait occur. The defer means that the timer isn't reset until the check has been published, which is probably a bug in general, but I suspect the time taken to publish a single check isn't long enough to notice this in the non-proxy case.
Basically, what the defer results in (as described in the issue above) is the check schedule time drifting by approximately the amount of time it takes for
executor.processCheckon line 57 to complete.This
executor.processCheckcall ultimately ends up here:https://github.com/sensu/sensu-go/blob/27ad8a3db009abfee339de2ad649a7f74266291f/backend/schedulerd/executor.go#L248-L257
Where you can see that we sleep for
splay * numEntitiesnumber of seconds, and sincesplayis calculated assplay_coverage / 100.0 * interval / num_entitiesthis results in check scheduling drifting by approximatelyinterval * splay_percentageevery time the timer fires.A simple solution is to remove the
deferso that the timer is reset immediately, though I'm not sure whether that creates a condition where the a timer could fire for a check that is still being scheduled (say with asplay_coverageof 100%). Perhaps a more robust solution would be to time the function internally and reset the timer tointerval - func_time, eg:Where
resetTimerLessresets the timer tomin(0, s.check.Interval - elapsed).resetTimerdoesn't appear to be used anywhere else though, so perhaps it's ok to break that existing interface to add the elapsed arg.I'm happy to draft a PR if we can agree on an approach.