fix(backend): improve schedule queries plan to leverage indices better for performance (#5273)

* backend: fix schedule queries plan

* rework

* fix skipped as non success

* update sqlx

* update

* all

* fix npm run check

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Lucas Abel
2025-02-12 16:34:03 +01:00
committed by GitHub
parent 55cff6d2d2
commit bf206515e8
46 changed files with 757 additions and 1549 deletions
@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE postgres_trigger \n SET \n server_id = $1, \n last_server_ping = now(),\n error = 'Connecting...'\n WHERE \n enabled IS TRUE \n AND workspace_id = $2 \n AND path = $3 \n AND (last_server_ping IS NULL \n OR last_server_ping < now() - INTERVAL '15 seconds'\n ) \n RETURNING true\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "01e92a4ba3074f1dce6ec98bc6c3fad4878f48db8c17c6d58590bd5df2e3350a"
}
@@ -1,198 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM v2_as_completed_job WHERE\n v2_as_completed_job.schedule_path = schedule.path AND v2_as_completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "schedule",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "args",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "timezone",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 17,
"name": "on_failure_extra_args",
"type_info": "Json"
},
{
"ordinal": 18,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"name": "on_recovery_extra_args",
"type_info": "Json"
},
{
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 26,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 27,
"name": "on_success_extra_args",
"type_info": "Json"
},
{
"ordinal": 28,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 29,
"name": "jobs",
"type_info": "JsonArray"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true,
null
]
},
"hash": "099894523449a70eb301ecd1d744210d39f405016ffec374e4b15a2528baccb5"
}
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n schemaname AS schema_name,\n tablename AS table_name,\n attnames AS columns,\n rowfilter AS where_clause\n FROM\n pg_publication_tables\n WHERE\n pubname = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "schema_name",
"type_info": "Name"
},
{
"ordinal": 1,
"name": "table_name",
"type_info": "Name"
},
{
"ordinal": 2,
"name": "columns",
"type_info": "NameArray"
},
{
"ordinal": 3,
"name": "where_clause",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Name"
]
},
"nullable": [
true,
true,
true,
true
]
},
"hash": "199a76c04e3f0891ad09af27b9534bbabdd8703bfdf4d43df2c65e50d4ca2c85"
}
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1 FROM v2_job j JOIN v2_job_queue USING (id)\n WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2 AND runnable_path = $4\n AND parent_job IS NULL\n AND scheduled_for = $3\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Timestamptz",
"Text"
]
},
"nullable": [
null
]
},
"hash": "3a47d8ec0f4ba1644951f0c88504c9f769ffe5004871a049e4232e0092532062"
}
@@ -1,118 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters: _",
"type_info": "JsonbArray"
},
{
"ordinal": 14,
"name": "initial_messages: _",
"type_info": "JsonbArray"
},
{
"ordinal": 15,
"name": "url_runnable_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"name": "can_return_message",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
false,
false,
true,
true,
false
]
},
"hash": "3d0f036a3176dcc787bef3f10a336c93f43dec5df570f869580056702b7d1e09"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_6",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "42177e249794a4b7b945b93efd853afc1f1ad354599de11ebc6e256b58543265"
}
@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n schedule.path, t.jobs FROM schedule,\n LATERAL(SELECT ARRAY(\n SELECT json_build_object('id', id, 'success', status = 'success', 'duration_ms', duration_ms)\n FROM v2_job_completed c JOIN v2_job j USING (id)\n WHERE trigger_kind = 'schedule'\n AND trigger = schedule.path\n AND c.workspace_id = $1\n AND j.workspace_id = $1\n AND parent_job IS NULL AND runnable_path = schedule.script_path\n AND status <> 'skipped'\n ORDER BY created_at DESC\n LIMIT 20\n ) AS jobs) t\n WHERE workspace_id = $1\n ORDER BY edited_at DESC\n LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "jobs",
"type_info": "JsonArray"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
null
]
},
"hash": "430ea56dea72c7d326735952bf85708dcc4811857b7e5631f0decf5d75ef3aa3"
}
@@ -1,119 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 12,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 13,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "is_static_website",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "4331bb1a3559f56c1ee91916b7f15251f0b32afa2445401170d931e0fe1febb8"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS concurrency_limit_stats_completed_job",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "4d3bbcc029ec0926bf97de4b5ef97dfd5eeda5977a8746be77c8aa65a7ec299e"
}
@@ -1,118 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM nats_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "nats_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "subjects",
"type_info": "VarcharArray"
},
{
"ordinal": 3,
"name": "stream_name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "consumer_name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "use_jetstream",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 15,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false
]
},
"hash": "59368ac2d4e0918c7ee4275a0b1e3935e9d57be68a1811ffdee904a5f56e7023"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_started_at_new",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "608393951c85d9e721b506d2f6a7a47667b6f9f3dc0d51987cce159433459ab0"
}
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id\n FROM v2_job j JOIN v2_job_queue USING (id)\n WHERE j.workspace_id = $2 AND trigger_kind = 'schedule' AND trigger = $1 AND runnable_path = $4\n AND parent_job IS NULL\n AND j.id != $3\n AND running = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Uuid",
"Text"
]
},
"nullable": [
false
]
},
"hash": "6513c1433dbfe03f7c778963a05d964fda13a1091a8206ee174ed3a161248126"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS root_job_index_by_path_2",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "6536214f31e9d600e868b01385d8c6395e2440ea27553b7ccb18d7149b106728"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_2",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "74754b03304a69391d61560848e89569fe5666e36ca2650dabd9b494dc30f435"
}
@@ -1,106 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM postgres_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "postgres_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "replication_slot_name",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "publication_name",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
false,
false,
false
]
},
"hash": "7f6649b177f4ec948e396e179ea044812f8103498080a24246063e06b0ebfccf"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n success AS \"success!\",\n result AS \"result: Json<Box<RawValue>>\",\n started_at AS \"started_at!\"FROM v2_as_completed_job WHERE workspace_id = $1 AND schedule_path = $2 AND script_path = $3 AND id != $4\n ORDER BY created_at DESC\n LIMIT $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "result: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "started_at!",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Uuid",
"Int8"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "830297547ea33969f96a5c4c2b82a540f1d147948e1b1a7523b21151ffa22305"
}
@@ -1,25 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n postgres_trigger\n SET \n last_server_ping = now(),\n error = $1\n WHERE\n workspace_id = $2\n AND path = $3\n AND server_id = $4 \n AND enabled IS TRUE\n RETURNING 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "833a4ecec12dfe67f28016a135ffe682b023d1868a182b7cac16ce799433c257"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_3",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "85181656012b18cd26998128c30b1bf1f70e17486a9d315db130852d5e325200"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n success AS \"success!\",\n result AS \"result: Json<Box<RawValue>>\",\n started_at AS \"started_at!\"\n FROM v2_as_completed_job\n WHERE workspace_id = $1 AND schedule_path = $2 AND script_path = $3 AND id != $4\n ORDER BY created_at DESC\n LIMIT $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "result: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "started_at!",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Uuid",
"Int8"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "86cc1e3c18e936a700d8842a51a6f8377ec190669e2e22f8d511871d6fbe07b8"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT status = 'success' AS \"success!\"\n FROM v2_job j JOIN v2_job_completed USING (id)\n WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2\n AND parent_job IS NULL\n AND runnable_path = $3\n AND j.id != $4\n ORDER BY created_at DESC\n LIMIT $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Uuid",
"Int8"
]
},
"nullable": [
null
]
},
"hash": "910b9b8afb3df5e437e43ff4adc97d0a03f6c26f0b14fc92900f7600e70a7a8b"
}
@@ -1,130 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n ai_models,\n code_completion_model,\n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name,\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_invite_enabled!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "auto_invite_as!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "auto_invite_mode!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "error_handler",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "ai_resource",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 8,
"name": "code_completion_model",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 10,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 12,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 17,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null,
true,
true,
true,
true,
false,
true,
true,
false,
true,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "91878f06c6e27d864bd50d8cd4adde4395ab6ef1181237a886fa398dcfa0b589"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS scheduled_root_job",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "93586f1ffc7b8dbe62b21a21acfa6854a34a99d2bf107feaafc499588f7330a0"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n postgres_trigger \n SET\n last_server_ping = NULL \n WHERE \n workspace_id = $1 \n AND path = $2 \n AND server_id IS NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "95e420b60fba20b36b2c6675998587d8cad3b67d4dfa9de52777d4ea9490b6b7"
}
@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 FROM v2_as_queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Timestamptz"
]
},
"nullable": [
null
]
},
"hash": "9e7e6fe1dfba032e586f64531e12529d57faf5e77f6792d5bda608ff9658d7c9"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE postgres_trigger SET enabled = FALSE, error = $1, server_id = NULL, last_server_ping = NULL WHERE workspace_id = $2 AND path = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a6c168c60bc8c42f70b18565e824efe29311aabfba6e09efa10bab6a551d658b"
}
@@ -1,106 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM kafka_trigger\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "kafka_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "topics",
"type_info": "VarcharArray"
},
{
"ordinal": 3,
"name": "group_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 14,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false
]
},
"hash": "a79b1d0884c02f92fd40b23c6181717ffa5e01250020cfb24e4bad276397dee2"
}
@@ -1,46 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members \n FROM usr u\n JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id\n RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_\n WHERE g_.workspace_id = $1 AND g_.name != 'all'\n GROUP BY g_.workspace_id, name, summary, extra_perms",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "members",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true,
false,
null
]
},
"hash": "ca5f42cb0e368d0817461600152aabefe718829fb6eae2f681c4c8328acc94b2"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "create index concurrently if not exists ix_v2_job_root_by_path\n on v2_job (workspace_id, runnable_path, created_at DESC)\n where parent_job is null",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "cb5a8545ea140ed69c7b70d8c08cb3c037309f09e84acbb7945a0466d6e9f576"
}
@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id AS \"id!\" FROM v2_as_queue WHERE schedule_path = $1 AND workspace_id = $2 AND id != $3 AND running = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "d6c8f4e49cf7b6db5c979c88e02bd41c7b823186ac056a0a676da85dc5d9a027"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_9",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "d949e8b91fbdf4c50c1c2cbc608cf0b385bff14faab495e049f2029790f76d25"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_8",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e00144305b880cca3994b53c4a080e111a159a749728ce5363100cf883bdf02a"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "SELECT status = 'success' AS \"success!\",\n result AS \"result: Json<Box<RawValue>>\",\n started_at AS \"started_at!\"FROM v2_job j JOIN v2_job_completed USING (id)\n WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2\n AND parent_job IS NULL\n AND runnable_path = $3\n AND j.id != $4\n ORDER BY created_at DESC\n LIMIT $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "result: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "started_at!",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Uuid",
"Int8"
]
},
"nullable": [
null,
true,
true
]
},
"hash": "e6a8ddfd74ebab55ede5989fd7d09b246b6f84e49c659bc8e2c7b66cfec6d976"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_7",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e9038a6fcfd8bdf4855c5919ce634b976cbe0280e7215862aaf5ef445663793d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_created_at",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "ebfd39f168722701fb63cd37aaef3741b9782d8e2d4b6ff6c104fdcac5f58323"
}
@@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM usr\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "operator",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "disabled",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "f96dd1dd944506c07ad58d178e9b6a09259fedca329d790c34e58703bf26f34c"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_5",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "fa94a9ee5514f6808d3813394e62c86a69b7b7378333322a57856f8e43c6cd77"
}
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,42 @@
-- Add up migration script here
CREATE OR REPLACE VIEW v2_as_completed_job AS
SELECT
j.id,
j.workspace_id,
j.parent_job,
j.created_by,
j.created_at,
c.duration_ms,
c.status = 'success' OR c.status = 'skipped' AS success,
j.runnable_id AS script_hash,
j.runnable_path AS script_path,
j.args,
c.result,
FALSE AS deleted,
j.raw_code,
c.status = 'canceled' AS canceled,
c.canceled_by,
c.canceled_reason,
j.kind AS job_kind,
CASE WHEN j.trigger_kind = 'schedule'::job_trigger_kind THEN j.trigger END
AS schedule_path,
j.permissioned_as,
COALESCE(c.flow_status, c.workflow_as_code_status) AS flow_status,
j.raw_flow,
j.flow_step_id IS NOT NULL AS is_flow_step,
j.script_lang AS language,
c.started_at,
c.status = 'skipped' AS is_skipped,
j.raw_lock,
j.permissioned_as_email AS email,
j.visible_to_owner,
c.memory_peak AS mem_peak,
j.tag,
j.priority,
NULL::TEXT AS logs,
c.result_columns,
j.script_entrypoint_override,
j.preprocessed
FROM v2_job_completed c
JOIN v2_job j USING (id)
;
+75
View File
@@ -504,6 +504,81 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
.await?;
});
run_windmill_migration!("add_ix_v2_II", &db, {
sqlx::query!(
"create index concurrently if not exists ix_v2_job_root_by_path
on v2_job (workspace_id, runnable_path, created_at DESC)
where parent_job is null"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_3"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_5"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_6"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_7"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_8"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_9"
)
.execute(db)
.await?;
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_created_at")
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_2"
)
.execute(db)
.await?;
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_started_at_new"
)
.execute(db)
.await?;
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS root_job_index_by_path_2")
.execute(db)
.await?;
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS scheduled_root_job")
.execute(db)
.await?;
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS concurrency_limit_stats_completed_job")
.execute(db)
.await?;
tracing::info!("Finished adding ix_v2_II migration");
});
run_windmill_migration!("fix_labeled_jobs_index", &db, {
tracing::info!("Special migration to add index concurrently on job labels 2");
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs")
+37 -34
View File
@@ -329,16 +329,29 @@ pub struct ListScheduleQuery {
pub path_start: Option<String>,
}
#[derive(sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
pub struct ScheduleLight {
pub workspace_id: String,
pub path: String,
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub timezone: String,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,
pub summary: Option<String>,
}
async fn list_schedule(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(lsq): Query<ListScheduleQuery>,
) -> JsonResult<Vec<Schedule>> {
) -> JsonResult<Vec<ScheduleLight>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(Pagination { per_page: lsq.per_page, page: lsq.page });
let mut sqlb = SqlBuilder::select_from("schedule")
.field("*")
.field("workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, summary")
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
@@ -357,7 +370,7 @@ async fn list_schedule(
sqlb.and_where_like_left("path", path_start);
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let rows = sqlx::query_as::<_, Schedule>(&sql)
let rows = sqlx::query_as::<_, ScheduleLight>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
@@ -366,36 +379,8 @@ async fn list_schedule(
#[derive(Serialize, Deserialize, Debug)]
pub struct ScheduleWJobs {
pub workspace_id: String,
pub path: String,
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub timezone: String,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
pub extra_perms: serde_json::Value,
pub email: String,
pub error: Option<String>,
pub on_failure: Option<String>,
pub on_failure_times: Option<i32>,
pub on_failure_exact: Option<bool>,
pub on_failure_extra_args: Option<serde_json::Value>,
pub on_recovery: Option<String>,
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub on_success: Option<String>,
pub on_success_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: bool,
pub retry: Option<serde_json::Value>,
pub jobs: Option<Vec<serde_json::Value>>,
pub summary: Option<String>,
pub no_flow_overlap: bool,
pub tag: Option<String>,
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
}
async fn list_schedule_with_jobs(
@@ -407,9 +392,27 @@ async fn list_schedule_with_jobs(
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(pagination);
let rows = sqlx::query_as!(ScheduleWJobs,
"SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM v2_as_completed_job WHERE
v2_as_completed_job.schedule_path = schedule.path AND v2_as_completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t
WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
// Query plan:
// - use of the `ix_completed_job_workspace_id_started_at_new_2` index first, then;
// - use of the `ix_v2_job_root_by_path` index; hence the `parent_job IS NULL` clause.
// - both `workspace_id = $1` checks are required to hit both indexes.
"SELECT
schedule.path, t.jobs FROM schedule,
LATERAL(SELECT ARRAY(
SELECT json_build_object('id', id, 'success', status = 'success', 'duration_ms', duration_ms)
FROM v2_job_completed c JOIN v2_job j USING (id)
WHERE trigger_kind = 'schedule'
AND trigger = schedule.path
AND c.workspace_id = $1
AND j.workspace_id = $1
AND parent_job IS NULL AND runnable_path = schedule.script_path
AND status <> 'skipped'
ORDER BY created_at DESC
LIMIT 20
) AS jobs) t
WHERE workspace_id = $1
ORDER BY edited_at DESC
LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
+22 -9
View File
@@ -1377,12 +1377,17 @@ async fn apply_schedule_handlers<'a, 'c, T: Serialize + Send + Sync>(
let exact = schedule.on_failure_exact.unwrap_or(false);
if times > 1 || exact {
let past_jobs = sqlx::query!(
"SELECT
success AS \"success!\",
result AS \"result: Json<Box<RawValue>>\",
started_at AS \"started_at!\"
FROM v2_as_completed_job
WHERE workspace_id = $1 AND schedule_path = $2 AND script_path = $3 AND id != $4
// Query plan:
// - use of the `ix_v2_job_root_by_path` index;
// hence the `parent_job IS NULL` clause.
// - select from `v2_job` first, then join with `v2_job_completed` to avoid a full
// table scan.
"SELECT status = 'success' AS \"success!\"
FROM v2_job j JOIN v2_job_completed USING (id)
WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2
AND parent_job IS NULL
AND runnable_path = $3
AND j.id != $4
ORDER BY created_at DESC
LIMIT $5",
&schedule.workspace_id,
@@ -1448,11 +1453,19 @@ async fn apply_schedule_handlers<'a, 'c, T: Serialize + Send + Sync>(
let tx = db.begin().await?;
let times = schedule.on_recovery_times.unwrap_or(1).max(1);
let past_jobs = sqlx::query!(
"SELECT
success AS \"success!\",
// Query plan:
// - use of the `ix_v2_job_root_by_path` index;
// hence the `parent_job IS NULL` clause.
// - select from `v2_job` first, then join with `v2_job_completed` to avoid a full
// table scan.
"SELECT status = 'success' AS \"success!\",
result AS \"result: Json<Box<RawValue>>\",
started_at AS \"started_at!\"\
FROM v2_as_completed_job WHERE workspace_id = $1 AND schedule_path = $2 AND script_path = $3 AND id != $4
FROM v2_job j JOIN v2_job_completed USING (id)
WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2
AND parent_job IS NULL
AND runnable_path = $3
AND j.id != $4
ORDER BY created_at DESC
LIMIT $5",
&schedule.workspace_id,
+14 -4
View File
@@ -9,7 +9,7 @@
use crate::push;
use crate::PushIsolationLevel;
use anyhow::Context;
use sqlx::{query_scalar, PgExecutor, Postgres, Transaction};
use sqlx::{PgExecutor, Postgres, Transaction};
use std::collections::HashMap;
use std::str::FromStr;
use windmill_common::db::Authed;
@@ -70,11 +70,21 @@ pub async fn push_scheduled_job<'c>(
// Scheduled events must be stored in the database in UTC
let next = next.with_timezone(&chrono::Utc);
let already_exists: bool = query_scalar!(
"SELECT EXISTS (SELECT 1 FROM v2_as_queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
let already_exists: bool = sqlx::query_scalar!(
// Query plan:
// - use of the `ix_v2_job_root_by_path` index; hence the `parent_job IS NULL` clause.
// - select from `v2_job` first, then join with `v2_job_queue` to avoid a full table scan
// on `scheduled_for = $3`.
"SELECT EXISTS (
SELECT 1 FROM v2_job j JOIN v2_job_queue USING (id)
WHERE j.workspace_id = $1 AND trigger_kind = 'schedule' AND trigger = $2 AND runnable_path = $4
AND parent_job IS NULL
AND scheduled_for = $3
)",
&schedule.workspace_id,
&schedule.path,
next
next,
&schedule.script_path
)
.fetch_one(&mut *tx)
.await?
+2 -1
View File
@@ -333,7 +333,8 @@ pub fn unsafe_raw(json: String) -> Box<RawValue> {
fn check_result_too_big(size: usize) -> error::Result<()> {
if *CLOUD_HOSTED && size > MAX_RESULT_SIZE {
return Err(error::Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
We highly recommend using object to store and pass heavy data (https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#read-a-file-from-s3-within-a-script)
Alternatively, if using this script as part of a flow, activate shared folder and use the shared folder to pass heavy data between steps.".to_owned()));
};
Ok(())
}
+16 -3
View File
@@ -1711,11 +1711,24 @@ async fn push_next_flow_job(
.await?;
if no_flow_overlap {
let overlapping = sqlx::query_scalar!(
"SELECT id AS \"id!\" FROM v2_as_queue WHERE schedule_path = $1 AND workspace_id = $2 AND id != $3 AND running = true",
// Query plan:
// - use of the `ix_v2_job_root_by_path` index; hence the `parent_job IS NULL`
// clause.
// - select from `v2_job` first, then join with `v2_job_queue` to avoid a full
// table scan on `running = true`.
"SELECT id
FROM v2_job j JOIN v2_job_queue USING (id)
WHERE j.workspace_id = $2 AND trigger_kind = 'schedule' AND trigger = $1 AND runnable_path = $4
AND parent_job IS NULL
AND j.id != $3
AND running = true",
flow_job.schedule_path.as_ref().unwrap(),
flow_job.workspace_id.as_str(),
flow_job.id
).fetch_all(db).await?;
flow_job.id,
flow_job.script_path.as_ref().unwrap()
)
.fetch_all(db)
.await?;
if overlapping.len() > 0 {
let overlapping_str = overlapping
.iter()
@@ -331,6 +331,6 @@
</div>
{:else if $app.lazyInitRequire == undefined}
{#each $app?.subgrids?.[subGridId] ?? [] as item}
<Component render={false} component={item.data} />
<Component selected={false} fullHeight={false} render={false} component={item.data} />
{/each}
{/if}
@@ -232,12 +232,9 @@
}
onMount(() => {
console.log(`on mount: `, $userStore?.operator, $workspaceStore, $userWorkspaces)
loadQueryFilters()
})
$: $userWorkspaces && $userStore && $workspaceStore && console.log(`user workspaces: `, $userWorkspaces)
$: updateQueryFilters(selectedFilterKind, filterUserFolders, filterEnabledDisabled)
</script>
@@ -250,265 +247,267 @@
f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'}
/>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.schedules}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.schedules}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{:else}
<CenteredPage>
<PageHeader
title="Schedules"
tooltip="Trigger Scripts and Flows according to a cron schedule"
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
>
<Button size="md" startIcon={{ icon: Plus }} on:click={() => scheduleEditor.openNew(false)}>
New&nbsp;schedule
</Button>
</PageHeader>
<div class="w-full h-full flex flex-col">
<div class="w-full pb-4 pt-6">
<input type="text" placeholder="Search schedule" bind:value={filter} class="search-item" />
<div class="flex flex-row items-center gap-2 mt-6">
<div class="text-sm shrink-0"> Filter by path of </div>
<ToggleButtonGroup bind:selected={selectedFilterKind}>
<ToggleButton small value="schedule" label="Schedule" icon={Calendar} />
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
</ToggleButtonGroup>
</div>
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
<CenteredPage>
<PageHeader
title="Schedules"
tooltip="Trigger Scripts and Flows according to a cron schedule"
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
>
<Button size="md" startIcon={{ icon: Plus }} on:click={() => scheduleEditor.openNew(false)}>
New&nbsp;schedule
</Button>
</PageHeader>
<div class="w-full h-full flex flex-col">
<div class="w-full pb-4 pt-6">
<input type="text" placeholder="Search schedule" bind:value={filter} class="search-item" />
<div class="flex flex-row items-center gap-2 mt-6">
<div class="text-sm shrink-0"> Filter by path of </div>
<ToggleButtonGroup bind:selected={selectedFilterKind}>
<ToggleButton small value="schedule" label="Schedule" icon={Calendar} />
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
</ToggleButtonGroup>
</div>
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
<div class="flex flex-row items-center justify-end gap-4">
<ToggleButtonGroup class="h-6 w-auto" bind:selected={filterEnabledDisabled}>
<ToggleButton small value="all" label="All" />
<ToggleButton small value="enabled" label="Enabled" />
<ToggleButton small value="disabled" label="Disabled" />
</ToggleButtonGroup>
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Toggle
size="xs"
bind:checked={filterUserFolders}
options={{ right: `Only u/${$userStore.username} and f/*` }}
/>
{/if}
<div class="flex flex-row items-center justify-end gap-4">
<ToggleButtonGroup class="h-6 w-auto" bind:selected={filterEnabledDisabled}>
<ToggleButton small value="all" label="All" />
<ToggleButton small value="enabled" label="Enabled" />
<ToggleButton small value="disabled" label="Disabled" />
</ToggleButtonGroup>
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Toggle
size="xs"
bind:checked={filterUserFolders}
options={{ right: `Only u/${$userStore.username} and f/*` }}
/>
{/if}
</div>
</div>
</div>
{#if loading}
{#each new Array(6) as _}
<Skeleton layout={[[6], 0.4]} />
{/each}
{:else if !schedules?.length}
<div class="text-center text-sm text-tertiary mt-2"> No schedules </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, error, summary, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, args, marked, jobs, paused_until } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const avg_s = jobs
? jobs.reduce((acc, x) => acc + x.duration_ms, 0) / jobs.length
: undefined}
{#if loading}
{#each new Array(6) as _}
<Skeleton layout={[[6], 0.4]} />
{/each}
{:else if !schedules?.length}
<div class="text-center text-sm text-tertiary mt-2"> No schedules </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { path, error, summary, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, args, marked, jobs, paused_until } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const avg_s = jobs
? jobs.reduce((acc, x) => acc + x.duration_ms, 0) / jobs.length
: undefined}
<div
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
<div
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
>
<div class="w-full flex gap-5 items-center">
<RowIcon kind={is_flow ? 'flow' : 'script'} />
>
<div class="w-full flex gap-5 items-center">
<RowIcon kind={is_flow ? 'flow' : 'script'} />
<a
href="#{path}"
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
class="min-w-0 grow hover:underline decoration-gray-400"
>
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
{#if marked}
<span class="text-xs">
{@html marked}
</span>
{:else}
{summary || script_path}
{/if}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
schedule: {path}
</div>
</a>
{#if paused_until && new Date(paused_until) > new Date()}
<div class="pb-1">
<Badge color="yellow"
>Paused until {new Date(paused_until).toLocaleString()}</Badge
>
</div>
{/if}
<div class="gap-2 items-center hidden md:flex">
<Badge large color="blue">{schedule}</Badge>
<Badge small color="gray">{timezone}</Badge>
</div>
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
<div class="w-10">
{#if error}
<Popover notClickable>
<span class="flex h-4 w-4">
<Circle
class="text-red-600 animate-ping absolute inline-flex fill-current"
size={12}
/>
<Circle class="text-red-600 relative inline-flex fill-current" size={12} />
</span>
<div slot="text">
The schedule disabled itself because there was an error scheduling the next
job: {error}
</div>
</Popover>
{/if}
</div>
<Toggle
checked={enabled}
on:change={(e) => {
if (canWrite) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
/>
<div class="flex gap-2 items-center justify-end">
<Button
href={`${base}/runs/?schedule_path=${path}&show_schedules=true&show_future_jobs=true`}
size="xs"
startIcon={{ icon: List }}
color="light"
variant="border"
>
Runs
</Button>
<Button
<a
href="#{path}"
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
size="xs"
startIcon={{ icon: canWrite ? Pen : Eye }}
color="dark"
class="min-w-0 grow hover:underline decoration-gray-400"
>
{canWrite ? 'Edit' : 'View'}
</Button>
<Dropdown
items={[
{
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
icon: Eye,
action: () => {
goto(href)
}
},
{
displayName: 'Delete',
type: 'delete',
icon: Trash,
disabled: !canWrite,
action: async () => {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path
})
loadSchedules()
}
},
{
displayName: canWrite ? 'Edit' : 'View',
icon: canWrite ? Pen : Eye,
action: () => {
scheduleEditor?.openEdit(path, is_flow)
}
},
{
displayName: 'View runs',
icon: List,
href:
base +
'/runs/?schedule_path=' +
path +
'&show_schedules=true&show_future_jobs=true'
},
{
displayName: 'Audit logs',
icon: Eye,
href: `${base}/audit_logs?resource=${path}`
},
{
displayName: 'Run now',
icon: Play,
action: () => {
runScheduleNow(script_path, args, is_flow)
}
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: Share,
action: () => {
shareModal.openDrawer(path, 'schedule')
}
}
]}
/>
</div>
</div>
<div class="w-full flex justify-between items-baseline">
{#if loadingSchedulesWithJobStats}
<div class="flex gap-1 ml-0.5 text-[0.7em] text-tertiary items-center">
<Loader2 size={14} class="animate-spin" />
<span>Job stats loading...</span>
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
{#if marked}
<span class="text-xs">
{@html marked}
</span>
{:else}
{summary || script_path}
{/if}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
schedule: {path}
</div>
</a>
{#if paused_until && new Date(paused_until) > new Date()}
<div class="pb-1">
<Badge color="yellow"
>Paused until {new Date(paused_until).toLocaleString()}</Badge
>
</div>
{/if}
<div class="gap-2 items-center hidden md:flex">
<Badge large color="blue">{schedule}</Badge>
<Badge small color="gray">{timezone}</Badge>
</div>
{:else}
<div class="flex gap-1.5 ml-0.5 items-baseline flex-row-reverse">
{#if avg_s}
<div class="pl-2 text-tertiary text-2xs">Avg: {(avg_s / 1000).toFixed(2)}s</div>
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
<div class="w-10">
{#if error}
<Popover notClickable>
<span class="flex h-4 w-4">
<Circle
class="text-red-600 animate-ping absolute inline-flex fill-current"
size={12}
/>
<Circle class="text-red-600 relative inline-flex fill-current" size={12} />
</span>
<div slot="text">
The schedule disabled itself because there was an error scheduling the next
job: {error}
</div>
</Popover>
{/if}
{#each jobs ?? [] as job}
{@const h = (avg_s ? job.duration_ms / avg_s : 1) * 7 + 3}
<a href="{base}/run/{job.id}?workspace={$workspaceStore}">
<JobPreview id={job.id}>
<div>
<div
class="{job.success ? 'bg-green-300' : 'bg-red-300'} mx-auto w-1.5"
style="height: {h}px"
/>
<!-- <div class="text-[0.6em] mt-0.5 text-center text-tertiary"
</div>
<Toggle
checked={enabled}
on:change={(e) => {
if (canWrite) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
/>
<div class="flex gap-2 items-center justify-end">
<Button
href={`${base}/runs/?schedule_path=${path}&show_schedules=true&show_future_jobs=true`}
size="xs"
startIcon={{ icon: List }}
color="light"
variant="border"
>
Runs
</Button>
<Button
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
size="xs"
startIcon={{ icon: canWrite ? Pen : Eye }}
color="dark"
>
{canWrite ? 'Edit' : 'View'}
</Button>
<Dropdown
items={[
{
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
icon: Eye,
action: () => {
goto(href)
}
},
{
displayName: 'Delete',
type: 'delete',
icon: Trash,
disabled: !canWrite,
action: async () => {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path
})
loadSchedules()
}
},
{
displayName: canWrite ? 'Edit' : 'View',
icon: canWrite ? Pen : Eye,
action: () => {
scheduleEditor?.openEdit(path, is_flow)
}
},
{
displayName: 'View runs',
icon: List,
href:
base +
'/runs/?schedule_path=' +
path +
'&show_schedules=true&show_future_jobs=true'
},
{
displayName: 'Audit logs',
icon: Eye,
href: `${base}/audit_logs?resource=${path}`
},
{
displayName: 'Run now',
icon: Play,
action: () => {
runScheduleNow(script_path, args, is_flow)
}
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: Share,
action: () => {
shareModal.openDrawer(path, 'schedule')
}
}
]}
/>
</div>
</div>
<div class="w-full flex justify-between items-baseline">
{#if loadingSchedulesWithJobStats}
<div class="flex gap-1 ml-0.5 text-[0.7em] text-tertiary items-center">
<Loader2 size={14} class="animate-spin" />
<span>Job stats loading...</span>
</div>
{:else}
<div class="flex gap-1.5 ml-0.5 items-baseline flex-row-reverse">
{#if avg_s}
<div class="pl-2 text-tertiary text-2xs"
>Avg: {(avg_s / 1000).toFixed(2)}s</div
>
{/if}
{#each jobs ?? [] as job}
{@const h = (avg_s ? job.duration_ms / avg_s : 1) * 7 + 3}
<a href="{base}/run/{job.id}?workspace={$workspaceStore}">
<JobPreview id={job.id}>
<div>
<div
class="{job.success ? 'bg-green-300' : 'bg-red-300'} mx-auto w-1.5"
style="height: {h}px"
/>
<!-- <div class="text-[0.6em] mt-0.5 text-center text-tertiary"
>{(job.duration_ms / 1000).toFixed(2)}s</div
> -->
</div>
</JobPreview>
</a>
{/each}
</div>
{/if}
<div
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
><div class="truncate">edited by {edited_by}</div><div class="truncate"
>the {displayDate(edited_at)}</div
</div>
</JobPreview>
</a>
{/each}
</div>
{/if}
<div
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
><div class="truncate">edited by {edited_by}</div><div class="truncate"
>the {displayDate(edited_at)}</div
></div
></div
></div
>
</div>
{/each}
</div>
{:else}
<NoItemFound />
>
</div>
{/each}
</div>
{:else}
<NoItemFound />
{/if}
</div>
{#if items && items?.length > 15 && nbDisplayed < items.length}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
>
{/if}
</div>
{#if items && items?.length > 15 && nbDisplayed < items.length}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
>
{/if}
</CenteredPage>
</CenteredPage>
{/if}
<ShareModal