-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement worker threads for upload processing
- Offload flashcard generation to a separate worker thread - Prevents blocking of main thread during uploads, improving responsiveness - Addresses bug causing server slowdown during large uploads (#123) Refs: https://nodejs.org/api/worker_threads.html https://stackoverflow.com/questions/61095741/how-do-i-avoid-blocking-an-express-rest-service
- Loading branch information
Showing
3 changed files
with
128 additions
and
91 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,63 @@ | ||
import { Body } from 'aws-sdk/clients/s3'; | ||
import Settings from '../../lib/parser/Settings'; | ||
import { ZipHandler } from '../../lib/anki/zip'; | ||
import { PrepareDeck } from '../../lib/parser/PrepareDeck'; | ||
import Package from '../../lib/parser/Package'; | ||
import { checkFlashcardsLimits } from '../../lib/User/checkFlashcardsLimits'; | ||
import { PackageResult } from './GeneratePackagesUseCase'; | ||
import { | ||
isCSVFile, | ||
isHTMLFile, | ||
isMarkdownFile, | ||
isPlainText, | ||
} from '../../lib/storage/checks'; | ||
|
||
export const isFileSupported = (filename: string) => | ||
isHTMLFile(filename) ?? | ||
isMarkdownFile(filename) ?? | ||
isPlainText(filename) ?? | ||
isCSVFile(filename); | ||
|
||
export const getPackagesFromZip = async ( | ||
fileContents: Body | undefined, | ||
paying: boolean, | ||
settings: Settings | ||
): Promise<PackageResult> => { | ||
const zipHandler = new ZipHandler(); | ||
const packages = []; | ||
|
||
if (!fileContents) { | ||
return { packages: [] }; | ||
} | ||
|
||
zipHandler.build(fileContents as Uint8Array, paying); | ||
|
||
const fileNames = zipHandler.getFileNames(); | ||
|
||
let cardCount = 0; | ||
for (const fileName of fileNames) { | ||
if (isFileSupported(fileName)) { | ||
const deck = await PrepareDeck(fileName, zipHandler.files, settings); | ||
|
||
if (deck) { | ||
packages.push(new Package(deck.name, deck.apkg)); | ||
cardCount += deck.deck.reduce((acc, d) => acc + d.cards.length, 0); | ||
|
||
// Checking the limit in place while iterating through the decks | ||
checkFlashcardsLimits({ | ||
cards: 0, | ||
decks: deck.deck, | ||
paying, | ||
}); | ||
} | ||
} | ||
|
||
// Checking the limit in place while iterating through the files | ||
checkFlashcardsLimits({ | ||
cards: cardCount, | ||
paying: paying, | ||
}); | ||
} | ||
|
||
return { packages }; | ||
}; |
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,55 @@ | ||
import { parentPort, workerData } from 'worker_threads'; | ||
import { UploadedFile } from '../../lib/storage/types'; | ||
import Settings from '../../lib/parser/Settings'; | ||
import Package from '../../lib/parser/Package'; | ||
import fs from 'fs'; | ||
import { PrepareDeck } from '../../lib/parser/PrepareDeck'; | ||
import { isZIPFile } from '../../lib/storage/checks'; | ||
import { getPackagesFromZip, isFileSupported } from './getPackagesFromZip'; | ||
|
||
interface GenerationData { | ||
paying: boolean; | ||
files: UploadedFile[]; | ||
settings: Settings; | ||
} | ||
|
||
function doGenerationWork(data: GenerationData) { | ||
console.log('doGenerationWork'); | ||
return new Promise(async (resolve) => { | ||
console.log('starting generation'); | ||
const { paying, files, settings } = data; | ||
let packages: Package[] = []; | ||
|
||
for (const file of files) { | ||
const fileContents = file.path ? fs.readFileSync(file.path) : file.buffer; | ||
const filename = file.originalname; | ||
const key = file.key; | ||
|
||
if (isFileSupported(filename)) { | ||
const d = await PrepareDeck( | ||
filename, | ||
[{ name: filename, contents: fileContents }], | ||
settings | ||
); | ||
if (d) { | ||
const pkg = new Package(d.name, d.apkg); | ||
packages = packages.concat(pkg); | ||
} | ||
} else if (isZIPFile(filename) || isZIPFile(key)) { | ||
const { packages: extraPackages } = await getPackagesFromZip( | ||
fileContents, | ||
paying, | ||
settings | ||
); | ||
packages = packages.concat(extraPackages); | ||
} | ||
} | ||
resolve({ packages }); | ||
}); | ||
} | ||
|
||
doGenerationWork(workerData.data) | ||
.then((result) => { | ||
parentPort?.postMessage(result); | ||
}) | ||
.catch(parentPort?.postMessage); |