-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add a cleanup mechanism for old db entries #104
base: main
Are you sure you want to change the base?
Changes from all commits
d261744
12d007a
9886d7f
481b54b
2edce18
fce4f55
89195be
6511b0a
9c9b49e
742aa3d
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,4 +1,4 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; | ||
import { createFlottformDatabase } from './database'; | ||
|
||
describe('Flottform database', () => { | ||
|
@@ -168,9 +168,77 @@ 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
+171
to
+173
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 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()', () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers(); | ||
}); | ||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
it('Should clean up stale entries after entryTTL', async () => { | ||
const db = await createFlottformDatabase({ | ||
cleanupPeriod: 1000, | ||
entryTimeToLive: 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() | ||
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 | ||
vi.advanceTimersByTime(1100); | ||
|
||
// 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({ | ||
cleanupPeriod: 1000, | ||
entryTimeToLive: 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() | ||
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,9 +13,13 @@ type EndpointInfo = { | |
session: RTCSessionDescriptionInit; | ||
iceCandidates: RTCIceCandidateInit[]; | ||
}; | ||
lastUpdate: number; | ||
}; | ||
type SafeEndpointInfo = Omit<EndpointInfo, 'hostKey' | 'clientKey'>; | ||
|
||
const DEFAULT_CLEANUP_PERIOD = 30 * 60 * 1000; | ||
const DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS = 25 * 60 * 1000; | ||
|
||
function createRandomHostKey(): string { | ||
return crypto.randomUUID(); | ||
} | ||
|
@@ -25,8 +29,52 @@ function createRandomEndpointId(): string { | |
|
||
class FlottformDatabase { | ||
private map = new Map<EndpointId, EndpointInfo>(); | ||
private cleanupTimeoutId: NodeJS.Timeout | null = null; | ||
private cleanupPeriod: number; | ||
private entryTimeToLive: number; | ||
|
||
constructor({ | ||
cleanupPeriod = DEFAULT_CLEANUP_PERIOD, | ||
entryTimeToLive = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS | ||
}: { | ||
cleanupPeriod?: number; | ||
entryTimeToLive?: number; | ||
} = {}) { | ||
this.cleanupPeriod = cleanupPeriod; | ||
this.entryTimeToLive = entryTimeToLive; | ||
this.startCleanup(); | ||
} | ||
|
||
constructor() {} | ||
private startCleanup() { | ||
this.cleanupTimeoutId = setTimeout(this.cleanupFn.bind(this), this.cleanupPeriod); | ||
} | ||
|
||
private cleanupFn() { | ||
if (this.map && this.map.size !== 0) { | ||
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.entryTimeToLive) { | ||
this.map.delete(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; | ||
} | ||
} | ||
|
||
// Stop the cleanup when the database is no longer needed | ||
destroy() { | ||
this.stopCleanup(); | ||
} | ||
|
||
async createEndpoint({ session }: { session: RTCSessionDescriptionInit }): Promise<EndpointInfo> { | ||
const entry = { | ||
|
@@ -35,7 +83,8 @@ class FlottformDatabase { | |
hostInfo: { | ||
session, | ||
iceCandidates: [] | ||
} | ||
}, | ||
lastUpdate: Date.now() | ||
}; | ||
this.map.set(entry.endpointId, entry); | ||
return entry; | ||
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 +95,7 @@ class FlottformDatabase { | |
if (!entry) { | ||
throw Error('Endpoint not found'); | ||
} | ||
entry.lastUpdate = Date.now(); | ||
const { hostKey: _ignore1, clientKey: _ignore2, ...endpoint } = entry; | ||
|
||
return endpoint; | ||
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 +105,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 +124,8 @@ class FlottformDatabase { | |
|
||
const newInfo = { | ||
...existingSession, | ||
hostInfo: { ...existingSession.hostInfo, session, iceCandidates } | ||
hostInfo: { ...existingSession.hostInfo, session, iceCandidates }, | ||
lastUpdate | ||
}; | ||
this.map.set(endpointId, newInfo); | ||
|
||
|
@@ -85,12 +138,14 @@ class FlottformDatabase { | |
endpointId, | ||
clientKey, | ||
session, | ||
iceCandidates | ||
iceCandidates, | ||
lastUpdate = Date.now() | ||
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 +160,8 @@ class FlottformDatabase { | |
const newInfo = { | ||
...existingSession, | ||
clientKey, | ||
clientInfo: { session, iceCandidates } | ||
clientInfo: { session, iceCandidates }, | ||
lastUpdate | ||
}; | ||
this.map.set(endpointId, newInfo); | ||
|
||
|
@@ -130,8 +186,14 @@ class FlottformDatabase { | |
} | ||
} | ||
|
||
export async function createFlottformDatabase(): Promise<FlottformDatabase> { | ||
return new FlottformDatabase(); | ||
export async function createFlottformDatabase({ | ||
cleanupPeriod = DEFAULT_CLEANUP_PERIOD, | ||
entryTimeToLive = DEFAULT_ENTRY_TIME_TO_LIVE_IN_MS | ||
}: { | ||
cleanupPeriod?: number; | ||
entryTimeToLive?: number; | ||
} = {}): Promise<FlottformDatabase> { | ||
return new FlottformDatabase({ cleanupPeriod, entryTimeToLive }); | ||
} | ||
|
||
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.baseApi
shouldn't be necessary if we usethis.baseApi
in the function instead of a parameter