-
Notifications
You must be signed in to change notification settings - Fork 580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: Refactor cronjob controller #3029
base: main
Are you sure you want to change the base?
Changes from all commits
f2d8bed
75020fb
bd7c7c2
33dfc92
546f791
5c7ae9a
9452deb
2d380a2
131a290
f7f4cb5
eafee0f
99c20e6
88b055c
2f4ad03
02d1fee
53e27ac
764ddf5
5ac3bbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"branches": 93.31, | ||
"functions": 97.05, | ||
"lines": 98.25, | ||
"statements": 97.98 | ||
"branches": 93.32, | ||
"functions": 97.59, | ||
"lines": 98.35, | ||
"statements": 98.08 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -152,9 +152,26 @@ export class CronjobController extends BaseController< | |
this._handleEventSnapUpdated = this._handleEventSnapUpdated.bind(this); | ||
this._handleSnapDisabledEvent = this._handleSnapDisabledEvent.bind(this); | ||
this._handleSnapEnabledEvent = this._handleSnapEnabledEvent.bind(this); | ||
|
||
// Subscribe to Snap events | ||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
this.#initializeEventListeners(); | ||
|
||
// Register action handlers | ||
this.#initializeActionHandlers(); | ||
|
||
this.dailyCheckIn().catch((error) => { | ||
/* istanbul ignore next */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added ignore statement here (and below) as this scenario is pretty difficult to replicate in our test environment. I don't think it's worth the effort to try and cover these. |
||
logError(error); | ||
}); | ||
|
||
this.#rescheduleBackgroundEvents(Object.values(this.state.events)); | ||
} | ||
|
||
/** | ||
* Initialize event listeners. | ||
*/ | ||
#initializeEventListeners() { | ||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
this.messagingSystem.subscribe( | ||
'SnapController:snapInstalled', | ||
this._handleSnapRegisterEvent, | ||
|
@@ -180,7 +197,12 @@ export class CronjobController extends BaseController< | |
this._handleEventSnapUpdated, | ||
); | ||
/* eslint-enable @typescript-eslint/unbound-method */ | ||
hmalik88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Initialize action handlers. | ||
*/ | ||
#initializeActionHandlers() { | ||
this.messagingSystem.registerActionHandler( | ||
`${controllerName}:scheduleBackgroundEvent`, | ||
(...args) => this.scheduleBackgroundEvent(...args), | ||
|
@@ -195,12 +217,64 @@ export class CronjobController extends BaseController< | |
`${controllerName}:getBackgroundEvents`, | ||
(...args) => this.getBackgroundEvents(...args), | ||
); | ||
} | ||
|
||
this.dailyCheckIn().catch((error) => { | ||
logError(error); | ||
/** | ||
* Execute a request. | ||
* | ||
* @param snapId - ID of a Snap. | ||
* @param request - Request to be executed. | ||
*/ | ||
async #executeRequest(snapId: SnapId, request: unknown) { | ||
await this.messagingSystem | ||
.call('SnapController:handleRequest', { | ||
snapId, | ||
origin: '', | ||
handler: HandlerType.OnCronjob, | ||
request: request as Record<string, unknown>, | ||
}) | ||
.catch((error) => { | ||
/* istanbul ignore next */ | ||
logError(error); | ||
}); | ||
} | ||
|
||
/** | ||
* Setup a timer. | ||
* | ||
* @param id - ID of a timer. | ||
* @param ms - Time in milliseconds. | ||
* @param snapId - ID of a Snap. | ||
* @param onComplete - Callback function to be executed when the timer completes. | ||
*/ | ||
#setupTimer(id: string, ms: number, snapId: SnapId, onComplete: () => void) { | ||
if (ms <= 0) { | ||
throw new Error('Cannot schedule execution in the past.'); | ||
} | ||
|
||
const timer = new Timer(ms); | ||
timer.start(() => { | ||
onComplete(); | ||
this.#timers.delete(id); | ||
this.#snapIds.delete(id); | ||
}); | ||
|
||
this.#rescheduleBackgroundEvents(Object.values(this.state.events)); | ||
this.#timers.set(id, timer); | ||
this.#snapIds.set(id, snapId); | ||
} | ||
|
||
/** | ||
* Cleanup a timer. | ||
* | ||
* @param id - ID of a timer. | ||
*/ | ||
#cleanupTimer(id: string) { | ||
const timer = this.#timers.get(id); | ||
if (timer) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this throw an error if the timer doesn't exist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I don't think so, this function is only called when cancelling a background event. |
||
timer.cancel(); | ||
this.#timers.delete(id); | ||
this.#snapIds.delete(id); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -273,23 +347,15 @@ export class CronjobController extends BaseController< | |
return; | ||
} | ||
|
||
const timer = new Timer(ms); | ||
timer.start(() => { | ||
this.#executeCronjob(job).catch((error) => { | ||
// TODO: Decide how to handle errors. | ||
logError(error); | ||
}); | ||
|
||
this.#timers.delete(job.id); | ||
this.#setupTimer(job.id, ms, job.snapId, () => { | ||
// TODO: Decide how to handle errors. | ||
this.#executeCronjob(job).catch(logError); | ||
this.#schedule(job); | ||
}); | ||
|
||
if (!this.state.jobs[job.id]?.lastRun) { | ||
this.#updateJobLastRunState(job.id, 0); // 0 for init, never ran actually | ||
} | ||
|
||
this.#timers.set(job.id, timer); | ||
this.#snapIds.set(job.id, job.snapId); | ||
} | ||
|
||
/** | ||
|
@@ -299,12 +365,7 @@ export class CronjobController extends BaseController< | |
*/ | ||
async #executeCronjob(job: Cronjob) { | ||
this.#updateJobLastRunState(job.id, Date.now()); | ||
await this.messagingSystem.call('SnapController:handleRequest', { | ||
snapId: job.snapId, | ||
origin: '', | ||
handler: HandlerType.OnCronjob, | ||
request: job.request, | ||
}); | ||
await this.#executeRequest(job.snapId, job.request); | ||
} | ||
|
||
/** | ||
|
@@ -358,10 +419,7 @@ export class CronjobController extends BaseController< | |
'Only the origin that scheduled this event can cancel it.', | ||
); | ||
|
||
const timer = this.#timers.get(id); | ||
timer?.cancel(); | ||
this.#timers.delete(id); | ||
this.#snapIds.delete(id); | ||
this.#cleanupTimer(id); | ||
this.update((state) => { | ||
delete state.events[id]; | ||
}); | ||
|
@@ -377,32 +435,12 @@ export class CronjobController extends BaseController< | |
const now = new Date(); | ||
const ms = date.getTime() - now.getTime(); | ||
|
||
if (ms <= 0) { | ||
throw new Error('Cannot schedule an event in the past.'); | ||
} | ||
|
||
const timer = new Timer(ms); | ||
timer.start(() => { | ||
this.messagingSystem | ||
.call('SnapController:handleRequest', { | ||
snapId: event.snapId, | ||
origin: '', | ||
handler: HandlerType.OnCronjob, | ||
request: event.request, | ||
}) | ||
.catch((error) => { | ||
logError(error); | ||
}); | ||
|
||
this.#timers.delete(event.id); | ||
this.#snapIds.delete(event.id); | ||
this.#setupTimer(event.id, ms, event.snapId, () => { | ||
this.#executeRequest(event.snapId, event.request).catch(logError); | ||
this.update((state) => { | ||
delete state.events[event.id]; | ||
}); | ||
}); | ||
|
||
this.#timers.set(event.id, timer); | ||
this.#snapIds.set(event.id, event.snapId); | ||
} | ||
|
||
/** | ||
|
@@ -493,7 +531,7 @@ export class CronjobController extends BaseController< | |
this.#dailyTimer = new Timer(DAILY_TIMEOUT); | ||
this.#dailyTimer.start(() => { | ||
this.dailyCheckIn().catch((error) => { | ||
// TODO: Decide how to handle errors. | ||
/* istanbul ignore next */ | ||
logError(error); | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed this in detail. but generally when I proposed we refactor the
CronjobController
, the main goal I had in mind was to unify the logic used to manage background events and cronjobs.We currently treat these as entirely separate entities, but I don't think they need to be. My general thought was that we should investigate treating them as one entity with the only variation being non-recurring versus recurring cronjobs (or background events - no strong opinion on the name).
I had intended for the referenced ticket to be to investigate ways to achieve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR unifies some logic, but I inherently found it hard to make a mental model of both being the same thing because the term non-recurring cronjob in of itself is an oxymoron. Maybe there's a better term to encompass both things. One divergence is that we fetch the cron jobs every time from the manifest, maybe if we can fetch once and unify the state then some of the other logic itself can be refactored. We can keep the original ticket open, I think this is an improvement nonetheless.