Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement automateFunctionRunStatusReport #2262

Merged
merged 10 commits into from
May 16, 2024
6 changes: 5 additions & 1 deletion packages/server/assets/automate/typedefs/automate.graphql
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
enum AutomateRunStatus {
PENDING
INITIALIZING
RUNNING
SUCCEEDED
FAILED
EXCEPTION
TIMEOUT
CANCELED
}

enum AutomateRunTriggerType {
Expand Down Expand Up @@ -324,7 +328,7 @@ extend type Query {

extend type Mutation {
automateFunctionRunStatusReport(
input: [AutomateFunctionRunStatusReportInput!]!
input: AutomateFunctionRunStatusReportInput!
): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "automate:report-results")
Expand Down
32 changes: 29 additions & 3 deletions packages/server/modules/automate/graph/resolvers/automate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
getAutomationRunsItems,
getAutomationRunsTotalCount,
getAutomationTriggerDefinitions,
getFunctionRun,
getLatestVersionAutomationRuns,
getProjectAutomationsItems,
getProjectAutomationsTotalCount,
storeAutomation,
storeAutomationRevision,
updateAutomation as updateDbAutomation
updateAutomation as updateDbAutomation,
upsertAutomationFunctionRun
} from '@/modules/automate/repositories/automations'
import {
createAutomation,
Expand Down Expand Up @@ -50,8 +52,10 @@ import {
getBranchesByIds
} from '@/modules/core/repositories/branches'
import {
setFunctionRunStatusReport,
manuallyTriggerAutomation,
triggerAutomationRevisionRun
triggerAutomationRevisionRun,
SetFunctionRunStatusReportDeps
} from '@/modules/automate/services/trigger'
import {
AutomateFunctionReleaseNotFoundError,
Expand All @@ -62,7 +66,6 @@ import {
dbToGraphqlTriggerTypeMap,
functionTemplateRepos
} from '@/modules/automate/helpers/executionEngine'
import { mapDbStatusToGqlStatus } from '@/modules/automate/services/runsManagement'
import { authorizeResolver } from '@/modules/shared'
import {
AutomationRevisionFunctionForInputRedaction,
Expand All @@ -79,6 +82,10 @@ import {
ProjectSubscriptions,
filteredSubscribe
} from '@/modules/shared/utils/subscriptions'
import {
mapDbStatusToGqlStatus,
mapGqlStatusToDbStatus
} from '@/modules/automate/utils/automateFunctionRunStatus'

/**
* TODO:
Expand Down Expand Up @@ -524,6 +531,25 @@ export = {
}
},
Mutation: {
async automateFunctionRunStatusReport(_parent, { input }) {
const deps: SetFunctionRunStatusReportDeps = {
getAutomationFunctionRunRecord: getFunctionRun,
upsertAutomationFunctionRunRecord: upsertAutomationFunctionRun
}

const payload = {
...input,
contextView: input.contextView ?? null,
results: (input.results as Automate.AutomateTypes.ResultsSchema) ?? null,
runId: input.functionRunId,
status: mapGqlStatusToDbStatus(input.status),
statusMessage: input.statusMessage ?? null
}

const result = await setFunctionRunStatusReport(deps)(payload)

return result
},
automateMutations: () => ({})
},
Subscription: {
Expand Down
18 changes: 12 additions & 6 deletions packages/server/modules/automate/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,23 @@ export type AutomationRevisionRecord = {

export type AutomationRunStatus =
| 'pending'
| 'initializing'
| 'running'
| 'success'
| 'failure'
| 'error'
| 'succeeded'
| 'failed'
| 'exception'
| 'timeout'
| 'canceled'

export const AutomationRunStatuses: Record<AutomationRunStatus, AutomationRunStatus> = {
pending: 'pending',
initializing: 'initializing',
running: 'running',
success: 'success',
failure: 'failure',
error: 'error'
succeeded: 'succeeded',
failed: 'failed',
exception: 'exception',
timeout: 'timeout',
canceled: 'canceled'
}

export type AutomationRunRecord = {
Expand Down
22 changes: 22 additions & 0 deletions packages/server/modules/automate/repositories/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ export async function getFullAutomationRevisionMetadata(
}
}

export type InsertableAutomationFunctionRun = Omit<
AutomationFunctionRunRecord,
'id' | 'runId'
gjedlicska marked this conversation as resolved.
Show resolved Hide resolved
gjedlicska marked this conversation as resolved.
Show resolved Hide resolved
>

export async function upsertAutomationFunctionRun(
automationFunctionRun: InsertableAutomationFunctionRun
) {
await AutomationFunctionRuns.knex()
.insert(
_.pick(automationFunctionRun, AutomationFunctionRuns.withoutTablePrefix.cols)
)
.onConflict(AutomationFunctionRuns.withoutTablePrefix.col.id)
.merge([
AutomationFunctionRuns.withoutTablePrefix.col.contextView,
AutomationFunctionRuns.withoutTablePrefix.col.elapsed,
AutomationFunctionRuns.withoutTablePrefix.col.results,
AutomationFunctionRuns.withoutTablePrefix.col.status,
AutomationFunctionRuns.withoutTablePrefix.col.statusMessage
])
}

export type InsertableAutomationRun = AutomationRunRecord & {
triggers: Omit<AutomationRunTriggerRecord, 'automationRunId'>[]
functionRuns: Omit<AutomationFunctionRunRecord, 'runId'>[]
Expand Down
74 changes: 9 additions & 65 deletions packages/server/modules/automate/services/runsManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,69 +13,13 @@ import {
updateFunctionRun
} from '@/modules/automate/repositories/automations'
import {
AutomateFunctionRunStatusReportInput,
AutomateRunStatus
} from '@/modules/core/graph/generated/graphql'
mapGqlStatusToDbStatus,
validateStatusChange
} from '@/modules/automate/utils/automateFunctionRunStatus'
import { AutomateFunctionRunStatusReportInput } from '@/modules/core/graph/generated/graphql'
import { Automate } from '@speckle/shared'
import { difference, groupBy, keyBy, mapValues, reduce, uniqBy } from 'lodash'

const AutomationRunStatusOrder: Array<AutomationRunStatus | AutomationRunStatus[]> = [
AutomationRunStatuses.pending,
AutomationRunStatuses.running,
[
AutomationRunStatuses.error,
AutomationRunStatuses.failure,
AutomationRunStatuses.success
]
]

export const mapGqlStatusToDbStatus = (status: AutomateRunStatus) => {
switch (status) {
case AutomateRunStatus.Initializing:
return AutomationRunStatuses.pending
case AutomateRunStatus.Running:
return AutomationRunStatuses.running
case AutomateRunStatus.Succeeded:
return AutomationRunStatuses.success
case AutomateRunStatus.Failed:
return AutomationRunStatuses.failure
}
}

export const mapDbStatusToGqlStatus = (status: AutomationRunStatus) => {
switch (status) {
case AutomationRunStatuses.pending:
return AutomateRunStatus.Initializing
case AutomationRunStatuses.running:
return AutomateRunStatus.Running
case AutomationRunStatuses.success:
return AutomateRunStatus.Succeeded
case AutomationRunStatuses.failure:
case AutomationRunStatuses.error:
return AutomateRunStatus.Failed
}
}

const validateStatusChange = (
previousStatus: AutomationRunStatus,
newStatus: AutomationRunStatus
) => {
if (previousStatus === newStatus) return

const previousStatusIndex = AutomationRunStatusOrder.findIndex((s) =>
Array.isArray(s) ? s.includes(previousStatus) : s === previousStatus
)
const newStatusIndex = AutomationRunStatusOrder.findIndex((s) =>
Array.isArray(s) ? s.includes(newStatus) : s === newStatus
)

if (newStatusIndex <= previousStatusIndex) {
throw new FunctionRunReportStatusesError(
`Invalid status change. Attempting to move from '${previousStatus}' to '${newStatus}'.`
)
}
}

const validateContextView = (contextView: string) => {
if (!contextView.length) {
throw new FunctionRunReportStatusesError(
Expand Down Expand Up @@ -112,13 +56,13 @@ export const resolveStatusFromFunctionRunStatuses = (
const anyRunning = functionRunStatuses.includes(AutomationRunStatuses.running)
if (anyRunning) return AutomationRunStatuses.running

const anyError = functionRunStatuses.includes(AutomationRunStatuses.error)
if (anyError) return AutomationRunStatuses.error
const anyError = functionRunStatuses.includes(AutomationRunStatuses.exception)
if (anyError) return AutomationRunStatuses.exception

const anyFailure = functionRunStatuses.includes(AutomationRunStatuses.failure)
if (anyFailure) return AutomationRunStatuses.failure
const anyFailure = functionRunStatuses.includes(AutomationRunStatuses.failed)
if (anyFailure) return AutomationRunStatuses.failed

return AutomationRunStatuses.success
return AutomationRunStatuses.succeeded
}

export type ReportFunctionRunStatusesDeps = {
Expand Down
52 changes: 48 additions & 4 deletions packages/server/modules/automate/services/trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
getFullAutomationRevisionMetadata,
getAutomationToken,
getAutomationTriggerDefinitions,
upsertAutomationRun
upsertAutomationRun,
upsertAutomationFunctionRun,
getFunctionRun
} from '@/modules/automate/repositories/automations'
import {
AutomationWithRevision,
Expand All @@ -14,7 +16,8 @@ import {
VersionCreatedTriggerManifest,
VersionCreationTriggerType,
BaseTriggerManifest,
isVersionCreatedTriggerManifest
isVersionCreatedTriggerManifest,
AutomationFunctionRunRecord
} from '@/modules/automate/helpers/types'
import { getBranchLatestCommits } from '@/modules/core/repositories/branches'
import { getCommit } from '@/modules/core/repositories/commits'
Expand Down Expand Up @@ -42,6 +45,7 @@ import {
} from '@/modules/automate/services/encryption'
import { LibsodiumEncryptionError } from '@/modules/shared/errors/encryption'
import { AutomateRunsEmitter } from '@/modules/automate/events/runs'
import { validateStatusChange } from '@/modules/automate/utils/automateFunctionRunStatus'

export type OnModelVersionCreateDeps = {
getTriggers: typeof getActiveTriggerDefinitions
Expand Down Expand Up @@ -177,6 +181,46 @@ const createAutomationRunData =
return automationRun
}

export type SetFunctionRunStatusReportDeps = {
getAutomationFunctionRunRecord: typeof getFunctionRun
upsertAutomationFunctionRunRecord: typeof upsertAutomationFunctionRun
}

export const setFunctionRunStatusReport =
(deps: SetFunctionRunStatusReportDeps) =>
async (
params: Pick<
AutomationFunctionRunRecord,
'runId' | 'status' | 'statusMessage' | 'contextView' | 'results'
>
): Promise<boolean> => {
const { getAutomationFunctionRunRecord, upsertAutomationFunctionRunRecord } = deps
const { runId, ...statusReportData } = params

const currentFunctionRunRecord = await getAutomationFunctionRunRecord(runId)

if (!currentFunctionRunRecord) {
return false
gjedlicska marked this conversation as resolved.
Show resolved Hide resolved
}

const currentStatus = currentFunctionRunRecord.status
const nextStatus = statusReportData.status

validateStatusChange(currentStatus, nextStatus)

const elapsed = new Date().getTime() - currentFunctionRunRecord.createdAt.getTime()

const nextFunctionRunRecord: AutomationFunctionRunRecord = {
...currentFunctionRunRecord,
...statusReportData,
elapsed
}

await upsertAutomationFunctionRunRecord(nextFunctionRunRecord)

return true
}

export type TriggerAutomationRevisionRunDeps = {
automateRunTrigger: typeof triggerAutomationRun
} & CreateAutomationRunDataDeps
Expand Down Expand Up @@ -260,10 +304,10 @@ export const triggerAutomationRevisionRun =
await upsertAutomationRun(automationRun)
} catch (error) {
const statusMessage = error instanceof Error ? error.message : `${error}`
automationRun.status = 'error'
automationRun.status = 'exception'
automationRun.functionRuns = automationRun.functionRuns.map((fr) => ({
...fr,
status: 'error',
status: 'exception',
statusMessage
}))
await upsertAutomationRun(automationRun)
Expand Down
Loading