From dcada36111d373d2c13bbfddc3f399adfa4bb632 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 7 Sep 2022 17:05:17 +0100 Subject: [PATCH] Fix for #7431 - reboot didn't work at all previously which is why apps couldn't be published with it enabled, this is now a self host only feature, I've removed the ability to enable a reboot cron in the Cloud and it will not run the lookup/execution. --- packages/backend-core/src/db/utils.ts | 11 +++- .../automation/SetupPanel/CronBuilder.svelte | 16 ++++-- .../src/api/controllers/deploy/index.ts | 2 +- packages/server/src/automations/index.js | 9 ++-- packages/server/src/automations/triggers.js | 54 +++++++++++++++---- packages/server/src/automations/utils.ts | 30 +++++++---- packages/server/src/threads/automation.ts | 6 +++ .../types/src/documents/app/automation.ts | 4 ++ 8 files changed, 103 insertions(+), 29 deletions(-) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 321ebd7f58..4926a60150 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -254,7 +254,16 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { return false }) if (idsOnly) { - return appDbNames + const devAppIds = appDbNames.filter(appId => isDevAppID(appId)) + const prodAppIds = appDbNames.filter(appId => !isDevAppID(appId)) + switch (dev) { + case true: + return devAppIds + case false: + return prodAppIds + default: + return appDbNames + } } const appPromises = appDbNames.map((app: any) => // skip setup otherwise databases could be re-created diff --git a/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte b/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte index 93b8394b49..f2da863424 100644 --- a/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte +++ b/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte @@ -1,6 +1,7 @@
diff --git a/packages/server/src/api/controllers/deploy/index.ts b/packages/server/src/api/controllers/deploy/index.ts index 86718294de..2ac6c8a15c 100644 --- a/packages/server/src/api/controllers/deploy/index.ts +++ b/packages/server/src/api/controllers/deploy/index.ts @@ -125,7 +125,7 @@ async function deployApp(deployment: any) { const prodAppDoc = await db.get(DocumentType.APP_METADATA) appDoc._rev = prodAppDoc._rev } catch (err) { - // ignore the error - doesn't exist + delete appDoc._rev } // switch to production app ID diff --git a/packages/server/src/automations/index.js b/packages/server/src/automations/index.js index 9c23e35d1c..2baa868890 100644 --- a/packages/server/src/automations/index.js +++ b/packages/server/src/automations/index.js @@ -1,16 +1,19 @@ const { processEvent } = require("./utils") const { queue, shutdown } = require("./bullboard") -const { TRIGGER_DEFINITIONS } = require("./triggers") +const { TRIGGER_DEFINITIONS, rebootTrigger } = require("./triggers") const { ACTION_DEFINITIONS } = require("./actions") /** * This module is built purely to kick off the worker farm and manage the inputs/outputs */ -exports.init = function () { +exports.init = async function () { // this promise will not complete - return queue.process(async job => { + const promise = queue.process(async job => { await processEvent(job) }) + // on init we need to trigger any reboot automations + await rebootTrigger() + return promise } exports.getQueues = () => { diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 216f24be02..395390113a 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -9,6 +9,7 @@ const { checkTestFlag } = require("../utilities/redis") const utils = require("./utils") const env = require("../environment") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") +const { getAllApps } = require("@budibase/backend-core/db") const TRIGGER_DEFINITIONS = definitions const JOB_OPTS = { @@ -16,24 +17,27 @@ const JOB_OPTS = { removeOnFail: true, } +async function getAllAutomations() { + const db = getAppDB() + let automations = await db.allDocs( + getAutomationParams(null, { include_docs: true }) + ) + return automations.rows.map(row => row.doc) +} + async function queueRelevantRowAutomations(event, eventType) { if (event.appId == null) { throw `No appId specified for ${eventType} - check event emitters.` } doInAppContext(event.appId, async () => { - const db = getAppDB() - let automations = await db.allDocs( - getAutomationParams(null, { include_docs: true }) - ) + let automations = await getAllAutomations() // filter down to the correct event type - automations = automations.rows - .map(automation => automation.doc) - .filter(automation => { - const trigger = automation.definition.trigger - return trigger && trigger.event === eventType - }) + automations = automations.filter(automation => { + const trigger = automation.definition.trigger + return trigger && trigger.event === eventType + }) for (let automation of automations) { let automationDef = automation.definition @@ -110,4 +114,34 @@ exports.externalTrigger = async function ( } } +exports.rebootTrigger = async () => { + // reboot cron option is only available on the main thread at + // startup and only usable in self host + if (env.isInThread() || !env.SELF_HOSTED) { + return + } + // iterate through all production apps, find the reboot crons + // and trigger events for them + const appIds = await getAllApps({ dev: false, idsOnly: true }) + for (let prodAppId of appIds) { + await doInAppContext(prodAppId, async () => { + let automations = await getAllAutomations() + let rebootEvents = [] + for (let automation of automations) { + if (utils.isRebootTrigger(automation)) { + const job = { + automation, + event: { + appId: prodAppId, + timestamp: Date.now(), + }, + } + rebootEvents.push(queue.add(job, JOB_OPTS)) + } + } + await Promise.all(rebootEvents) + }) + } +} + exports.TRIGGER_DEFINITIONS = TRIGGER_DEFINITIONS diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index e0979ac0d9..3093f147dc 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -17,6 +17,7 @@ import { tenancy } from "@budibase/backend-core" import { quotas } from "@budibase/pro" import { Automation } from "@budibase/types" +const REBOOT_CRON = "@reboot" const WH_STEP_ID = definitions.WEBHOOK.stepId const CRON_STEP_ID = definitions.CRON.stepId const Runner = new Thread(ThreadType.AUTOMATION) @@ -109,22 +110,33 @@ export async function clearMetadata() { await db.bulkDocs(automationMetadata) } +export function isCronTrigger(auto: Automation) { + return ( + auto && + auto.definition.trigger && + auto.definition.trigger.stepId === CRON_STEP_ID + ) +} + +export function isRebootTrigger(auto: Automation) { + const trigger = auto ? auto.definition.trigger : null + return isCronTrigger(auto) && trigger?.inputs.cron === REBOOT_CRON +} + /** * This function handles checking of any cron jobs that need to be enabled/updated. * @param {string} appId The ID of the app in which we are checking for webhooks * @param {object|undefined} automation The automation object to be updated. */ -export async function enableCronTrigger(appId: any, automation: any) { +export async function enableCronTrigger(appId: any, automation: Automation) { const trigger = automation ? automation.definition.trigger : null - function isCronTrigger(auto: any) { - return ( - auto && - auto.definition.trigger && - auto.definition.trigger.stepId === CRON_STEP_ID - ) - } + // need to create cron job - if (isCronTrigger(automation) && trigger?.inputs.cron) { + if ( + isCronTrigger(automation) && + !isRebootTrigger(automation) && + trigger?.inputs.cron + ) { // make a job id rather than letting Bull decide, makes it easier to handle on way out const jobId = `${appId}_cron_${newid()}` const job: any = await queue.add( diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 3136155869..f64552a92f 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -458,6 +458,9 @@ class Orchestrator { export function execute(input: AutomationEvent, callback: WorkerCallback) { const appId = input.data.event.appId + if (!appId) { + throw new Error("Unable to execute, event doesn't contain app ID.") + } doInAppContext(appId, async () => { const automationOrchestrator = new Orchestrator( input.data.automation, @@ -475,6 +478,9 @@ export function execute(input: AutomationEvent, callback: WorkerCallback) { export const removeStalled = async (input: AutomationEvent) => { const appId = input.data.event.appId + if (!appId) { + throw new Error("Unable to execute, event doesn't contain app ID.") + } await doInAppContext(appId, async () => { const automationOrchestrator = new Orchestrator( input.data.automation, diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index 50562461e4..a038e73d11 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -25,6 +25,10 @@ export interface AutomationStep { export interface AutomationTrigger { id: string stepId: string + inputs: { + [key: string]: any + } + cronJobId?: string } export enum AutomationStatus {