Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bin/core/src/api/execute/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::OnceLock,
time::Duration,
};

use anyhow::Context as _;
Expand Down Expand Up @@ -133,6 +134,10 @@ impl Resolve<ExecuteArgs> for RunAction {

let mut update = update.clone();

let timeout = (action.config.execution_timeout > 0).then(|| {
Duration::from_secs(action.config.execution_timeout as u64)
});

update_update(update.clone()).await?;

let default_args = parse_action_arguments(
Expand Down Expand Up @@ -219,7 +224,7 @@ impl Resolve<ExecuteArgs> for RunAction {
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
CommandOptions::default().cancel(cancel),
CommandOptions::default().cancel(cancel).timeout(timeout),
)
.await;

Expand Down
12 changes: 12 additions & 0 deletions client/core/rs/src/entities/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ pub struct ActionConfig {
#[builder(default)]
pub reload_deno_deps: bool,

/// Maximum seconds the Action may run before its process group is
/// killed and the run failed. 0 or below means no timeout.
#[serde(default = "default_execution_timeout")]
#[builder(default = "default_execution_timeout()")]
#[partial_default(default_execution_timeout())]
pub execution_timeout: i32,

/// Typescript file contents using pre-initialized `komodo` client.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
Expand Down Expand Up @@ -204,6 +211,10 @@ fn default_run_at_startup() -> bool {
false
}

fn default_execution_timeout() -> i32 {
0
}

fn default_webhook_enabled() -> bool {
true
}
Expand All @@ -227,6 +238,7 @@ impl Default for ActionConfig {
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
reload_deno_deps: Default::default(),
execution_timeout: default_execution_timeout(),
arguments_format: Default::default(),
file_contents: Default::default(),
arguments: Default::default(),
Expand Down
5 changes: 5 additions & 0 deletions client/core/ts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export interface ActionConfig {
* this can usually be kept false outside of development.
*/
reload_deno_deps?: boolean;
/**
* Maximum seconds the Action may run before its process group is
* killed and the run failed. 0 or below means no timeout.
*/
execution_timeout: number;
/**
* Typescript file contents using pre-initialized `komodo` client.
* Supports variable / secret interpolation.
Expand Down
8 changes: 8 additions & 0 deletions ui/public/schema/resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -5042,6 +5042,14 @@
"null"
]
},
"execution_timeout": {
"description": "Maximum seconds the Action may run before its process group is\nkilled and the run failed. 0 or below means no timeout.",
"type": [
"integer",
"null"
],
"format": "int32"
},
"file_contents": {
"description": "Typescript file contents using pre-initialized `komodo` client.\nSupports variable / secret interpolation.",
"type": [
Expand Down
67 changes: 66 additions & 1 deletion ui/src/resources/action/config.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Anchor, Group, Select, Stack, Text, TextInput } from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import { useState } from "react";
import { notifications } from "@mantine/notifications";
import { useEffect, useState } from "react";
import { Types } from "komodo_client";
import {
usePermissions,
Expand Down Expand Up @@ -139,6 +140,19 @@ export default function ActionConfig({ id }: { id: string }) {
},
},
},
{
label: "Timeout",
labelHidden: true,
fields: {
execution_timeout: (execution_timeout, set) => (
<ExecutionTimeout
arg={execution_timeout}
set={set}
disabled={disabled}
/>
),
},
},
{
label: "Schedule",
description:
Expand Down Expand Up @@ -298,6 +312,57 @@ export default function ActionConfig({ id }: { id: string }) {
);
}

function ExecutionTimeout({
arg,
set,
disabled,
}: {
arg: number;
set: (input: Partial<Types.ActionConfig>) => void;
disabled: boolean;
}) {
const [input, setInput] = useState(arg.toString());
useEffect(() => {
setInput(arg.toString());
}, [arg]);
// Integral and within i32, matching the server field.
const valid = (value: string) => {
const num = Number(value);
return Number.isInteger(num) && num >= -2147483648 && num <= 2147483647;
};
const error = valid(input)
? undefined
: "Timeout must be a whole number of seconds";
return (
<ConfigItem
label="Execution timeout"
description="Maximum time the Action may run before its process group is killed and the run is failed. 0 or below disables the timeout."
>
<Group gap="xs">
<TextInput
w={100}
placeholder="time in seconds"
value={input}
onChange={(e) => setInput(e.target.value)}
onBlur={(e) => {
if (valid(e.target.value)) {
set({ execution_timeout: Number(e.target.value) });
} else {
notifications.show({
message: "Execution timeout must be a whole number of seconds",
color: "red",
});
}
}}
error={error}
disabled={disabled}
/>
seconds
</Group>
</ConfigItem>
);
}

const defaultArguments = (format: Types.FileFormat) => {
switch (format) {
case Types.FileFormat.KeyValue:
Expand Down
Loading