feat: queue robot run
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { createRemoteBrowserForRun, destroyRemoteBrowser } from "../browser-management/controller";
|
import { createRemoteBrowserForRun, destroyRemoteBrowser, getActiveBrowserIdByState } from "../browser-management/controller";
|
||||||
import { chromium } from 'playwright-extra';
|
import { chromium } from 'playwright-extra';
|
||||||
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
import { browserPool } from "../server";
|
import { browserPool } from "../server";
|
||||||
@@ -22,6 +22,7 @@ import { encrypt, decrypt } from '../utils/auth';
|
|||||||
import { WorkflowFile } from 'maxun-core';
|
import { WorkflowFile } from 'maxun-core';
|
||||||
import { Page } from 'playwright';
|
import { Page } from 'playwright';
|
||||||
import { airtableUpdateTasks, processAirtableUpdates } from '../workflow-management/integrations/airtable';
|
import { airtableUpdateTasks, processAirtableUpdates } from '../workflow-management/integrations/airtable';
|
||||||
|
import { pgBoss } from '../pgboss-worker';
|
||||||
chromium.use(stealthPlugin());
|
chromium.use(stealthPlugin());
|
||||||
|
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
@@ -494,6 +495,8 @@ router.delete('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res)
|
|||||||
/**
|
/**
|
||||||
* PUT endpoint for starting a remote browser instance and saving run metadata to the storage.
|
* PUT endpoint for starting a remote browser instance and saving run metadata to the storage.
|
||||||
* Making it ready for interpretation and returning a runId.
|
* Making it ready for interpretation and returning a runId.
|
||||||
|
*
|
||||||
|
* If the user has reached their browser limit, the run will be queued using PgBoss.
|
||||||
*/
|
*/
|
||||||
router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -525,35 +528,81 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`)
|
console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`);
|
||||||
|
|
||||||
const id = createRemoteBrowserForRun(req.user.id);
|
|
||||||
|
|
||||||
|
// Generate runId first
|
||||||
const runId = uuid();
|
const runId = uuid();
|
||||||
|
|
||||||
|
// Check if user has reached browser limit
|
||||||
|
const userBrowserIds = browserPool.getAllBrowserIdsForUser(req.user.id);
|
||||||
|
const canCreateBrowser = userBrowserIds.length < 2;
|
||||||
|
|
||||||
|
if (canCreateBrowser) {
|
||||||
|
// User has available browser slots, create it directly
|
||||||
|
const id = createRemoteBrowserForRun(req.user.id);
|
||||||
|
|
||||||
const run = await Run.create({
|
const run = await Run.create({
|
||||||
status: 'running',
|
status: 'running',
|
||||||
name: recording.recording_meta.name,
|
name: recording.recording_meta.name,
|
||||||
robotId: recording.id,
|
robotId: recording.id,
|
||||||
robotMetaId: recording.recording_meta.id,
|
robotMetaId: recording.recording_meta.id,
|
||||||
startedAt: new Date().toLocaleString(),
|
startedAt: new Date().toLocaleString(),
|
||||||
finishedAt: '',
|
finishedAt: '',
|
||||||
browserId: id,
|
browserId: id,
|
||||||
interpreterSettings: req.body,
|
interpreterSettings: req.body,
|
||||||
log: '',
|
log: '',
|
||||||
runId,
|
runId,
|
||||||
runByUserId: req.user.id,
|
runByUserId: req.user.id,
|
||||||
serializableOutput: {},
|
serializableOutput: {},
|
||||||
binaryOutput: {},
|
binaryOutput: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const plainRun = run.toJSON();
|
const plainRun = run.toJSON();
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
browserId: id,
|
browserId: id,
|
||||||
runId: plainRun.runId,
|
runId: plainRun.runId,
|
||||||
robotMetaId: recording.recording_meta.id,
|
robotMetaId: recording.recording_meta.id,
|
||||||
});
|
queued: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const browserId = getActiveBrowserIdByState(req.user.id, "run")
|
||||||
|
|
||||||
|
if (browserId) {
|
||||||
|
// User has reached the browser limit, queue the run
|
||||||
|
try {
|
||||||
|
// Create the run record with 'queued' status
|
||||||
|
await Run.create({
|
||||||
|
status: 'queued',
|
||||||
|
name: recording.recording_meta.name,
|
||||||
|
robotId: recording.id,
|
||||||
|
robotMetaId: recording.recording_meta.id,
|
||||||
|
startedAt: new Date().toLocaleString(),
|
||||||
|
finishedAt: '',
|
||||||
|
browserId: browserId, // Random will be updated later
|
||||||
|
interpreterSettings: req.body,
|
||||||
|
log: 'Run queued - waiting for available browser slot',
|
||||||
|
runId,
|
||||||
|
runByUserId: req.user.id,
|
||||||
|
serializableOutput: {},
|
||||||
|
binaryOutput: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
browserId: browserId,
|
||||||
|
runId: runId,
|
||||||
|
robotMetaId: recording.recording_meta.id,
|
||||||
|
queued: true,
|
||||||
|
});
|
||||||
|
} catch (queueError: any) {
|
||||||
|
logger.log('error', `Failed to queue run job: ${queueError.message}`);
|
||||||
|
return res.status(503).send({ error: 'Unable to queue run, please try again later' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('info', "Browser id does not exist");
|
||||||
|
return res.send('');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { message } = e as Error;
|
const { message } = e as Error;
|
||||||
logger.log('info', `Error while creating a run with robot id: ${req.params.id} - ${message}`);
|
logger.log('info', `Error while creating a run with robot id: ${req.params.id} - ${message}`);
|
||||||
@@ -608,82 +657,20 @@ router.post('/runs/run/:id', requireSignIn, async (req: AuthenticatedRequest, re
|
|||||||
return res.status(404).send(false);
|
return res.status(404).send(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// interpret the run in active browser
|
try {
|
||||||
const browser = browserPool.getRemoteBrowser(plainRun.browserId);
|
// Queue the execution job
|
||||||
let currentPage = browser?.getCurrentPage();
|
await pgBoss.createQueue('execute-run');
|
||||||
if (browser && currentPage) {
|
|
||||||
const workflow = AddGeneratedFlags(recording.recording);
|
const jobId = await pgBoss.send('execute-run', {
|
||||||
const interpretationInfo = await browser.interpreter.InterpretRecording(
|
userId: req.user.id,
|
||||||
workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings);
|
runId: req.params.id,
|
||||||
const binaryOutputService = new BinaryOutputService('maxun-run-screenshots');
|
browserId: plainRun.browserId
|
||||||
const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput);
|
|
||||||
await destroyRemoteBrowser(plainRun.browserId, req.user?.id);
|
|
||||||
await run.update({
|
|
||||||
...run,
|
|
||||||
status: 'success',
|
|
||||||
finishedAt: new Date().toLocaleString(),
|
|
||||||
browserId: plainRun.browserId,
|
|
||||||
log: interpretationInfo.log.join('\n'),
|
|
||||||
serializableOutput: interpretationInfo.serializableOutput,
|
|
||||||
binaryOutput: uploadedBinaryOutput,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let totalRowsExtracted = 0;
|
logger.log('info', `Queued run execution job with ID: ${jobId} for run: ${req.params.id}`);
|
||||||
let extractedScreenshotsCount = 0;
|
} catch (queueError: any) {
|
||||||
let extractedItemsCount = 0;
|
logger.log('error', `Failed to queue run execution`);
|
||||||
|
|
||||||
if (run.dataValues.binaryOutput && run.dataValues.binaryOutput["item-0"]) {
|
|
||||||
extractedScreenshotsCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (run.dataValues.serializableOutput && run.dataValues.serializableOutput["item-0"]) {
|
|
||||||
const itemsArray = run.dataValues.serializableOutput["item-0"];
|
|
||||||
extractedItemsCount = itemsArray.length;
|
|
||||||
|
|
||||||
totalRowsExtracted = itemsArray.reduce((total, item) => {
|
|
||||||
return total + Object.keys(item).length;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Extracted Items Count: ${extractedItemsCount}`);
|
|
||||||
console.log(`Extracted Screenshots Count: ${extractedScreenshotsCount}`);
|
|
||||||
console.log(`Total Rows Extracted: ${totalRowsExtracted}`);
|
|
||||||
|
|
||||||
capture(
|
|
||||||
'maxun-oss-run-created-manual',
|
|
||||||
{
|
|
||||||
runId: req.params.id,
|
|
||||||
user_id: req.user?.id,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
status: 'success',
|
|
||||||
totalRowsExtracted,
|
|
||||||
extractedItemsCount,
|
|
||||||
extractedScreenshotsCount,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
googleSheetUpdateTasks[plainRun.runId] = {
|
|
||||||
robotId: plainRun.robotMetaId,
|
|
||||||
runId: plainRun.runId,
|
|
||||||
status: 'pending',
|
|
||||||
retries: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
airtableUpdateTasks[plainRun.runId] = {
|
|
||||||
robotId: plainRun.robotMetaId,
|
|
||||||
runId: plainRun.runId,
|
|
||||||
status: 'pending',
|
|
||||||
retries: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
processAirtableUpdates();
|
|
||||||
processGoogleSheetUpdates();
|
|
||||||
} catch (err: any) {
|
|
||||||
logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`);
|
|
||||||
}
|
|
||||||
return res.send(true);
|
|
||||||
} else {
|
|
||||||
throw new Error('Could not destroy browser');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { message } = e as Error;
|
const { message } = e as Error;
|
||||||
|
|||||||
Reference in New Issue
Block a user