-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: script to garbage collect unused files in the storage (#271)
- Loading branch information
1 parent
5083c36
commit 6987580
Showing
7 changed files
with
174 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { HandlerContextWithPath, WorldRecord } from '../../types' | ||
import SQL from 'sql-template-strings' | ||
import { IHttpServerComponent } from '@well-known-components/interfaces' | ||
|
||
function formatSecs(millis: number): string { | ||
return `${(millis / 1000).toFixed(2)} secs` | ||
} | ||
|
||
export async function garbageCollectionHandler( | ||
context: HandlerContextWithPath<'database' | 'logs' | 'storage', '/gc'> | ||
): Promise<IHttpServerComponent.IResponse> { | ||
const { database, logs, storage } = context.components | ||
const logger = logs.getLogger('garbage-collection') | ||
|
||
async function getAllActiveKeys() { | ||
const start = Date.now() | ||
logger.info('Getting all keys active in the database...') | ||
|
||
const activeKeys = new Set<string>() | ||
const result = await database.query<WorldRecord>( | ||
SQL`SELECT * | ||
FROM worlds | ||
WHERE worlds.entity IS NOT NULL` | ||
) | ||
result.rows.forEach((row) => { | ||
// Add entity file and deployment auth-chain | ||
activeKeys.add(row.entity_id) | ||
activeKeys.add(`${row.entity_id}.auth`) | ||
|
||
// Add all referenced content files | ||
for (const file of row.entity.content) { | ||
activeKeys.add(file.hash) | ||
} | ||
}) | ||
|
||
logger.info(`Done in ${formatSecs(Date.now() - start)}. Database contains ${activeKeys.size} active keys.`) | ||
|
||
return activeKeys | ||
} | ||
|
||
logger.info('Starting garbage collection...') | ||
|
||
const activeKeys = await getAllActiveKeys() | ||
|
||
logger.info('Getting keys from storage that are not currently active...') | ||
const start = Date.now() | ||
let totalRemovedKeys = 0 | ||
const batch = new Set<string>() | ||
for await (const key of storage.allFileIds()) { | ||
if (!activeKeys.has(key)) { | ||
batch.add(key) | ||
} | ||
|
||
if (batch.size === 1000) { | ||
logger.info(`Deleting a batch of ${batch.size} keys from storage...`) | ||
await storage.delete([...batch]) | ||
totalRemovedKeys += batch.size | ||
batch.clear() | ||
} | ||
} | ||
|
||
if (batch.size > 0) { | ||
logger.info(`Deleting a batch of ${batch.size} keys from storage...`) | ||
await storage.delete([...batch]) | ||
totalRemovedKeys += batch.size | ||
} | ||
logger.info( | ||
`Done in ${formatSecs(Date.now() - start)}. Deleted ${totalRemovedKeys} keys that are not active in the storage.` | ||
) | ||
|
||
logger.info('Garbage collection finished.') | ||
|
||
return { | ||
status: 200, | ||
body: { | ||
message: `Garbage collection removed ${totalRemovedKeys} unused keys.` | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { test } from '../components' | ||
import { stringToUtf8Bytes } from 'eth-connect' | ||
import { makeid } from '../utils' | ||
|
||
test('garbage collection works', function ({ components }) { | ||
it('cleans up all unused files', async () => { | ||
const { localFetch, storage, worldCreator } = components | ||
|
||
const worldName = worldCreator.randomWorldName() | ||
|
||
// deploy an initial version of the scene | ||
const files = new Map<string, Uint8Array>() | ||
files.set('abc.png', stringToUtf8Bytes(makeid(150))) | ||
files.set('abc.txt', stringToUtf8Bytes(makeid(50))) | ||
|
||
const { entityId, entity } = await worldCreator.createWorldWithScene({ | ||
worldName, | ||
metadata: { | ||
main: 'abc.txt', | ||
scene: { | ||
base: '20,24', | ||
parcels: ['20,24'] | ||
}, | ||
worldConfiguration: { | ||
name: worldName | ||
} | ||
}, | ||
files | ||
}) | ||
|
||
expect(await storage.exist(entityId)).toBeTruthy() | ||
expect(await storage.exist(`${entityId}.auth`)).toBeTruthy() | ||
expect(await storage.exist(entity.content[0].hash)).toBeTruthy() | ||
expect(await storage.exist(entity.content[1].hash)).toBeTruthy() | ||
|
||
// deploy a new version of the scene | ||
const newFiles = new Map<string, Uint8Array>() | ||
newFiles.set('abc.png', stringToUtf8Bytes(makeid(150))) | ||
newFiles.set('abc.txt', stringToUtf8Bytes(makeid(50))) | ||
|
||
const { entityId: entityId2, entity: entity2 } = await worldCreator.createWorldWithScene({ | ||
worldName, | ||
metadata: { | ||
main: 'abc.txt', | ||
scene: { | ||
base: '20,24', | ||
parcels: ['20,24'] | ||
}, | ||
worldConfiguration: { | ||
name: worldName | ||
} | ||
}, | ||
files: newFiles | ||
}) | ||
|
||
expect(await storage.exist(entityId2)).toBeTruthy() | ||
expect(await storage.exist(`${entityId2}.auth`)).toBeTruthy() | ||
expect(await storage.exist(entity2.content[0].hash)).toBeTruthy() | ||
expect(await storage.exist(entity2.content[1].hash)).toBeTruthy() | ||
|
||
// run garbage collection | ||
const response = await localFetch.fetch('/gc', { | ||
method: 'POST', | ||
headers: { | ||
Authorization: 'Bearer setup_some_secret_here' | ||
} | ||
}) | ||
|
||
// Check old files have been removed | ||
expect(response.status).toEqual(200) | ||
expect(await response.json()).toMatchObject({ message: 'Garbage collection removed 4 unused keys.' }) | ||
expect(await storage.exist(entityId)).toBeFalsy() | ||
expect(await storage.exist(`${entityId}.auth`)).toBeFalsy() | ||
expect(await storage.exist(entity.content[0].hash)).toBeFalsy() | ||
expect(await storage.exist(entity.content[1].hash)).toBeFalsy() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters