-
Notifications
You must be signed in to change notification settings - Fork 0
Add a cleanup mechanism for old db entries #104
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
base: main
Are you sure you want to change the base?
Changes from 8 commits
d261744
12d007a
9886d7f
481b54b
2edce18
fce4f55
89195be
6511b0a
9c9b49e
742aa3d
338636c
8852cc5
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -168,9 +168,68 @@ describe('Flottform database', () => { | |||||
| session: answer, | ||||||
| iceCandidates: [] | ||||||
| }) | ||||||
| ).rejects.toThrow(/peerkey/i); | ||||||
| ).rejects.toThrow( | ||||||
| /clientKey is wrong: Another peer is already connected and you cannot change this info without the correct key anymore. If you lost your key, initiate a new Flottform connection./i | ||||||
| ); | ||||||
|
Comment on lines
+172
to
+174
Member
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. Since
Contributor
Author
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. Yes that test was not working since as you've said the string 'peerkey' is not used anymore and is replaced by this string: 'clientKey is wrong: Another peer is already connected and you cannot change this info without the correct key anymore. If you lost your key, initiate a new Flottform connection.' |
||||||
| const infoAfter = await db.getEndpoint({ endpointId }); | ||||||
| expect(infoBefore).toStrictEqual(infoAfter); | ||||||
| }); | ||||||
| }); | ||||||
|
|
||||||
| describe('startCleanup()', () => { | ||||||
| function sleep(ms: number) { | ||||||
|
Member
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. Just to make totally clear this expects you to wait for it, I would add this:
Suggested change
But the helper function itself could also go somewhere else and doesn't need to be inside the
Contributor
Author
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. Since I changed to fake timers like you suggested, I don't think we need it anymore! |
||||||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||||||
| } | ||||||
|
|
||||||
| it('Should clean up stale entries after entryTTL', async () => { | ||||||
| const db = await createFlottformDatabase(1000, 500); | ||||||
| const conn = new RTCPeerConnection(); | ||||||
| const offer = await conn.createOffer(); | ||||||
| const { endpointId } = await db.createEndpoint({ session: offer }); | ||||||
|
|
||||||
| const connPeer = new RTCPeerConnection(); | ||||||
| await connPeer.setRemoteDescription(offer); | ||||||
| const answer = await connPeer.createAnswer(); | ||||||
| const clientKey = 'random-key'; | ||||||
|
|
||||||
| await db.putClientInfo({ | ||||||
| endpointId, | ||||||
| clientKey, | ||||||
| session: answer, | ||||||
| iceCandidates: [], | ||||||
| lastUpdate: Date.now() | ||||||
|
Member
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. lastUpdate should be taken care of internally only - no need to set it here |
||||||
| }); | ||||||
|
|
||||||
| // Sleep for enough time to trigger the first cleanup | ||||||
| await sleep(1100); | ||||||
|
Member
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. Is it possible to use fake timers for this and not actually wait for a second? |
||||||
|
|
||||||
| // The endpoint should be cleaned by now | ||||||
| expect(async () => await db.getEndpoint({ endpointId })).rejects.toThrow(/endpoint/i); | ||||||
| }); | ||||||
|
|
||||||
| it("Shouldn't clean up entries before entryTTL is expired", async () => { | ||||||
| const db = await createFlottformDatabase(1000, 500); | ||||||
| const conn = new RTCPeerConnection(); | ||||||
| const offer = await conn.createOffer(); | ||||||
| const { endpointId } = await db.createEndpoint({ session: offer }); | ||||||
|
|
||||||
| const connPeer = new RTCPeerConnection(); | ||||||
| await connPeer.setRemoteDescription(offer); | ||||||
| const answer = await connPeer.createAnswer(); | ||||||
| const clientKey = 'random-key'; | ||||||
|
|
||||||
| await db.putClientInfo({ | ||||||
| endpointId, | ||||||
| clientKey, | ||||||
| session: answer, | ||||||
| iceCandidates: [], | ||||||
| lastUpdate: Date.now() | ||||||
|
Member
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. lastUpdate should be taken care of internally only - no need to set it here |
||||||
| }); | ||||||
|
|
||||||
| // The endpoint shouldn't be cleaned by now | ||||||
| const retrievedInfo = await db.getEndpoint({ endpointId }); | ||||||
| expect(retrievedInfo).toBeDefined(); | ||||||
| expect(retrievedInfo?.hostInfo.session).toStrictEqual(offer); | ||||||
| }); | ||||||
| }); | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,7 @@ type EndpointInfo = { | |||||||||
| session: RTCSessionDescriptionInit; | ||||||||||
| iceCandidates: RTCIceCandidateInit[]; | ||||||||||
| }; | ||||||||||
| lastUpdate: number; | ||||||||||
| }; | ||||||||||
| type SafeEndpointInfo = Omit<EndpointInfo, 'hostKey' | 'clientKey'>; | ||||||||||
|
|
||||||||||
|
|
@@ -25,8 +26,46 @@ function createRandomEndpointId(): string { | |||||||||
|
|
||||||||||
| class FlottformDatabase { | ||||||||||
| private map = new Map<EndpointId, EndpointInfo>(); | ||||||||||
| private cleanupTimeoutId: NodeJS.Timeout | null = null; | ||||||||||
| private cleanupPeriod: number; | ||||||||||
| private entryTTL: number; // Time-to-Live for each entry | ||||||||||
|
Member
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. How about |
||||||||||
|
|
||||||||||
| constructor(cleanupPeriod = 30 * 60 * 1000, entryTTL = 25 * 60 * 1000) { | ||||||||||
| this.cleanupPeriod = cleanupPeriod; | ||||||||||
| this.entryTTL = entryTTL; | ||||||||||
| this.startCleanup(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private startCleanup() { | ||||||||||
| this.cleanupTimeoutId = setTimeout(this.cleanupFn.bind(this), this.cleanupPeriod); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private cleanupFn() { | ||||||||||
| if (!this.map || this.map.size === 0) return; | ||||||||||
|
Member
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. I think if we fall down to size 0, it will never call the
Contributor
Author
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. The cleanup mechanism is designed to run periodically, regardless of whether the map is empty at the time of execution. The purpose is to ensure stale entries are regularly deleted, even if none exist during a specific cleanup cycle. This mechanism continues to run until the server hosting the database is stopped. For example, while the map might be empty during the first cleanup call, it’s possible that by the next cycle, new entries have been added, with some of them becoming stale. Stopping the cleanup when the map is empty will lead to not cleaning up stale entries in subsequent cycles. Therefore, it’s crucial that the cleanup function runs unconditionally. |
||||||||||
| const now = Date.now(); | ||||||||||
| // Loop over all entries and delete the stale ones. | ||||||||||
| for (const [endpointId, endpointInfo] of this.map) { | ||||||||||
| const lastUpdated = endpointInfo.lastUpdate; | ||||||||||
| if (now - lastUpdated > this.entryTTL) { | ||||||||||
| this.map.delete(endpointId); | ||||||||||
| console.log(`Cleaned up stale entry: ${endpointId}`); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| this.cleanupTimeoutId = setTimeout(this.startCleanup.bind(this), this.cleanupPeriod); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private stopCleanup() { | ||||||||||
| // Clear the interval to stop cleanup | ||||||||||
| if (this.cleanupTimeoutId) { | ||||||||||
| clearTimeout(this.cleanupTimeoutId); | ||||||||||
| this.cleanupTimeoutId = null; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| constructor() {} | ||||||||||
| // Stop the cleanup when the database is no longer needed | ||||||||||
| destroy() { | ||||||||||
| this.stopCleanup(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async createEndpoint({ session }: { session: RTCSessionDescriptionInit }): Promise<EndpointInfo> { | ||||||||||
| const entry = { | ||||||||||
|
|
@@ -35,7 +74,8 @@ class FlottformDatabase { | |||||||||
| hostInfo: { | ||||||||||
| session, | ||||||||||
| iceCandidates: [] | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| lastUpdate: Date.now() | ||||||||||
| }; | ||||||||||
| this.map.set(entry.endpointId, entry); | ||||||||||
| return entry; | ||||||||||
|
Member
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. This is exposing |
||||||||||
|
|
@@ -46,6 +86,7 @@ class FlottformDatabase { | |||||||||
| if (!entry) { | ||||||||||
| throw Error('Endpoint not found'); | ||||||||||
| } | ||||||||||
| entry.lastUpdate = Date.now(); | ||||||||||
| const { hostKey: _ignore1, clientKey: _ignore2, ...endpoint } = entry; | ||||||||||
|
|
||||||||||
| return endpoint; | ||||||||||
|
Member
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. The |
||||||||||
|
|
@@ -55,12 +96,14 @@ class FlottformDatabase { | |||||||||
| endpointId, | ||||||||||
| hostKey, | ||||||||||
| session, | ||||||||||
| iceCandidates | ||||||||||
| iceCandidates, | ||||||||||
| lastUpdate = Date.now() | ||||||||||
| }: { | ||||||||||
| endpointId: EndpointId; | ||||||||||
| hostKey: HostKey; | ||||||||||
| session: RTCSessionDescriptionInit; | ||||||||||
| iceCandidates: RTCIceCandidateInit[]; | ||||||||||
| lastUpdate?: number; | ||||||||||
| }): Promise<SafeEndpointInfo> { | ||||||||||
| const existingSession = this.map.get(endpointId); | ||||||||||
| if (!existingSession) { | ||||||||||
|
|
@@ -72,7 +115,8 @@ class FlottformDatabase { | |||||||||
|
|
||||||||||
| const newInfo = { | ||||||||||
| ...existingSession, | ||||||||||
| hostInfo: { ...existingSession.hostInfo, session, iceCandidates } | ||||||||||
| hostInfo: { ...existingSession.hostInfo, session, iceCandidates }, | ||||||||||
| lastUpdate | ||||||||||
| }; | ||||||||||
| this.map.set(endpointId, newInfo); | ||||||||||
|
|
||||||||||
|
|
@@ -85,12 +129,14 @@ class FlottformDatabase { | |||||||||
| endpointId, | ||||||||||
| clientKey, | ||||||||||
| session, | ||||||||||
| iceCandidates | ||||||||||
| iceCandidates, | ||||||||||
| lastUpdate = Date.now() | ||||||||||
|
Member
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. This could be done in the function directly where it's necessary instead of passing an argument (and potentially overwriting it) |
||||||||||
| }: { | ||||||||||
| endpointId: EndpointId; | ||||||||||
| clientKey: ClientKey; | ||||||||||
| session: RTCSessionDescriptionInit; | ||||||||||
| iceCandidates: RTCIceCandidateInit[]; | ||||||||||
| lastUpdate?: number; | ||||||||||
| }): Promise<Required<SafeEndpointInfo>> { | ||||||||||
| const existingSession = this.map.get(endpointId); | ||||||||||
| if (!existingSession) { | ||||||||||
|
|
@@ -105,7 +151,8 @@ class FlottformDatabase { | |||||||||
| const newInfo = { | ||||||||||
| ...existingSession, | ||||||||||
| clientKey, | ||||||||||
| clientInfo: { session, iceCandidates } | ||||||||||
| clientInfo: { session, iceCandidates }, | ||||||||||
| lastUpdate | ||||||||||
| }; | ||||||||||
| this.map.set(endpointId, newInfo); | ||||||||||
|
|
||||||||||
|
|
@@ -130,8 +177,11 @@ class FlottformDatabase { | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export async function createFlottformDatabase(): Promise<FlottformDatabase> { | ||||||||||
| return new FlottformDatabase(); | ||||||||||
| export async function createFlottformDatabase( | ||||||||||
| cleanupPeriod = 30 * 60 * 1000, | ||||||||||
| entryTTL = 25 * 60 * 1000 | ||||||||||
|
Member
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. The default values are here a second time. Can we move them into a constant at the beginning of the file?
Suggested change
|
||||||||||
| ): Promise<FlottformDatabase> { | ||||||||||
| return new FlottformDatabase(cleanupPeriod, entryTTL); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export type { FlottformDatabase }; | ||||||||||
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.
Passing
this.baseApishouldn't be necessary if we usethis.baseApiin the function instead of a parameter