-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(data caching): add WIP read through data cache
Uses an FsDataStore to stream data to disk as it's read from another source. Currently writes, but never reads. Reading when implemented will use the DB to lookup hashes by ID.
- Loading branch information
Showing
4 changed files
with
186 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import crypto from 'crypto'; | ||
import { Writable } from 'stream'; | ||
import winston from 'winston'; | ||
|
||
import { toB64Url } from '../lib/encoding.js'; | ||
import { | ||
ContiguousData, | ||
ContiguousDataSource, | ||
ContiguousDataStore, | ||
} from '../types.js'; | ||
|
||
export class ReadThroughDataCache implements ContiguousDataSource { | ||
private log: winston.Logger; | ||
private dataSource: ContiguousDataSource; | ||
private dataStore: ContiguousDataStore; | ||
|
||
constructor({ | ||
log, | ||
dataSource, | ||
dataStore, | ||
}: { | ||
log: winston.Logger; | ||
dataSource: ContiguousDataSource; | ||
dataStore: ContiguousDataStore; | ||
}) { | ||
this.log = log.child({ class: 'ReadThroughDataCache' }); | ||
this.dataSource = dataSource; | ||
this.dataStore = dataStore; | ||
} | ||
|
||
async getData(id: string): Promise<ContiguousData> { | ||
this.log.info(`Fetching data for ${id}`); | ||
// TODO check if data is in FS store | ||
// TODO stream from FS store if it is | ||
|
||
const data = await this.dataSource.getData(id); | ||
let cacheStream: Writable; | ||
try { | ||
cacheStream = await this.dataStore.createWriteStream(); | ||
// TODO handle stream errors | ||
data.stream.pipe(cacheStream); | ||
} catch (error: any) { | ||
this.log.error('Error creating cache stream:', { | ||
id, | ||
message: error.message, | ||
stack: error.stack, | ||
}); | ||
} | ||
|
||
const hash = crypto.createHash('sha256'); | ||
data.stream.on('data', (chunk) => { | ||
hash.update(chunk); | ||
}); | ||
|
||
data.stream.on('error', (error) => { | ||
this.log.error('Error reading data:', { | ||
id, | ||
message: error.message, | ||
stack: error.stack, | ||
}); | ||
// TODO delete temp file | ||
}); | ||
|
||
// TODO should this be on cacheStream? | ||
data.stream.on('end', () => { | ||
if (cacheStream !== undefined) { | ||
const digest = hash.digest(); | ||
const b64uDigest = toB64Url(digest); | ||
|
||
this.log.info('Successfully cached data:', { | ||
id, | ||
hash: b64uDigest, | ||
}); | ||
this.dataStore.finalize(cacheStream, digest); | ||
} else { | ||
this.log.error('Error caching data:', { | ||
id, | ||
message: 'no cache stream', | ||
}); | ||
} | ||
}); | ||
|
||
return data; | ||
} | ||
} |
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,85 @@ | ||
import crypto from 'crypto'; | ||
import fs from 'fs'; | ||
import { Readable } from 'stream'; | ||
import winston from 'winston'; | ||
|
||
import { toB64Url } from '../lib/encoding.js'; | ||
import { ContiguousDataStore } from '../types.js'; | ||
|
||
export class FsDataStore implements ContiguousDataStore { | ||
private log: winston.Logger; | ||
private baseDir: string; | ||
|
||
constructor({ log, baseDir }: { log: winston.Logger; baseDir: string }) { | ||
this.log = log.child({ class: this.constructor.name }); | ||
this.baseDir = baseDir; | ||
} | ||
|
||
private tempDir() { | ||
return `${this.baseDir}/tmp`; | ||
} | ||
|
||
private createTempPath() { | ||
return `${this.tempDir()}/${crypto.randomBytes(16).toString('hex')}`; | ||
} | ||
|
||
private dataDir(b64uHashString: string) { | ||
const hashPrefix = `${b64uHashString.substring( | ||
0, | ||
2, | ||
)}/${b64uHashString.substring(2, 4)}`; | ||
return `${this.baseDir}/data/${hashPrefix}`; | ||
} | ||
|
||
private dataPath(hash: Buffer) { | ||
const hashString = toB64Url(hash); | ||
return `${this.dataDir(hashString)}/${hashString}`; | ||
} | ||
|
||
async has(hash: Buffer) { | ||
try { | ||
await fs.promises.access(this.dataPath(hash), fs.constants.F_OK); | ||
return true; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
async get(hash: Buffer): Promise<Readable | undefined> { | ||
try { | ||
if (await this.has(hash)) { | ||
return fs.createReadStream(this.dataPath(hash)); | ||
} | ||
} catch (error: any) { | ||
// TODO log hash | ||
this.log.error('Failed to get contigous data stream', { | ||
message: error.message, | ||
stack: error.stack, | ||
}); | ||
} | ||
return undefined; | ||
} | ||
|
||
async createWriteStream() { | ||
const tempPath = this.createTempPath(); | ||
await fs.promises.mkdir(this.tempDir(), { recursive: true }); | ||
const file = fs.createWriteStream(tempPath); | ||
return file; | ||
} | ||
|
||
async finalize(stream: fs.WriteStream, hash: Buffer) { | ||
try { | ||
stream.end(); | ||
const dataDir = this.dataDir(toB64Url(hash)); | ||
await fs.promises.mkdir(dataDir, { recursive: true }); | ||
await fs.promises.rename(stream.path, this.dataPath(hash)); | ||
} catch (error: any) { | ||
this.log.error('Failed to finalize contigous data stream', { | ||
message: error.message, | ||
stack: error.stack, | ||
}); | ||
} | ||
} | ||
|
||
// TODO del? | ||
} |
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