mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 00:01:37 +00:00
fix: improve jobs runs page performance (#6942)
This commit is contained in:
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "create index concurrently if not exists ix_job_created_at ON v2_job (created_at DESC)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3a5edf3dd884b5a8862bb112f6520967ed4a218782192c6c6fc1498f45d753a6"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_started_at_new_2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3dca0aded0ec744b084359e1a77dc4af312fda13d832f9e8b236655628e5b81c"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "create index concurrently if not exists ix_job_workspace_id_completed_at_all ON v2_job_completed (workspace_id, completed_at DESC)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5c403799772e1c44bd00e5376893a6428b3c7cbdef4af70a6660c1a5900d17a6"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "create index concurrently if not exists ix_v2_job_workspace_id_created_at ON v2_job (workspace_id, created_at DESC) where kind in ('script', 'flow', 'singlescriptflow') AND parent_job IS NULL",
|
||||
"query": "create index concurrently if not exists ix_v2_job_workspace_id_created_at ON v2_job (workspace_id, created_at DESC) where kind in ('script', 'flow', 'singlestepflow') AND parent_job IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d585aa6301c41308b02a1f0fbf068221e732e48dfa6e34d5b025adbbdcbb03e0"
|
||||
"hash": "5d99d2b058d4896f9ac1cea04fa35c003bc4e897ee01746d6f17aeee387d1505"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"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 completed_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": [
|
||||
{
|
||||
@@ -26,5 +26,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "430ea56dea72c7d326735952bf85708dcc4811857b7e5631f0decf5d75ef3aa3"
|
||||
"hash": "5e6dcb3e7a9bc174a040cfba96555eb5140c563a2b191714ef9028b56391b794"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "69550451b86f221a3d2ef626be7073fc77421710992b10f526aa36bc64ff0930"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_job_created_at",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6ad24aef02d86ea507d232ad0bbf798240d4fcdd52ce2745532ca30f736d25ca"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "create index concurrently if not exists ix_completed_job_workspace_id_started_at_new_2 ON v2_job_completed (workspace_id, started_at DESC)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "931703a98d2ee5fb58d3380896baaee032e731db1e6bd49d991a54f49ab8fa46"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP INDEX CONCURRENTLY IF EXISTS ix_v2_job_root_by_path",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ea9385509319f66b9330221eb50b85edafb9408d0306f17bb78b65a5d81c570b"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_job_v2_job_root_by_path_2 ON v2_job (workspace_id, runnable_path) WHERE parent_job IS NULL;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f17c9fed09897191ee70214223d4b83a05f1d649bfedff9f24cc3e6c702d42df"
|
||||
}
|
||||
@@ -8058,11 +8058,12 @@ paths:
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedBefore"
|
||||
- $ref: "#/components/parameters/CompletedBefore"
|
||||
- $ref: "#/components/parameters/CompletedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBeforeQueue"
|
||||
- $ref: "#/components/parameters/CreatedAfterQueue"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ScheduledForBeforeNow"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfterCompletedJob"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
@@ -8272,19 +8273,18 @@ paths:
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedBefore"
|
||||
- $ref: "#/components/parameters/CompletedBefore"
|
||||
- $ref: "#/components/parameters/CompletedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBeforeQueue"
|
||||
- $ref: "#/components/parameters/CreatedAfterQueue"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ScheduledForBeforeNow"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfterCompletedJob"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/AllowWildcards"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -14245,11 +14245,12 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedBefore"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ScheduledForBeforeNow"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfterCompletedJob"
|
||||
- $ref: "#/components/parameters/CompletedBefore"
|
||||
- $ref: "#/components/parameters/CompletedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBeforeQueue"
|
||||
- $ref: "#/components/parameters/CreatedAfterQueue"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
@@ -14838,33 +14839,40 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedOrStartedAfter:
|
||||
name: created_or_started_after
|
||||
description:
|
||||
filter on created_at for non non started job and started_at otherwise
|
||||
after (exclusive) timestamp
|
||||
|
||||
CompletedBefore:
|
||||
name: completed_before
|
||||
description: filter on started before (inclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedOrStartedAfterCompletedJob:
|
||||
name: created_or_started_after_completed_jobs
|
||||
description:
|
||||
filter on created_at for non non started job and started_at otherwise
|
||||
after (exclusive) timestamp but only for the completed jobs
|
||||
CompletedAfter:
|
||||
name: completed_after
|
||||
description: filter on started after (exclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedOrStartedBefore:
|
||||
name: created_or_started_before
|
||||
|
||||
CreatedAfterQueue:
|
||||
name: created_after_queue
|
||||
description:
|
||||
filter on created_at for non non started job and started_at otherwise
|
||||
before (inclusive) timestamp
|
||||
filter on jobs created after X for jobs in the queue only
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CreatedBeforeQueue:
|
||||
name: created_before_queue
|
||||
description:
|
||||
filter on jobs created before X for jobs in the queue only
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Success:
|
||||
name: success
|
||||
description: filter on successful jobs
|
||||
@@ -15669,6 +15677,9 @@ components:
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
completed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
duration_ms:
|
||||
type: integer
|
||||
success:
|
||||
@@ -16302,30 +16313,30 @@ components:
|
||||
ScriptLang:
|
||||
type: string
|
||||
enum: [
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
powershell,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
snowflake,
|
||||
mssql,
|
||||
oracledb,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
php,
|
||||
rust,
|
||||
ansible,
|
||||
csharp,
|
||||
nu,
|
||||
java,
|
||||
ruby,
|
||||
duckdb,
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
powershell,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
snowflake,
|
||||
mssql,
|
||||
oracledb,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
php,
|
||||
rust,
|
||||
ansible,
|
||||
csharp,
|
||||
nu,
|
||||
java,
|
||||
ruby,
|
||||
duckdb,
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
|
||||
Preview:
|
||||
type: object
|
||||
|
||||
@@ -166,7 +166,7 @@ async fn get_concurrent_intervals(
|
||||
.clone();
|
||||
let mut sqlb_c = SqlBuilder::select_from("v2_job_completed")
|
||||
.fields(UnifiedJob::completed_job_fields())
|
||||
.order_by("started_at", lq.order_desc.unwrap_or(true))
|
||||
.order_by("completed_at", lq.order_desc.unwrap_or(true))
|
||||
.limit(row_limit)
|
||||
.clone();
|
||||
let mut sqlb_q_user = SqlBuilder::select_from("v2_job_queue")
|
||||
@@ -176,7 +176,7 @@ async fn get_concurrent_intervals(
|
||||
.clone();
|
||||
let mut sqlb_c_user = SqlBuilder::select_from("v2_job_completed")
|
||||
.fields(&["id"])
|
||||
.order_by("started_at", lq.order_desc.unwrap_or(true))
|
||||
.order_by("completed_at", lq.order_desc.unwrap_or(true))
|
||||
.limit(row_limit)
|
||||
.clone();
|
||||
|
||||
@@ -209,6 +209,10 @@ async fn get_concurrent_intervals(
|
||||
started_after: _,
|
||||
created_before: _,
|
||||
created_after: _,
|
||||
created_before_queue: _,
|
||||
created_after_queue: _,
|
||||
completed_after: _,
|
||||
completed_before: _,
|
||||
created_or_started_before: _,
|
||||
created_or_started_after: _,
|
||||
created_or_started_after_completed_jobs: _,
|
||||
@@ -296,6 +300,7 @@ async fn get_concurrent_intervals(
|
||||
duration_ms: j.duration_ms,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let jobs = running_jobs_db
|
||||
.into_iter()
|
||||
.filter(|j| running_jobs_user.iter().any(|id| j.id == *id))
|
||||
@@ -306,6 +311,7 @@ async fn get_concurrent_intervals(
|
||||
)
|
||||
.map(From::from)
|
||||
.collect();
|
||||
|
||||
Ok(Json(ExtendedJobs {
|
||||
jobs,
|
||||
obscured_jobs,
|
||||
|
||||
@@ -778,7 +778,7 @@ macro_rules! get_job_query {
|
||||
("v2_job_completed", $($opts:tt)*) => {
|
||||
get_job_query!(
|
||||
@impl "v2_job_completed", ($($opts)*),
|
||||
"v2_job_completed.duration_ms, CASE WHEN status = 'success' OR status = 'skipped' THEN true ELSE false END as success, result_columns, deleted, status = 'skipped' as is_skipped, result->'wm_labels' as labels, \
|
||||
"v2_job_completed.duration_ms, v2_job_completed.completed_at, CASE WHEN status = 'success' OR status = 'skipped' THEN true ELSE false END as success, result_columns, deleted, status = 'skipped' as is_skipped, result->'wm_labels' as labels, \
|
||||
CASE WHEN result is null or pg_column_size(result) < 90000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result",
|
||||
"",
|
||||
)
|
||||
@@ -1820,8 +1820,8 @@ impl From<ListCompletedQuery> for ListQueueQuery {
|
||||
created_by: lcq.created_by,
|
||||
started_before: lcq.started_before,
|
||||
started_after: lcq.started_after,
|
||||
created_before: lcq.created_before,
|
||||
created_after: lcq.created_after,
|
||||
created_before: lcq.created_before_queue.or(lcq.created_before),
|
||||
created_after: lcq.created_after_queue.or(lcq.created_after),
|
||||
created_or_started_before: lcq.created_or_started_before,
|
||||
created_or_started_after: lcq.created_or_started_after,
|
||||
worker: lcq.worker,
|
||||
@@ -1855,11 +1855,11 @@ pub fn filter_list_queue_query(
|
||||
if join_outstanding_wait_times {
|
||||
sqlb.left()
|
||||
.join("outstanding_wait_time")
|
||||
.on_eq("v2_job.id", "outstanding_wait_time.job_id");
|
||||
.on_eq("v2_job_queue.id", "outstanding_wait_time.job_id");
|
||||
}
|
||||
|
||||
if w_id != "admins" || !lq.all_workspaces.is_some_and(|x| x) {
|
||||
sqlb.and_where_eq("v2_job.workspace_id", "?".bind(&w_id));
|
||||
sqlb.and_where_eq("v2_job_queue.workspace_id", "?".bind(&w_id));
|
||||
}
|
||||
|
||||
if let Some(w) = &lq.worker {
|
||||
@@ -1960,8 +1960,7 @@ pub fn filter_list_queue_query(
|
||||
}
|
||||
|
||||
if lq.is_not_schedule.unwrap_or(false) {
|
||||
sqlb.and_where("trigger_kind != 'schedule'")
|
||||
.or_where("trigger_kind IS NULL");
|
||||
sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'");
|
||||
}
|
||||
|
||||
sqlb
|
||||
@@ -2360,10 +2359,13 @@ async fn list_jobs(
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(lq): Query<ListCompletedQuery>,
|
||||
) -> error::JsonResult<Vec<Job>> {
|
||||
let limit = pagination.per_page.unwrap_or(1000);
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let lqc = lq.clone();
|
||||
|
||||
if offset > 0 {
|
||||
tracing::warn!("offset is not 0, but is ignored for list_jobs. Use created_before or completed_before instead.");
|
||||
}
|
||||
|
||||
if lq.success.is_some() && lq.running.is_some_and(|x| x) {
|
||||
return Err(error::Error::BadRequest(
|
||||
"cannot specify both success and running".to_string(),
|
||||
@@ -2372,7 +2374,7 @@ async fn list_jobs(
|
||||
let sqlc = if lq.running.is_none() {
|
||||
Some(list_completed_jobs_query(
|
||||
&w_id,
|
||||
Some(per_page + offset),
|
||||
Some(per_page),
|
||||
0,
|
||||
&ListCompletedQuery { order_desc: Some(true), ..lqc },
|
||||
UnifiedJob::completed_job_fields(),
|
||||
@@ -2392,19 +2394,13 @@ async fn list_jobs(
|
||||
&w_id,
|
||||
&ListQueueQuery { order_desc: Some(true), ..lq.into() },
|
||||
UnifiedJob::queued_job_fields(),
|
||||
Pagination { per_page: Some(limit), page: None },
|
||||
Pagination { per_page: None, page: None },
|
||||
true,
|
||||
get_scope_tags(&authed),
|
||||
);
|
||||
|
||||
if let Some(sqlc) = sqlc {
|
||||
format!(
|
||||
"{} UNION ALL {} LIMIT {} OFFSET {};",
|
||||
&sqlq.subquery()?,
|
||||
&sqlc.subquery()?,
|
||||
per_page,
|
||||
offset
|
||||
)
|
||||
format!("{} UNION ALL {}", &sqlq.subquery()?, &sqlc.subquery()?,)
|
||||
} else {
|
||||
sqlq.limit(per_page).offset(offset).query()?
|
||||
}
|
||||
@@ -2418,6 +2414,7 @@ async fn list_jobs(
|
||||
}
|
||||
sqlc.unwrap().limit(per_page).offset(offset).query()?
|
||||
};
|
||||
// tracing::info!("sql: {}", sql);
|
||||
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
|
||||
|
||||
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql)
|
||||
@@ -3295,6 +3292,7 @@ pub struct UnifiedJob {
|
||||
pub created_by: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub running: Option<bool>,
|
||||
pub script_hash: Option<ScriptHash>,
|
||||
@@ -3327,13 +3325,14 @@ pub struct UnifiedJob {
|
||||
|
||||
const CJ_FIELDS: &[&str] = &[
|
||||
"'CompletedJob' as typ",
|
||||
"v2_job.id",
|
||||
"v2_job.workspace_id",
|
||||
"v2_job_completed.id",
|
||||
"v2_job_completed.workspace_id",
|
||||
"v2_job.parent_job",
|
||||
"v2_job.created_by",
|
||||
"v2_job.created_at",
|
||||
"v2_job_completed.started_at",
|
||||
"null as scheduled_for",
|
||||
"v2_job_completed.completed_at",
|
||||
"null as running",
|
||||
"v2_job.runnable_id as script_hash",
|
||||
"v2_job.runnable_path as script_path",
|
||||
@@ -3366,13 +3365,14 @@ const CJ_FIELDS: &[&str] = &[
|
||||
|
||||
const QJ_FIELDS: &[&str] = &[
|
||||
"'QueuedJob' as typ",
|
||||
"v2_job.id",
|
||||
"v2_job.workspace_id",
|
||||
"v2_job_queue.id",
|
||||
"v2_job_queue.workspace_id",
|
||||
"v2_job.parent_job",
|
||||
"v2_job.created_by",
|
||||
"v2_job.created_at",
|
||||
"v2_job_queue.created_at",
|
||||
"v2_job_queue.started_at",
|
||||
"v2_job_queue.scheduled_for",
|
||||
"null as completed_at",
|
||||
"v2_job_queue.running",
|
||||
"v2_job.runnable_id as script_hash",
|
||||
"v2_job.runnable_path as script_path",
|
||||
@@ -3425,6 +3425,7 @@ impl<'a> From<UnifiedJob> for Job {
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at,
|
||||
completed_at: uj.completed_at,
|
||||
duration_ms: uj.duration_ms.unwrap(),
|
||||
success: uj.success.unwrap(),
|
||||
script_hash: uj.script_hash,
|
||||
@@ -7507,7 +7508,7 @@ pub fn filter_list_completed_query(
|
||||
if join_outstanding_wait_times {
|
||||
sqlb.left()
|
||||
.join("outstanding_wait_time")
|
||||
.on_eq("v2_job.id", "outstanding_wait_time.job_id");
|
||||
.on_eq("v2_job_completed.id", "outstanding_wait_time.job_id");
|
||||
}
|
||||
|
||||
if let Some(label) = &lq.label {
|
||||
@@ -7535,7 +7536,8 @@ pub fn filter_list_completed_query(
|
||||
}
|
||||
|
||||
if w_id != "admins" || !lq.all_workspaces.is_some_and(|x| x) {
|
||||
sqlb.and_where_eq("v2_job.workspace_id", "?".bind(&w_id));
|
||||
sqlb.and_where_eq("v2_job_completed.workspace_id", "?".bind(&w_id))
|
||||
.and_where_eq("v2_job.workspace_id", "?".bind(&w_id));
|
||||
}
|
||||
|
||||
if let Some(p) = &lq.schedule_path {
|
||||
@@ -7605,6 +7607,13 @@ pub fn filter_list_completed_query(
|
||||
sqlb.and_where_ge("started_at", "?".bind(&dt.to_rfc3339()));
|
||||
}
|
||||
|
||||
if let Some(dt) = &lq.completed_after {
|
||||
sqlb.and_where_ge("completed_at", "?".bind(&dt.to_rfc3339()));
|
||||
}
|
||||
if let Some(dt) = &lq.completed_before {
|
||||
sqlb.and_where_le("completed_at", "?".bind(&dt.to_rfc3339()));
|
||||
}
|
||||
|
||||
if let Some(sk) = &lq.is_skipped {
|
||||
if *sk {
|
||||
sqlb.and_where_eq("status", "'skipped'");
|
||||
@@ -7640,8 +7649,7 @@ pub fn filter_list_completed_query(
|
||||
}
|
||||
|
||||
if lq.is_not_schedule.unwrap_or(false) {
|
||||
sqlb.and_where("trigger_kind != 'schedule'")
|
||||
.or_where("trigger_kind IS NULL");
|
||||
sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'");
|
||||
}
|
||||
|
||||
sqlb
|
||||
@@ -7658,7 +7666,14 @@ pub fn list_completed_jobs_query(
|
||||
) -> SqlBuilder {
|
||||
let mut sqlb = SqlBuilder::select_from("v2_job_completed")
|
||||
.fields(fields)
|
||||
.order_by("v2_job.created_at", lq.order_desc.unwrap_or(true))
|
||||
.order_by(
|
||||
if lq.completed_before.is_some() || lq.completed_after.is_some() {
|
||||
"v2_job_completed.completed_at"
|
||||
} else {
|
||||
"v2_job.created_at"
|
||||
},
|
||||
lq.order_desc.unwrap_or(true),
|
||||
)
|
||||
.offset(offset)
|
||||
.clone();
|
||||
if let Some(per_page) = per_page {
|
||||
@@ -7687,6 +7702,10 @@ pub struct ListCompletedQuery {
|
||||
pub created_or_started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_or_started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_or_started_after_completed_jobs: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_before_queue: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after_queue: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub completed_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub completed_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub success: Option<bool>,
|
||||
pub running: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
@@ -7726,8 +7745,8 @@ async fn list_completed_jobs(
|
||||
offset,
|
||||
&lq,
|
||||
&[
|
||||
"v2_job.id",
|
||||
"v2_job.workspace_id",
|
||||
"v2_job_completed.id",
|
||||
"v2_job_completed.workspace_id",
|
||||
"v2_job.parent_job",
|
||||
"v2_job.created_by",
|
||||
"v2_job.created_at",
|
||||
|
||||
@@ -447,19 +447,6 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("create index concurrently if not exists ix_completed_job_workspace_id_started_at_new_2 ON v2_job_completed (workspace_id, started_at DESC)")
|
||||
.execute(db)
|
||||
.await?;
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("create index concurrently if not exists ix_job_root_job_index_by_path_2 ON v2_job (workspace_id, runnable_path, created_at desc) WHERE parent_job IS NULL")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS root_job_index_by_path_2")
|
||||
.execute(db)
|
||||
.await?;
|
||||
@@ -467,13 +454,6 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("create index concurrently if not exists ix_job_created_at ON v2_job (created_at DESC)")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query(
|
||||
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new_2",
|
||||
)
|
||||
@@ -523,9 +503,9 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
});
|
||||
|
||||
run_windmill_migration!("v2_improve_v2_job_indices_ii", &db, |tx| {
|
||||
sqlx::query!("create index concurrently if not exists ix_v2_job_workspace_id_created_at ON v2_job (workspace_id, created_at DESC) where kind in ('script', 'flow', 'singlescriptflow') AND parent_job IS NULL")
|
||||
.execute(db)
|
||||
.await?;
|
||||
sqlx::query!("create index concurrently if not exists ix_v2_job_workspace_id_created_at ON v2_job (workspace_id, created_at DESC) where kind in ('script', 'flow', 'singlestepflow') AND parent_job IS NULL")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS ix_job_workspace_id_created_at_new_6")
|
||||
.execute(db)
|
||||
@@ -609,5 +589,52 @@ WHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');"
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
|
||||
run_windmill_migration!("v2_job_completed_completed_at_9", db, |tx| {
|
||||
let migration_job_name = "v2_job_completed_completed_at";
|
||||
let mut i = 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
sqlx::query!("create index concurrently if not exists ix_job_workspace_id_completed_at_all ON v2_job_completed (workspace_id, completed_at DESC)")
|
||||
.execute(db)
|
||||
.await?;
|
||||
i += 1;
|
||||
|
||||
sqlx::query!("CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_job_v2_job_root_by_path_2 ON v2_job (workspace_id, runnable_path) WHERE parent_job IS NULL;")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("create index concurrently if not exists ix_job_root_job_index_by_path_2 ON v2_job (workspace_id, runnable_path, created_at desc) WHERE parent_job IS NULL")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!(
|
||||
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_started_at_new_2"
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
|
||||
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS ix_job_created_at")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
|
||||
sqlx::query!("DROP INDEX CONCURRENTLY IF EXISTS ix_v2_job_root_by_path")
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
i += 1;
|
||||
tracing::info!("step {i} of {migration_job_name} migration");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@ async fn list_schedule_with_jobs(
|
||||
AND j.workspace_id = $1
|
||||
AND parent_job IS NULL AND runnable_path = schedule.script_path
|
||||
AND status <> 'skipped'
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY completed_at DESC
|
||||
LIMIT 20
|
||||
) AS jobs) t
|
||||
WHERE workspace_id = $1
|
||||
|
||||
@@ -263,6 +263,7 @@ pub struct CompletedJob {
|
||||
pub created_by: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub duration_ms: i64,
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -2427,8 +2427,7 @@ pub async fn pull(
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
let has_concurent_limit = false
|
||||
|| (job.is_dependency() && job.concurrent_limit.is_some() && cfg!(feature = "private") && !*WMDEBUG_NO_DJOB_DEBOUNCING);
|
||||
let has_concurent_limit = job.is_dependency() && job.concurrent_limit.is_some() && cfg!(feature = "private") && !*WMDEBUG_NO_DJOB_DEBOUNCING;
|
||||
// if we don't have private flag, we don't have concurrency limit
|
||||
|
||||
// concurrency check. If more than X jobs for this path are already running, we re-queue and pull another job from the queue
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { HistoryIcon } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
|
||||
import HistoricInputs from './HistoricInputs.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { JobService } from '$lib/gen'
|
||||
|
||||
export let path: string
|
||||
export let selected: string | undefined = undefined
|
||||
export let selectInitial: boolean = false
|
||||
export let loading: boolean = false
|
||||
interface Props {
|
||||
path: string;
|
||||
selected?: string | undefined;
|
||||
selectInitial?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
path,
|
||||
selected = undefined,
|
||||
selectInitial = false,
|
||||
loading = $bindable(false)
|
||||
}: Props = $props();
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadInitial() {
|
||||
@@ -31,27 +40,33 @@
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadInitial()
|
||||
$effect(() => {
|
||||
$workspaceStore && untrack(() => loadInitial())
|
||||
});
|
||||
</script>
|
||||
|
||||
<PopoverV2 closeButton={false}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<HistoryIcon size={14} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="p-2 h-[400px] overflow-hidden w-80 border shadow-sm">
|
||||
<HistoricInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
dispatch('select', { jobId: e.detail?.jobId, initial: false })
|
||||
} else {
|
||||
dispatch('unselect')
|
||||
}
|
||||
}}
|
||||
{selected}
|
||||
runnableId={path}
|
||||
runnableType={'FlowPath'}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#snippet trigger()}
|
||||
|
||||
<HistoryIcon size={14} />
|
||||
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
|
||||
<div class="p-2 h-[400px] overflow-hidden w-80 border shadow-sm">
|
||||
<HistoricInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
dispatch('select', { jobId: e.detail?.jobId, initial: false })
|
||||
} else {
|
||||
dispatch('unselect')
|
||||
}
|
||||
}}
|
||||
{selected}
|
||||
runnableId={path}
|
||||
runnableType={'FlowPath'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/snippet}
|
||||
</PopoverV2>
|
||||
|
||||
@@ -137,10 +137,10 @@
|
||||
}
|
||||
|
||||
function minJobTime(jobs: CompletedJob[]): Date {
|
||||
let min: Date = new Date(jobs[0].started_at)
|
||||
let min: Date = new Date(jobs[0].completed_at!)
|
||||
for (const job of jobs) {
|
||||
if (job.started_at != undefined) {
|
||||
const date = new Date(job.started_at)
|
||||
if (job.completed_at != undefined) {
|
||||
const date = new Date(job.completed_at)
|
||||
if (date < min) {
|
||||
min = date
|
||||
}
|
||||
@@ -150,10 +150,10 @@
|
||||
}
|
||||
|
||||
function maxJobTime(jobs: CompletedJob[]): Date {
|
||||
let max: Date = new Date(jobs[0].started_at)
|
||||
let max: Date = new Date(jobs[0].completed_at!)
|
||||
for (const job of jobs) {
|
||||
if (new Date(job.started_at) > max) {
|
||||
max = new Date(job.started_at)
|
||||
if (new Date(job.completed_at!) > max) {
|
||||
max = new Date(job.completed_at!)
|
||||
}
|
||||
}
|
||||
return max
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { Button } from '$lib/components/common'
|
||||
import {
|
||||
InputService,
|
||||
@@ -10,7 +11,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores.js'
|
||||
import { base } from '$lib/base'
|
||||
import { classNames, displayDateOnly, sendUserToast } from '$lib/utils.js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
import { ArrowLeftIcon, Edit, ExternalLink, X } from 'lucide-svelte'
|
||||
@@ -21,37 +22,49 @@
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import SaveInputsButton from './SaveInputsButton.svelte'
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
export let scriptPath: string | null = null
|
||||
export let flowPath: string | null = null
|
||||
export let canSaveInputs: boolean = true
|
||||
|
||||
// Are the current Inputs valid and able to be saved?
|
||||
export let isValid: boolean
|
||||
export let args: object
|
||||
|
||||
interface Props {
|
||||
scriptHash?: string | null;
|
||||
scriptPath?: string | null;
|
||||
flowPath?: string | null;
|
||||
canSaveInputs?: boolean;
|
||||
// Are the current Inputs valid and able to be saved?
|
||||
isValid: boolean;
|
||||
args: object;
|
||||
}
|
||||
|
||||
let {
|
||||
scriptHash = null,
|
||||
scriptPath = null,
|
||||
flowPath = null,
|
||||
canSaveInputs = true,
|
||||
isValid,
|
||||
args
|
||||
}: Props = $props();
|
||||
|
||||
interface EditableInput extends Input {
|
||||
isEditing?: boolean
|
||||
isSaving?: boolean
|
||||
}
|
||||
|
||||
let previousInputs: Input[] | undefined = undefined
|
||||
let savedInputs: EditableInput[] | undefined = undefined
|
||||
let selectedInput: Input | null
|
||||
let jobs: Job[] = []
|
||||
let loading: boolean = false
|
||||
let previousInputs: Input[] | undefined = $state(undefined)
|
||||
let savedInputs: EditableInput[] | undefined = $state(undefined)
|
||||
let selectedInput = $state() as Input | null
|
||||
let jobs: Job[] = $state([])
|
||||
let loading: boolean = $state(false)
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: runnableId = scriptHash || scriptPath || flowPath || undefined
|
||||
let runnableId = $derived(scriptHash || scriptPath || flowPath || undefined)
|
||||
|
||||
let runnableType: RunnableType | undefined = $derived(scriptHash
|
||||
? 'ScriptHash'
|
||||
: scriptPath
|
||||
? 'ScriptPath'
|
||||
: flowPath
|
||||
? 'FlowPath'
|
||||
: undefined)
|
||||
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
$: runnableType = scriptHash
|
||||
? 'ScriptHash'
|
||||
: scriptPath
|
||||
? 'ScriptPath'
|
||||
: flowPath
|
||||
? 'FlowPath'
|
||||
: undefined
|
||||
|
||||
let hasAlreadyFailed = false
|
||||
async function loadInputHistory() {
|
||||
@@ -115,14 +128,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
if ($workspaceStore && (scriptHash || scriptPath || flowPath)) {
|
||||
untrack(() => {
|
||||
loadInputHistory()
|
||||
loadSavedInputs()
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let previewArgs: any = undefined
|
||||
let previewArgs: any = $state(undefined)
|
||||
|
||||
function selectArgs(selected_args: any) {
|
||||
previewArgs = selected_args
|
||||
@@ -197,7 +212,7 @@
|
||||
`w-full flex items-center text-sm group justify-between gap-4 py-1.5 px-4 text-left border rounded-sm hover:bg-surface-hover transition-all`,
|
||||
selectedInput === i ? 'border-blue-500 bg-blue-50 dark:bg-blue-900' : ''
|
||||
)}
|
||||
on:click={async () => {
|
||||
onclick={async () => {
|
||||
if (!i.isEditing) {
|
||||
if (selectedInput === i) {
|
||||
selectedInput = null
|
||||
@@ -211,7 +226,7 @@
|
||||
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
|
||||
{#if i.isEditing}
|
||||
<form
|
||||
on:submit={() => {
|
||||
onsubmit={() => {
|
||||
updateInput(i)
|
||||
i.isEditing = false
|
||||
i.isSaving = false
|
||||
@@ -296,7 +311,7 @@
|
||||
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-surface-hover transition-a`,
|
||||
'border-orange-400'
|
||||
)}
|
||||
on:click={async () => {
|
||||
onclick={async () => {
|
||||
if (!$workspaceStore) {
|
||||
return
|
||||
}
|
||||
@@ -364,7 +379,7 @@
|
||||
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-surface-hover transition-a`,
|
||||
selectedInput === i ? 'border-blue-500 bg-blue-50 dark:bg-blue-900' : ''
|
||||
)}
|
||||
on:click={async () => {
|
||||
onclick={async () => {
|
||||
if (selectedInput === i) {
|
||||
selectedInput = null
|
||||
} else {
|
||||
|
||||
@@ -327,7 +327,7 @@
|
||||
loadingJobs: true
|
||||
}
|
||||
}
|
||||
const previousJobId = await JobService.listJobs({
|
||||
const previousJobId = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
scriptPathExact: path,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow'].join(','),
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
type CompletedJob,
|
||||
type ExtendedJobs,
|
||||
ConcurrencyGroupsService,
|
||||
type ObscuredJob
|
||||
type ObscuredJob,
|
||||
|
||||
|
||||
|
||||
} from '$lib/gen'
|
||||
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -136,11 +139,32 @@
|
||||
|
||||
export async function loadExtraJobs(): Promise<boolean> {
|
||||
if (jobs && jobs.length > 0) {
|
||||
const lastJob = jobs[jobs.length - 1]
|
||||
// const minCreated = lastJob?.created_at
|
||||
const minCreated = new Date(new Date(lastJob.created_at!).getTime() - 1).toISOString()
|
||||
let minQueueTs: string | undefined = undefined
|
||||
let minCompletedTs: string | undefined = undefined
|
||||
|
||||
let olderJobs = await fetchJobs(undefined, minTs, undefined, minCreated)
|
||||
let cursor = 0
|
||||
|
||||
while (jobs && cursor < jobs?.length) {
|
||||
cursor++
|
||||
const job = jobs[jobs.length - 1 - cursor]
|
||||
if (job.type == 'CompletedJob') {
|
||||
minCompletedTs = job.completed_at
|
||||
break
|
||||
} else if (job.type == 'QueuedJob' && minQueueTs == undefined) {
|
||||
minQueueTs = job.created_at
|
||||
}
|
||||
}
|
||||
|
||||
const ts = minCompletedTs ?? minQueueTs
|
||||
|
||||
if (!ts) {
|
||||
sendUserToast("No jobs to load from")
|
||||
return false
|
||||
}
|
||||
// const minCreated = lastJob?.created_at
|
||||
const minCreated = new Date(new Date(ts).getTime() - 1).toISOString()
|
||||
|
||||
let olderJobs = await fetchJobs(minCreated, minTs, undefined)
|
||||
jobs = jobs.concat(olderJobs)
|
||||
computeCompletedJobs()
|
||||
return olderJobs?.length < 1000
|
||||
@@ -149,10 +173,9 @@
|
||||
}
|
||||
|
||||
async function fetchJobs(
|
||||
startedBefore: string | undefined,
|
||||
startedAfter: string | undefined,
|
||||
startedAfterCompletedJobs: string | undefined,
|
||||
createdBefore: string | undefined
|
||||
completedBefore: string | undefined,
|
||||
completedAfter: string | undefined,
|
||||
createdAfterQueue: string | undefined
|
||||
): Promise<Job[]> {
|
||||
loadingFetch = true
|
||||
try {
|
||||
@@ -160,12 +183,11 @@
|
||||
let scriptPathExact = path === null || path === '' ? undefined : path
|
||||
return JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
createdOrStartedBefore: startedBefore,
|
||||
createdOrStartedAfter: startedAfter,
|
||||
createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
|
||||
completedBefore,
|
||||
completedAfter,
|
||||
createdAfterQueue,
|
||||
schedulePath,
|
||||
scriptPathExact,
|
||||
createdBefore,
|
||||
createdBy: user === null || user === '' ? undefined : user,
|
||||
scriptPathStart: scriptPathStart,
|
||||
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
|
||||
@@ -211,9 +233,8 @@
|
||||
|
||||
async function fetchExtendedJobs(
|
||||
concurrencyKey: string | null,
|
||||
startedBefore: string | undefined,
|
||||
startedAfter: string | undefined,
|
||||
startedAfterCompletedJobs: string | undefined
|
||||
createdBeforeQueue: string | undefined,
|
||||
completedAfter: string | undefined
|
||||
): Promise<ExtendedJobs> {
|
||||
loadingFetch = true
|
||||
try {
|
||||
@@ -221,9 +242,11 @@
|
||||
rowLimit: 1000,
|
||||
concurrencyKey: concurrencyKey == null || concurrencyKey == '' ? undefined : concurrencyKey,
|
||||
workspace: $workspaceStore!,
|
||||
createdOrStartedBefore: startedBefore,
|
||||
createdOrStartedAfter: startedAfter,
|
||||
createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
|
||||
completedAfter,
|
||||
createdBeforeQueue: createdBeforeQueue,
|
||||
// createdOrStartedBefore: startedBefore,
|
||||
// createdOrStartedAfter: startedAfter,
|
||||
// createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
|
||||
schedulePath,
|
||||
scriptPathExact: path === null || path === '' ? undefined : path,
|
||||
createdBy: user === null || user === '' ? undefined : user,
|
||||
@@ -294,7 +317,7 @@
|
||||
// lookback won't be needed anymore (just filter ended_at > minTs instead
|
||||
const extendedMinTs = subtractDaysFromDateString(minTs, lookback)
|
||||
if (concurrencyKey == null || concurrencyKey === '') {
|
||||
let newJobs = await fetchJobs(maxTs, undefined, extendedMinTs, undefined)
|
||||
let newJobs = await fetchJobs(maxTs, undefined, extendedMinTs)
|
||||
extendedJobs = { jobs: newJobs, obscured_jobs: [] } as ExtendedJobs
|
||||
|
||||
// Filter on minTs here and not in the backend
|
||||
@@ -302,7 +325,7 @@
|
||||
jobs = sortMinDate(minTs, newJobs)
|
||||
externalJobs = []
|
||||
} else {
|
||||
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, undefined, extendedMinTs)
|
||||
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs)
|
||||
const newJobs = extendedJobs.jobs
|
||||
const newExternalJobs = extendedJobs.obscured_jobs
|
||||
|
||||
@@ -351,6 +374,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
let lastQueueTs: string | undefined = undefined
|
||||
|
||||
async function syncer() {
|
||||
if (success == 'waiting') {
|
||||
minTs = undefined
|
||||
@@ -379,40 +404,47 @@
|
||||
if (success == 'running') {
|
||||
loadJobsIntern(false)
|
||||
} else {
|
||||
let ts: string | undefined = undefined
|
||||
let minQueueCreatedAt: string | undefined = undefined
|
||||
let completedTs: string | undefined = undefined
|
||||
|
||||
let cursor = 0
|
||||
|
||||
while (cursor < jobs.length && minTs == undefined) {
|
||||
let invCursor = jobs.length - 1 - cursor
|
||||
let isQueuedJob = invCursor == 0 || jobs[invCursor].type == 'QueuedJob'
|
||||
if (isQueuedJob) {
|
||||
if (cursor > 0) {
|
||||
let inc = invCursor == 0 && jobs[invCursor].type == 'CompletedJob' ? 0 : 1
|
||||
const date = new Date(jobs[invCursor + inc]?.created_at!)
|
||||
date.setMilliseconds(date.getMilliseconds() + 1)
|
||||
ts = date.toISOString()
|
||||
if (minTs == undefined) {
|
||||
while (cursor < jobs.length) {
|
||||
const cjob = jobs[cursor]
|
||||
if (cjob.type == 'QueuedJob') {
|
||||
minQueueCreatedAt = cjob.created_at
|
||||
} else if (cjob.type == 'CompletedJob' && completedTs == undefined) {
|
||||
completedTs = new Date(cjob.completed_at!).toISOString()
|
||||
}
|
||||
break
|
||||
cursor++
|
||||
}
|
||||
cursor++
|
||||
}
|
||||
|
||||
let queueTs: string | undefined
|
||||
if (minQueueCreatedAt) {
|
||||
const queueTs = new Date(minQueueCreatedAt).toISOString()
|
||||
lastQueueTs = queueTs
|
||||
} else {
|
||||
queueTs = lastQueueTs
|
||||
}
|
||||
|
||||
|
||||
loading = true
|
||||
let newJobs: Job[]
|
||||
if (concurrencyKey == null || concurrencyKey === '') {
|
||||
newJobs = await fetchJobs(maxTs, minTs ?? ts, undefined, undefined)
|
||||
newJobs = await fetchJobs(maxTs, minTs ?? completedTs, queueTs)
|
||||
} else {
|
||||
// Obscured jobs have no ids, so we have to do the full request
|
||||
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, undefined, minTs ?? ts)
|
||||
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, minTs ?? completedTs)
|
||||
externalJobs = computeExternalJobs(extendedJobs.obscured_jobs)
|
||||
|
||||
// Filter on minTs here and not in the backend
|
||||
// to get enough data for the concurrency graph
|
||||
newJobs = sortMinDate(minTs ?? ts, extendedJobs.jobs)
|
||||
newJobs = sortMinDate(minTs ?? completedTs, extendedJobs.jobs)
|
||||
}
|
||||
if (newJobs && newJobs.length > 0 && jobs) {
|
||||
jobs = updateWithNewJobs(jobs, newJobs)
|
||||
jobs = jobs
|
||||
if (concurrencyKey == null || concurrencyKey === '') {
|
||||
if (!extendedJobs) {
|
||||
extendedJobs = { jobs: jobs, obscured_jobs: [] } as ExtendedJobs
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
import { Button } from '../common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let minTs: string | undefined
|
||||
export let maxTs: string | undefined
|
||||
export let loading: boolean = false
|
||||
export let selectedManualDate = 0
|
||||
export let loadText: string | undefined = undefined
|
||||
export let serviceLogsChoices: boolean = false
|
||||
interface Props {
|
||||
minTs: string | undefined;
|
||||
maxTs: string | undefined;
|
||||
loading?: boolean;
|
||||
selectedManualDate?: number;
|
||||
loadText?: string | undefined;
|
||||
serviceLogsChoices?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
minTs = $bindable(),
|
||||
maxTs = $bindable(),
|
||||
loading = false,
|
||||
selectedManualDate = $bindable(0),
|
||||
loadText = undefined,
|
||||
serviceLogsChoices = false
|
||||
}: Props = $props();
|
||||
|
||||
export function computeMinMax(): { minTs: string; maxTs: string | undefined } | undefined {
|
||||
return manualDates[selectedManualDate].computeMinMax()
|
||||
@@ -62,7 +73,7 @@
|
||||
}
|
||||
]
|
||||
|
||||
$: manualDates = [
|
||||
let manualDates = $derived([
|
||||
{
|
||||
label: loadText ?? 'Last 1000 runs',
|
||||
computeMinMax: () => {
|
||||
@@ -70,7 +81,7 @@
|
||||
}
|
||||
},
|
||||
...fixedManualDates
|
||||
]
|
||||
])
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -181,9 +181,14 @@
|
||||
<div class="overflow-hidden min-w-0">
|
||||
<div class="flex flex-row items-center gap-1 text-secondary text-2xs">
|
||||
{#if job}
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
{isJobRecent ? 'Started' : ''}
|
||||
<TimeAgo bind:isRecent={isJobRecent} agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
{#if ('started_at' in job && job.started_at) || ('completed_at' in job && job.completed_at)}
|
||||
{#if 'completed_at' in job && job.completed_at}
|
||||
{isJobRecent ? 'Ended' : ''}
|
||||
<TimeAgo bind:isRecent={isJobRecent} agoOnlyIfRecent date={job.completed_at ?? ''} />
|
||||
{:else if 'started_at' in job && job.started_at}
|
||||
{isJobRecent ? 'Started' : ''}
|
||||
<TimeAgo bind:isRecent={isJobRecent} agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
{/if}
|
||||
{#if job && (job.self_wait_time_ms || job.aggregate_wait_time_ms)}
|
||||
<WaitTimeWarning
|
||||
self_wait_time_ms={job.self_wait_time_ms}
|
||||
@@ -200,12 +205,12 @@
|
||||
Cancelling job... (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
|
||||
{/if}
|
||||
{:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
Waiting for executor (scheduled for <TimeAgo
|
||||
Waiting executor (<TimeAgo
|
||||
agoOnlyIfRecent
|
||||
date={job.scheduled_for || ''}
|
||||
/>)
|
||||
{:else}
|
||||
Waiting for executor (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
|
||||
Waiting executor (<TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}: Props = $props()
|
||||
|
||||
function getTime(job: Job): string | undefined {
|
||||
return job['started_at'] ?? job['scheduled_for'] ?? job['created_at']
|
||||
return job['completed_at'] ?? job['started_at'] ?? job['scheduled_for'] ?? job['created_at']
|
||||
}
|
||||
|
||||
function groupJobsByDay(jobs: Job[]): {
|
||||
|
||||
@@ -109,7 +109,7 @@ export class StepHistoryLoader {
|
||||
? module.value.path
|
||||
: (initialPath === '' ? path : initialPath) + '/' + module.id
|
||||
|
||||
const previousJobId = await JobService.listJobs({
|
||||
const previousJobId = await JobService.listCompletedJobs({
|
||||
workspace: workspaceId,
|
||||
scriptPathExact: scriptPath,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
|
||||
|
||||
Reference in New Issue
Block a user