Compare commits
3 Commits
v1.682.0
...
feat/jobs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be43f3b3fe | ||
|
|
0385996ba1 | ||
|
|
27e9769db8 |
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
14
backend/.sqlx/query-086b2564495eb07d8ea362bbfa33c9571ab11fde8172aeea6b93ccfeca1c1418.json
generated
Normal file
14
backend/.sqlx/query-086b2564495eb07d8ea362bbfa33c9571ab11fde8172aeea6b93ccfeca1c1418.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed USING jobs_pending_deletion d\n WHERE v2_job_completed.id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "086b2564495eb07d8ea362bbfa33c9571ab11fde8172aeea6b93ccfeca1c1418"
|
||||
}
|
||||
@@ -30,7 +30,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-24aa22975e2bd9cd03bbfc289a14525d78eb8a4e7d88954994e1fdbb79247368.json
generated
Normal file
14
backend/.sqlx/query-24aa22975e2bd9cd03bbfc289a14525d78eb8a4e7d88954994e1fdbb79247368.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats USING jobs_pending_deletion d\n WHERE job_stats.job_id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "24aa22975e2bd9cd03bbfc289a14525d78eb8a4e7d88954994e1fdbb79247368"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_logs WHERE job_id = ANY($1) RETURNING log_file_index",
|
||||
"query": "DELETE FROM job_logs USING jobs_pending_deletion d\n WHERE job_logs.job_id = d.id AND d.marked_by = $1\n RETURNING job_logs.log_file_index",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,12 +11,12 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "288e99211bbd45a337fc9b79c43c5139ee535e23c8b3362c52eef49998349f15"
|
||||
"hash": "2b66cee1feee58537c949837e1b3ae8d2504cde2714b8bc40337d34e355ff3c5"
|
||||
}
|
||||
16
backend/.sqlx/query-2f2c8b6e3b75524f286a700b87a52697c28154b9ed551bcf9549275acd53625a.json
generated
Normal file
16
backend/.sqlx/query-2f2c8b6e3b75524f286a700b87a52697c28154b9ed551bcf9549275acd53625a.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job (id, tag, workspace_id, created_at, created_by, permissioned_as, permissioned_as_email, kind, same_worker, visible_to_owner)\n VALUES ($1, $2, 'admins', now() - interval '30 minutes', $3, $3, 'bench@test.com', 'script', false, true)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2f2c8b6e3b75524f286a700b87a52697c28154b9ed551bcf9549275acd53625a"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COALESCE((SELECT value::bigint FROM global_settings WHERE name = 'retention_period_secs'), 2592000)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "coalesce",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "37937656035a487aa252bad3bdba28c129943e1c3ccd5bc3c8f652435d1a0021"
|
||||
}
|
||||
14
backend/.sqlx/query-3d71a98588d436f27ec6e597e856151e4bcddb638c3700f2f854e14856bc4d82.json
generated
Normal file
14
backend/.sqlx/query-3d71a98588d436f27ec6e597e856151e4bcddb638c3700f2f854e14856bc4d82.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_logs USING jobs_pending_deletion d\n WHERE job_logs.job_id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3d71a98588d436f27ec6e597e856151e4bcddb638c3700f2f854e14856bc4d82"
|
||||
}
|
||||
22
backend/.sqlx/query-3ebf915f5a49f1de1003e790fec459ffec018847ba20dd30fdc7f8012d03bbd6.json
generated
Normal file
22
backend/.sqlx/query-3ebf915f5a49f1de1003e790fec459ffec018847ba20dd30fdc7f8012d03bbd6.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT q.id FROM v2_job_queue q\n JOIN v2_job j ON j.id = q.id\n WHERE j.parent_job IS NULL\n AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3ebf915f5a49f1de1003e790fec459ffec018847ba20dd30fdc7f8012d03bbd6"
|
||||
}
|
||||
17
backend/.sqlx/query-408caf44233591446c21d7e180d91cec9ad768b06fc4cd816b3e066cbe71873c.json
generated
Normal file
17
backend/.sqlx/query-408caf44233591446c21d7e180d91cec9ad768b06fc4cd816b3e066cbe71873c.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO jobs_pending_deletion (id, marked_by)\n SELECT jc.id, $4 FROM v2_job_completed jc\n JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($2)\n AND j.tag = $3\n ORDER BY jc.completed_at ASC\n LIMIT 1000\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"UuidArray",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "408caf44233591446c21d7e180d91cec9ad768b06fc4cd816b3e066cbe71873c"
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT jc.id FROM v2_job_completed jc\n LEFT JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)\n ORDER BY jc.completed_at ASC\n LIMIT $2\n FOR UPDATE OF jc SKIP LOCKED\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "45997fcb4d9d62c7f7011966bf59bdb86e12ce1d0c8e925e738d2645121a5c1f"
|
||||
}
|
||||
14
backend/.sqlx/query-4a89f9f21876e7274ad8691ee2986918e22b12491d0977e320abaddcaf604c43.json
generated
Normal file
14
backend/.sqlx/query-4a89f9f21876e7274ad8691ee2986918e22b12491d0977e320abaddcaf604c43.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job USING jobs_pending_deletion d\n WHERE v2_job.id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a89f9f21876e7274ad8691ee2986918e22b12491d0977e320abaddcaf604c43"
|
||||
}
|
||||
@@ -37,7 +37,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -70,7 +71,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
12
backend/.sqlx/query-51e1d713dc57f57531dce518e564bd9440582ffd3e396a947f3c93f139e770e2.json
generated
Normal file
12
backend/.sqlx/query-51e1d713dc57f57531dce518e564bd9440582ffd3e396a947f3c93f139e770e2.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM jobs_pending_deletion",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "51e1d713dc57f57531dce518e564bd9440582ffd3e396a947f3c93f139e770e2"
|
||||
}
|
||||
20
backend/.sqlx/query-56740517a422c64d8e5b162cfde0fd2887738435313af441e4028ebe6efdf210.json
generated
Normal file
20
backend/.sqlx/query-56740517a422c64d8e5b162cfde0fd2887738435313af441e4028ebe6efdf210.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM jobs_pending_deletion",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "56740517a422c64d8e5b162cfde0fd2887738435313af441e4028ebe6efdf210"
|
||||
}
|
||||
@@ -245,7 +245,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-6967f6f7fef9a3d1364fd549974a8b77bd9bac9fbc3379ef9f5bd28841fdf66b.json
generated
Normal file
14
backend/.sqlx/query-6967f6f7fef9a3d1364fd549974a8b77bd9bac9fbc3379ef9f5bd28841fdf66b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO job_stats (job_id, workspace_id) VALUES ($1, 'admins')\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6967f6f7fef9a3d1364fd549974a8b77bd9bac9fbc3379ef9f5bd28841fdf66b"
|
||||
}
|
||||
@@ -29,7 +29,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-7420b825e8e2ee74b7839dbffec5709b99faecb45044069bfa895262fb5ca940.json
generated
Normal file
14
backend/.sqlx/query-7420b825e8e2ee74b7839dbffec5709b99faecb45044069bfa895262fb5ca940.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE jobs_pending_deletion SET marked_by = $1, marked_at = now()\n WHERE marked_at < now() - interval '30 minutes'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7420b825e8e2ee74b7839dbffec5709b99faecb45044069bfa895262fb5ca940"
|
||||
}
|
||||
15
backend/.sqlx/query-74ac1a31ded8aed51a06a44aee9cd4b7ad3a17ed0a03c376957e819701726e4b.json
generated
Normal file
15
backend/.sqlx/query-74ac1a31ded8aed51a06a44aee9cd4b7ad3a17ed0a03c376957e819701726e4b.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats WHERE job_id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "74ac1a31ded8aed51a06a44aee9cd4b7ad3a17ed0a03c376957e819701726e4b"
|
||||
}
|
||||
@@ -40,7 +40,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM v2_job_completed WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
"query": "SELECT COUNT(*) FROM jobs_pending_deletion WHERE marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,12 +11,12 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "10cdcf3155d8e58350e7df9acdbc73893c57716663a579b8bd120b542cc186f7"
|
||||
"hash": "7d0546f5c70c8f9015791c8ee0717ad453e5d5e11253e90efc8518b03552e461"
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
backend/.sqlx/query-94778a1971be08e528c4fc87a15998b157a6ea80732c324bb422b1c5a0fad606.json
generated
Normal file
15
backend/.sqlx/query-94778a1971be08e528c4fc87a15998b157a6ea80732c324bb422b1c5a0fad606.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_logs WHERE job_id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "94778a1971be08e528c4fc87a15998b157a6ea80732c324bb422b1c5a0fad606"
|
||||
}
|
||||
@@ -32,7 +32,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-9fda5c99a4917fbb0ad38f3e1f57328353d49fa1b311c9d45d3d4e75d6d9e7d3.json
generated
Normal file
14
backend/.sqlx/query-9fda5c99a4917fbb0ad38f3e1f57328353d49fa1b311c9d45d3d4e75d6d9e7d3.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM jobs_pending_deletion WHERE marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9fda5c99a4917fbb0ad38f3e1f57328353d49fa1b311c9d45d3d4e75d6d9e7d3"
|
||||
}
|
||||
@@ -155,7 +155,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
17
backend/.sqlx/query-a9d80d005362d1748e354c6bb6f19b5205ecd4c92f16f2b756dddd3485b1b7d6.json
generated
Normal file
17
backend/.sqlx/query-a9d80d005362d1748e354c6bb6f19b5205ecd4c92f16f2b756dddd3485b1b7d6.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH marked AS (\n INSERT INTO jobs_pending_deletion (id, marked_by)\n SELECT jc.id, $4 FROM v2_job_completed jc\n LEFT JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)\n ORDER BY jc.completed_at ASC\n LIMIT $2\n ON CONFLICT DO NOTHING\n RETURNING id\n )\n DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM marked)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a9d80d005362d1748e354c6bb6f19b5205ecd4c92f16f2b756dddd3485b1b7d6"
|
||||
}
|
||||
@@ -160,7 +160,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-b8205e1b6b9b75e1d1d445e23d158d6d0525c78f1d447d4abb7c5ef674bd6559.json
generated
Normal file
14
backend/.sqlx/query-b8205e1b6b9b75e1d1d445e23d158d6d0525c78f1d447d4abb7c5ef674bd6559.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_result_stream_v2 USING jobs_pending_deletion d\n WHERE job_result_stream_v2.job_id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b8205e1b6b9b75e1d1d445e23d158d6d0525c78f1d447d4abb7c5ef674bd6559"
|
||||
}
|
||||
15
backend/.sqlx/query-bc2755b3815b163b7199ed30118e3497f1a33b81b47e4877f0f443ed52a89755.json
generated
Normal file
15
backend/.sqlx/query-bc2755b3815b163b7199ed30118e3497f1a33b81b47e4877f0f443ed52a89755.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job WHERE created_by = $1 AND tag = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bc2755b3815b163b7199ed30118e3497f1a33b81b47e4877f0f443ed52a89755"
|
||||
}
|
||||
14
backend/.sqlx/query-bd768adb92ca06a0733278d01e016220bf72da0bf62f13816e161b789530b85a.json
generated
Normal file
14
backend/.sqlx/query-bd768adb92ca06a0733278d01e016220bf72da0bf62f13816e161b789530b85a.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats USING jobs_pending_deletion d\n WHERE job_stats.job_id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bd768adb92ca06a0733278d01e016220bf72da0bf62f13816e161b789530b85a"
|
||||
}
|
||||
@@ -105,7 +105,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
25
backend/.sqlx/query-c687c38563027e898cdaf2b3175d6f898c32e419cbb29707c54c92d8608ec762.json
generated
Normal file
25
backend/.sqlx/query-c687c38563027e898cdaf2b3175d6f898c32e419cbb29707c54c92d8608ec762.json
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT jc.id FROM v2_job_completed jc\n JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)\n AND j.tag = $4\n ORDER BY jc.completed_at ASC\n LIMIT $2\n FOR UPDATE OF jc SKIP LOCKED\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c687c38563027e898cdaf2b3175d6f898c32e419cbb29707c54c92d8608ec762"
|
||||
}
|
||||
14
backend/.sqlx/query-c95643742fc5d3d9f16e3ba49bf67a3c88576981111b7141b61af500e1e15d6a.json
generated
Normal file
14
backend/.sqlx/query-c95643742fc5d3d9f16e3ba49bf67a3c88576981111b7141b61af500e1e15d6a.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job USING jobs_pending_deletion d\n WHERE v2_job.id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c95643742fc5d3d9f16e3ba49bf67a3c88576981111b7141b61af500e1e15d6a"
|
||||
}
|
||||
14
backend/.sqlx/query-ca2d6133fb8c93f3073583514aba62f4eacac185ed98cce7cb06dba9ba74cb36.json
generated
Normal file
14
backend/.sqlx/query-ca2d6133fb8c93f3073583514aba62f4eacac185ed98cce7cb06dba9ba74cb36.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO job_logs (job_id, workspace_id, logs) VALUES ($1, 'admins', 'test log output')\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ca2d6133fb8c93f3073583514aba62f4eacac185ed98cce7cb06dba9ba74cb36"
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
18
backend/.sqlx/query-db3b300319cd3a5dee5dd078dd9df4729e68170d85ab6988e19ac6c8fc753253.json
generated
Normal file
18
backend/.sqlx/query-db3b300319cd3a5dee5dd078dd9df4729e68170d85ab6988e19ac6c8fc753253.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH marked AS (\n INSERT INTO jobs_pending_deletion (id, marked_by)\n SELECT jc.id, $5 FROM v2_job_completed jc\n JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)\n AND j.tag = $4\n ORDER BY jc.completed_at ASC\n LIMIT $2\n ON CONFLICT DO NOTHING\n RETURNING id\n )\n DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM marked)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "db3b300319cd3a5dee5dd078dd9df4729e68170d85ab6988e19ac6c8fc753253"
|
||||
}
|
||||
14
backend/.sqlx/query-de2acdcaee4f75bb04abd795354acb1d67493f89a79601e81e804154caa1fd19.json
generated
Normal file
14
backend/.sqlx/query-de2acdcaee4f75bb04abd795354acb1d67493f89a79601e81e804154caa1fd19.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_completed (id, workspace_id, duration_ms, deleted, status, completed_at, started_at)\n VALUES ($1, 'admins', 100, false, 'success', now() - interval '30 minutes', now() - interval '30 minutes')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "de2acdcaee4f75bb04abd795354acb1d67493f89a79601e81e804154caa1fd19"
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval\n ORDER BY completed_at ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f21c608aa332b1600d2236ceba2be64c825e8bf5a2cb6446a2d973d028d6311f"
|
||||
}
|
||||
14
backend/.sqlx/query-f36c2e5c4617e88c4fb7708842e6c6bab3194842be4a4306e3e7863eababdcf0.json
generated
Normal file
14
backend/.sqlx/query-f36c2e5c4617e88c4fb7708842e6c6bab3194842be4a4306e3e7863eababdcf0.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_result_stream_v2 USING jobs_pending_deletion d\n WHERE job_result_stream_v2.job_id = d.id AND d.marked_by = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f36c2e5c4617e88c4fb7708842e6c6bab3194842be4a4306e3e7863eababdcf0"
|
||||
}
|
||||
15
backend/.sqlx/query-f4b86c8950739b4e8cbd447d9ac0814a1ae69067735721106af19c188c77377c.json
generated
Normal file
15
backend/.sqlx/query-f4b86c8950739b4e8cbd447d9ac0814a1ae69067735721106af19c188c77377c.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f4b86c8950739b4e8cbd447d9ac0814a1ae69067735721106af19c188c77377c"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval\n ORDER BY completed_at ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fa4b7688a4e83dee51d5c64b7660b824a7ec745ad62845d469a5be6219a2be29"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS jobs_pending_deletion;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS jobs_pending_deletion (
|
||||
id UUID PRIMARY KEY,
|
||||
marked_by VARCHAR(10) NOT NULL,
|
||||
marked_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_pending_deletion_marked_by
|
||||
ON jobs_pending_deletion (marked_by);
|
||||
|
||||
-- Aggressive autovacuum: this table churns heavily (bulk insert + delete each cycle).
|
||||
-- Vacuum after every batch clear to prevent dead tuple bloat.
|
||||
ALTER TABLE jobs_pending_deletion SET (
|
||||
autovacuum_vacuum_scale_factor = 0,
|
||||
autovacuum_vacuum_threshold = 50,
|
||||
autovacuum_analyze_scale_factor = 0,
|
||||
autovacuum_analyze_threshold = 50
|
||||
);
|
||||
@@ -79,9 +79,9 @@ use windmill_common::{
|
||||
WORKER_CONFIG, WORKER_GROUP,
|
||||
},
|
||||
KillpillSender, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE, CRITICAL_ALERT_MUTE_UI_ENABLED,
|
||||
CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED,
|
||||
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
|
||||
CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, INSTANCE_NAME,
|
||||
JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE,
|
||||
OTEL_LOGS_ENABLED, OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
|
||||
};
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
|
||||
@@ -1057,109 +1057,178 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a batch of expired jobs with LIMIT and SKIP LOCKED for high-scale environments.
|
||||
/// Uses a single transaction per batch to minimize lock duration.
|
||||
/// Returns the number of jobs deleted in this batch.
|
||||
/// Delete a batch of expired jobs using the `jobs_pending_deletion` staging table.
|
||||
///
|
||||
/// Each step runs in its own transaction so that crashes are recoverable: if the server
|
||||
/// restarts mid-cleanup, leftover rows in `jobs_pending_deletion` are picked up on the next run.
|
||||
/// No job UUIDs are held in application memory — all joins stay inside Postgres.
|
||||
///
|
||||
/// Rows are scoped by `marked_by` (the server's `INSTANCE_NAME`) so multiple servers
|
||||
/// can run cleanup concurrently without interfering with each other. Stale rows from dead
|
||||
/// servers are adopted via `marked_at` timestamp after 30 minutes.
|
||||
///
|
||||
/// Returns the number of jobs marked for deletion in this batch.
|
||||
async fn delete_expired_jobs_batch(
|
||||
db: &DB,
|
||||
job_retention_secs: i64,
|
||||
batch_size: i64,
|
||||
) -> error::Result<usize> {
|
||||
let mut tx = db.begin().await?;
|
||||
let instance = &*INSTANCE_NAME;
|
||||
|
||||
// Fetch active ROOT job IDs that started before the retention period. We only care about
|
||||
// these because their child jobs could be old enough to be deletion candidates.
|
||||
// Jobs started after the retention period can't have children old enough to delete.
|
||||
let active_root_job_ids: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"SELECT q.id FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
WHERE j.parent_job IS NULL
|
||||
AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
job_retention_secs
|
||||
// Step 0: Adopt orphaned rows from dead servers (stale for >30 min), then check
|
||||
// for leftover rows from a previous incomplete run by this same instance.
|
||||
sqlx::query!(
|
||||
"UPDATE jobs_pending_deletion SET marked_by = $1, marked_at = now()
|
||||
WHERE marked_at < now() - interval '30 minutes'",
|
||||
instance
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
// Use FOR UPDATE SKIP LOCKED to avoid contention between replicas
|
||||
// ORDER BY completed_at ensures we delete oldest jobs first
|
||||
let deleted_jobs: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids
|
||||
let pending_count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM jobs_pending_deletion WHERE marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let deleted_count = deleted_jobs.len();
|
||||
|
||||
if deleted_count > 0 {
|
||||
tracing::debug!(
|
||||
"Deleting batch of {} expired jobs (retention: {}s)",
|
||||
deleted_count,
|
||||
job_retention_secs
|
||||
let marked_count = if pending_count > 0 {
|
||||
tracing::info!(
|
||||
"Job cleanup: resuming with {} leftover jobs from previous run (instance {})",
|
||||
pending_count,
|
||||
instance
|
||||
);
|
||||
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM job_stats WHERE job_id = ANY($1)",
|
||||
&deleted_jobs
|
||||
pending_count as usize
|
||||
} else {
|
||||
// Step 1: Mark expired jobs and atomically remove them from v2_job_completed.
|
||||
let active_root_job_ids: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"SELECT q.id FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
WHERE j.parent_job IS NULL
|
||||
AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
job_retention_secs
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting job stats: {:?}", e);
|
||||
}
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
match sqlx::query_scalar!(
|
||||
"DELETE FROM job_logs WHERE job_id = ANY($1) RETURNING log_file_index",
|
||||
&deleted_jobs
|
||||
let result = sqlx::query!(
|
||||
"WITH marked AS (
|
||||
INSERT INTO jobs_pending_deletion (id, marked_by)
|
||||
SELECT jc.id, $4 FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
)
|
||||
DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM marked)",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids,
|
||||
instance
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(log_file_index) => {
|
||||
let paths = log_file_index
|
||||
.into_iter()
|
||||
.filter_map(|opt| opt)
|
||||
.flat_map(|inner_vec| inner_vec.into_iter())
|
||||
.collect();
|
||||
delete_log_files_from_disk_and_store(paths, TMP_DIR, "").await;
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting job logs: {:?}", e),
|
||||
}
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
if let Err(e) = sqlx::query!("DELETE FROM v2_job WHERE id = ANY($1)", &deleted_jobs)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting job: {:?}", e);
|
||||
}
|
||||
result.rows_affected() as usize
|
||||
};
|
||||
|
||||
// Should already be deleted but just in case
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2 WHERE job_id = ANY($1)",
|
||||
&deleted_jobs
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting job result stream: {:?}", e);
|
||||
}
|
||||
if marked_count == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
tracing::debug!(
|
||||
"Deleting batch of {} expired jobs (retention: {}s, instance: {})",
|
||||
marked_count,
|
||||
job_retention_secs,
|
||||
instance
|
||||
);
|
||||
|
||||
Ok(deleted_count)
|
||||
// Step 2: Delete from job_stats
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM job_stats USING jobs_pending_deletion d
|
||||
WHERE job_stats.job_id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting job stats: {:?}", e);
|
||||
}
|
||||
|
||||
// Step 3: Delete from job_logs (collect log_file_index for disk cleanup)
|
||||
match sqlx::query_scalar!(
|
||||
"DELETE FROM job_logs USING jobs_pending_deletion d
|
||||
WHERE job_logs.job_id = d.id AND d.marked_by = $1
|
||||
RETURNING job_logs.log_file_index",
|
||||
instance
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
{
|
||||
Ok(log_file_index) => {
|
||||
let paths = log_file_index
|
||||
.into_iter()
|
||||
.filter_map(|opt| opt)
|
||||
.flat_map(|inner_vec| inner_vec.into_iter())
|
||||
.collect();
|
||||
delete_log_files_from_disk_and_store(paths, TMP_DIR, "").await;
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting job logs: {:?}", e),
|
||||
}
|
||||
|
||||
// Step 4: Delete from v2_job_completed (idempotent — already done for fresh batches
|
||||
// via the CTE in step 1, but needed for crash-recovery and adopted orphan rows).
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM v2_job_completed USING jobs_pending_deletion d
|
||||
WHERE v2_job_completed.id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting completed jobs: {:?}", e);
|
||||
}
|
||||
|
||||
// Step 5: Delete from v2_job
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM v2_job USING jobs_pending_deletion d
|
||||
WHERE v2_job.id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting jobs: {:?}", e);
|
||||
}
|
||||
|
||||
// Step 6: Delete from job_result_stream_v2
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2 USING jobs_pending_deletion d
|
||||
WHERE job_result_stream_v2.job_id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting job result stream: {:?}", e);
|
||||
}
|
||||
|
||||
// Step 7: Clear this instance's rows from the staging table
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM jobs_pending_deletion WHERE marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error clearing jobs_pending_deletion: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(marked_count)
|
||||
}
|
||||
|
||||
async fn delete_log_files_from_disk_and_store(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Protect certain internal settings from deletion
|
||||
*/
|
||||
|
||||
#[cfg(feature = "operator")]
|
||||
#[cfg(all(feature = "operator", feature = "private"))]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -74,7 +74,7 @@ mod tests {
|
||||
);
|
||||
desired.insert("test_op_setting_b".to_string(), serde_json::json!(42));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -97,7 +97,7 @@ mod tests {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert("test_op_existing".to_string(), serde_json::json!("new"));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -117,7 +117,7 @@ mod tests {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert("test_op_keep".to_string(), serde_json::json!("keep"));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -141,7 +141,7 @@ mod tests {
|
||||
// Sync with empty desired — protected key should survive
|
||||
let desired = BTreeMap::new();
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
|
||||
// Sync with empty desired
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -183,7 +183,7 @@ mod tests {
|
||||
insert_global_setting(&db, "test_op_ephemeral", serde_json::json!("gone")).await;
|
||||
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -230,7 +230,7 @@ mod tests {
|
||||
serde_json::json!({"dedicated_worker": false}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
serde_json::json!({"new": true}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -290,7 +290,7 @@ mod tests {
|
||||
serde_json::json!({"keep": true}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -308,7 +308,7 @@ mod tests {
|
||||
|
||||
// Sync with empty worker configs
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
serde_json::json!({"init_bash": "echo native"}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -353,7 +353,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
@@ -396,10 +396,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// Sync both
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &global_settings)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &global_settings)
|
||||
.await
|
||||
.expect("global sync should succeed");
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &worker_configs)
|
||||
windmill_operator::db_sync_ee::sync_worker_configs(&db, &worker_configs)
|
||||
.await
|
||||
.expect("worker sync should succeed");
|
||||
|
||||
@@ -433,10 +433,10 @@ mod tests {
|
||||
desired.insert("test_op_idempotent".to_string(), serde_json::json!("value"));
|
||||
|
||||
// Run sync twice — should be idempotent
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("first sync should succeed");
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
windmill_operator::db_sync_ee::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("second sync should succeed");
|
||||
|
||||
|
||||
@@ -1,299 +1,524 @@
|
||||
/// Integration test for batched job cleanup
|
||||
/// Integration benchmark comparing old (in-memory UUID) vs new (jobs_pending_deletion table)
|
||||
/// job cleanup approaches.
|
||||
///
|
||||
/// Both methods scope their candidate selection to a specific `tag` so that each run
|
||||
/// operates on an isolated set of jobs. This ensures fair comparison regardless of other
|
||||
/// expired jobs in the database.
|
||||
///
|
||||
/// Prerequisites:
|
||||
/// - PostgreSQL running with windmill database
|
||||
/// - PostgreSQL running with windmill database (migrations applied)
|
||||
/// - DATABASE_URL env var or default: postgres://postgres:changeme@localhost:5432/windmill
|
||||
///
|
||||
/// Run all tests:
|
||||
/// cargo test --test job_cleanup_test -- --nocapture
|
||||
/// Run comparison benchmark:
|
||||
/// cargo test -p windmill-queue --test job_cleanup_test test_compare_cleanup_methods -- --nocapture --ignored
|
||||
///
|
||||
/// Run specific test:
|
||||
/// cargo test --test job_cleanup_test test_batched_job_cleanup -- --nocapture
|
||||
/// cargo test --test job_cleanup_test test_skip_locked_prevents_contention -- --nocapture
|
||||
///
|
||||
/// Setup test data (run in psql first):
|
||||
/// -- Set short retention for testing
|
||||
/// UPDATE global_settings SET value = '3600' WHERE name = 'retention_period_secs';
|
||||
///
|
||||
/// -- Insert expired test jobs
|
||||
/// INSERT INTO v2_job (id, tag, workspace_id, created_at, created_by, permissioned_as, permissioned_as_email, kind, same_worker, visible_to_owner)
|
||||
/// SELECT gen_random_uuid(), 'deno', 'admins', now() - interval '2 hours', 'test_user', 'test_user', 'test@example.com', 'script', false, true
|
||||
/// FROM generate_series(1, 10000);
|
||||
///
|
||||
/// INSERT INTO v2_job_completed (id, workspace_id, duration_ms, deleted, status, completed_at, started_at)
|
||||
/// SELECT j.id, j.workspace_id, 100, false, 'success', j.created_at + interval '1 second', j.created_at
|
||||
/// FROM v2_job j WHERE j.created_by = 'test_user';
|
||||
/// Run individual tests:
|
||||
/// cargo test -p windmill-queue --test job_cleanup_test test_old_method -- --nocapture --ignored
|
||||
/// cargo test -p windmill-queue --test job_cleanup_test test_new_method -- --nocapture --ignored
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_batched_job_cleanup() {
|
||||
// Connect to database
|
||||
let database_url = std::env::var("DATABASE_URL")
|
||||
.unwrap_or("postgres://postgres:changeme@localhost:5432/windmill".to_string());
|
||||
const DEFAULT_DB_URL: &str = "postgres://postgres:changeme@localhost:5432/windmill";
|
||||
const DEFAULT_JOB_COUNT: usize = 50_000;
|
||||
|
||||
let db = PgPoolOptions::new()
|
||||
async fn connect() -> Pool<Postgres> {
|
||||
let url = std::env::var("DATABASE_URL").unwrap_or(DEFAULT_DB_URL.to_string());
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&database_url)
|
||||
.connect(&url)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
.expect("Failed to connect to database")
|
||||
}
|
||||
|
||||
// Get retention period from settings
|
||||
let retention_secs: i64 = 2592000;
|
||||
async fn insert_test_jobs(db: &Pool<Postgres>, count: usize, tag: &str) {
|
||||
for chunk in (0..count).collect::<Vec<_>>().chunks(1000) {
|
||||
let chunk_ids: Vec<Uuid> = chunk.iter().map(|_| Uuid::new_v4()).collect();
|
||||
|
||||
println!(
|
||||
"Retention period: {} seconds ({} days)",
|
||||
retention_secs,
|
||||
retention_secs / 86400
|
||||
);
|
||||
for id in &chunk_ids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job (id, tag, workspace_id, created_at, created_by, permissioned_as, permissioned_as_email, kind, same_worker, visible_to_owner)
|
||||
VALUES ($1, $2, 'admins', now() - interval '30 minutes', $3, $3, 'bench@test.com', 'script', false, true)",
|
||||
id,
|
||||
tag,
|
||||
"bench_user"
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("Failed to insert v2_job");
|
||||
}
|
||||
|
||||
// Count jobs eligible for deletion
|
||||
let eligible_count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM v2_job_completed WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
retention_secs
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.expect("Failed to count eligible jobs")
|
||||
.unwrap_or(0);
|
||||
for id in &chunk_ids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_completed (id, workspace_id, duration_ms, deleted, status, completed_at, started_at)
|
||||
VALUES ($1, 'admins', 100, false, 'success', now() - interval '30 minutes', now() - interval '30 minutes')",
|
||||
id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("Failed to insert v2_job_completed");
|
||||
}
|
||||
|
||||
println!("Jobs eligible for deletion: {}", eligible_count);
|
||||
for id in &chunk_ids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO job_stats (job_id, workspace_id) VALUES ($1, 'admins')
|
||||
ON CONFLICT DO NOTHING",
|
||||
id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if eligible_count == 0 {
|
||||
println!("No jobs to delete. To create test data, run:");
|
||||
println!(" psql $DATABASE_URL -c \"UPDATE global_settings SET value = '3600' WHERE name = 'retention_period_secs';\"");
|
||||
println!(" Then insert expired jobs (see test file header for SQL)");
|
||||
return;
|
||||
for id in &chunk_ids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO job_logs (job_id, workspace_id, logs) VALUES ($1, 'admins', 'test log output')
|
||||
ON CONFLICT DO NOTHING",
|
||||
id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test different batch sizes
|
||||
let batch_sizes = [1000, 5000, 10000];
|
||||
async fn cleanup_test_data(db: &Pool<Postgres>, tag: &str) {
|
||||
sqlx::query!("DELETE FROM jobs_pending_deletion")
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_stats WHERE job_id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"bench_user",
|
||||
tag
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_logs WHERE job_id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"bench_user",
|
||||
tag
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM v2_job WHERE created_by = $1 AND tag = $2)",
|
||||
"bench_user",
|
||||
tag
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM v2_job WHERE created_by = $1 AND tag = $2",
|
||||
"bench_user",
|
||||
tag
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
for batch_size in batch_sizes {
|
||||
let remaining: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM v2_job_completed WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
/// Old method: single transaction, DELETE RETURNING into Vec<Uuid>, then ANY($1) cascades.
|
||||
/// Scoped to jobs with the given `tag` for isolation.
|
||||
async fn run_old_method(
|
||||
db: &Pool<Postgres>,
|
||||
retention_secs: i64,
|
||||
batch_size: i64,
|
||||
tag: &str,
|
||||
) -> (usize, std::time::Duration) {
|
||||
let start = Instant::now();
|
||||
let mut total = 0usize;
|
||||
|
||||
loop {
|
||||
let batch_start = Instant::now();
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
let active_roots: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"SELECT q.id FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
WHERE j.parent_job IS NULL
|
||||
AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
retention_secs
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
.unwrap();
|
||||
|
||||
if remaining == 0 {
|
||||
println!("All jobs deleted, stopping batch size tests");
|
||||
let deleted_jobs: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)
|
||||
AND j.tag = $4
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
retention_secs,
|
||||
batch_size,
|
||||
&active_roots,
|
||||
tag
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let count = deleted_jobs.len();
|
||||
if count == 0 {
|
||||
tx.commit().await.unwrap();
|
||||
break;
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM job_stats WHERE job_id = ANY($1)", &deleted_jobs)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!("DELETE FROM job_logs WHERE job_id = ANY($1)", &deleted_jobs)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!("DELETE FROM v2_job WHERE id = ANY($1)", &deleted_jobs)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2 WHERE job_id = ANY($1)",
|
||||
&deleted_jobs
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
total += count;
|
||||
|
||||
let rate = count as f64 / batch_start.elapsed().as_secs_f64();
|
||||
println!(
|
||||
"\n--- Testing batch_size={} (remaining: {}) ---",
|
||||
batch_size, remaining
|
||||
" [old] batch: {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
count,
|
||||
batch_start.elapsed(),
|
||||
rate
|
||||
);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let mut total_deleted = 0u64;
|
||||
let mut batch_num = 0u32;
|
||||
let max_batches = 5;
|
||||
(total, start.elapsed())
|
||||
}
|
||||
|
||||
loop {
|
||||
if batch_num >= max_batches {
|
||||
println!("Reached max batches limit ({})", max_batches);
|
||||
break;
|
||||
}
|
||||
/// New method: INSERT INTO jobs_pending_deletion, then per-table DELETE USING joins.
|
||||
/// Scoped to jobs with the given `tag` for isolation.
|
||||
async fn run_new_method(
|
||||
db: &Pool<Postgres>,
|
||||
retention_secs: i64,
|
||||
batch_size: i64,
|
||||
tag: &str,
|
||||
) -> (usize, std::time::Duration) {
|
||||
let start = Instant::now();
|
||||
let mut total = 0usize;
|
||||
let instance = "bench";
|
||||
|
||||
let batch_start = Instant::now();
|
||||
loop {
|
||||
let batch_start = Instant::now();
|
||||
|
||||
// Run batched deletion with LIMIT and SKIP LOCKED (same as monitor.rs)
|
||||
let deleted_jobs: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT id FROM v2_job_completed
|
||||
WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
ORDER BY completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
retention_secs,
|
||||
batch_size as i64
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.expect("Failed to delete batch");
|
||||
let active_roots: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"SELECT q.id FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
WHERE j.parent_job IS NULL
|
||||
AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
retention_secs
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let batch_count = deleted_jobs.len();
|
||||
let batch_elapsed = batch_start.elapsed();
|
||||
let result = sqlx::query!(
|
||||
"WITH marked AS (
|
||||
INSERT INTO jobs_pending_deletion (id, marked_by)
|
||||
SELECT jc.id, $5 FROM v2_job_completed jc
|
||||
JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)
|
||||
AND j.tag = $4
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
)
|
||||
DELETE FROM v2_job_completed WHERE id IN (SELECT id FROM marked)",
|
||||
retention_secs,
|
||||
batch_size,
|
||||
&active_roots,
|
||||
tag,
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if batch_count == 0 {
|
||||
println!("No more jobs to delete");
|
||||
break;
|
||||
}
|
||||
|
||||
// Delete related records
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_stats WHERE job_id = ANY($1)",
|
||||
&deleted_jobs
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
sqlx::query!("DELETE FROM job_logs WHERE job_id = ANY($1)", &deleted_jobs)
|
||||
.execute(&db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
sqlx::query!("DELETE FROM v2_job WHERE id = ANY($1)", &deleted_jobs)
|
||||
.execute(&db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2 WHERE job_id = ANY($1)",
|
||||
&deleted_jobs
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
total_deleted += batch_count as u64;
|
||||
batch_num += 1;
|
||||
|
||||
let rate = batch_count as f64 / batch_elapsed.as_secs_f64();
|
||||
println!(
|
||||
" Batch {}: deleted {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
batch_num, batch_count, batch_elapsed, rate
|
||||
);
|
||||
let count = result.rows_affected() as usize;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let total_elapsed = start.elapsed();
|
||||
let overall_rate = if total_elapsed.as_secs_f64() > 0.0 {
|
||||
total_deleted as f64 / total_elapsed.as_secs_f64()
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_stats USING jobs_pending_deletion d
|
||||
WHERE job_stats.job_id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_logs USING jobs_pending_deletion d
|
||||
WHERE job_logs.job_id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM v2_job USING jobs_pending_deletion d
|
||||
WHERE v2_job.id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2 USING jobs_pending_deletion d
|
||||
WHERE job_result_stream_v2.job_id = d.id AND d.marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
"DELETE FROM jobs_pending_deletion WHERE marked_by = $1",
|
||||
instance
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
total += count;
|
||||
|
||||
let rate = count as f64 / batch_start.elapsed().as_secs_f64();
|
||||
println!(
|
||||
" [new] batch: {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
count,
|
||||
batch_start.elapsed(),
|
||||
rate
|
||||
);
|
||||
}
|
||||
|
||||
(total, start.elapsed())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_compare_cleanup_methods() {
|
||||
let db = connect().await;
|
||||
let job_count = std::env::var("BENCH_JOB_COUNT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_JOB_COUNT);
|
||||
|
||||
let retention_secs: i64 = 600;
|
||||
let batch_sizes: &[i64] = &[1000, 5000, 10000, 20000];
|
||||
|
||||
for &batch_size in batch_sizes {
|
||||
println!(
|
||||
"\n========== batch_size={}, job_count={} ==========",
|
||||
batch_size, job_count
|
||||
);
|
||||
|
||||
let tag_old = format!("bench_old_{}", batch_size);
|
||||
let tag_new = format!("bench_new_{}", batch_size);
|
||||
|
||||
// Clean slate for both tags
|
||||
cleanup_test_data(&db, &tag_old).await;
|
||||
cleanup_test_data(&db, &tag_new).await;
|
||||
|
||||
// --- Old method ---
|
||||
println!("[old] Inserting {} test jobs...", job_count);
|
||||
insert_test_jobs(&db, job_count, &tag_old).await;
|
||||
println!("[old] Running old method (in-memory UUID, single tx)...");
|
||||
let (old_total, old_dur) = run_old_method(&db, retention_secs, batch_size, &tag_old).await;
|
||||
let old_rate = if old_dur.as_secs_f64() > 0.0 {
|
||||
old_total as f64 / old_dur.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
println!(
|
||||
"batch_size={}: deleted {} jobs in {} batches, total time {:?} ({:.0} jobs/sec)",
|
||||
batch_size, total_deleted, batch_num, total_elapsed, overall_rate
|
||||
"[old] Deleted {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
old_total, old_dur, old_rate
|
||||
);
|
||||
cleanup_test_data(&db, &tag_old).await;
|
||||
|
||||
// --- New method ---
|
||||
println!("[new] Inserting {} test jobs...", job_count);
|
||||
insert_test_jobs(&db, job_count, &tag_new).await;
|
||||
println!("[new] Running new method (jobs_pending_deletion table)...");
|
||||
let (new_total, new_dur) = run_new_method(&db, retention_secs, batch_size, &tag_new).await;
|
||||
let new_rate = if new_dur.as_secs_f64() > 0.0 {
|
||||
new_total as f64 / new_dur.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
println!(
|
||||
"[new] Deleted {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
new_total, new_dur, new_rate
|
||||
);
|
||||
cleanup_test_data(&db, &tag_new).await;
|
||||
|
||||
println!("\n--- Summary (batch_size={}) ---", batch_size);
|
||||
println!(
|
||||
" Old: {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
old_total, old_dur, old_rate
|
||||
);
|
||||
println!(
|
||||
" New: {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
new_total, new_dur, new_rate
|
||||
);
|
||||
if old_total != new_total {
|
||||
println!(
|
||||
" WARNING: job counts differ (old={}, new={})",
|
||||
old_total, new_total
|
||||
);
|
||||
}
|
||||
if old_dur.as_secs_f64() > 0.0 && new_dur.as_secs_f64() > 0.0 {
|
||||
let speedup = old_dur.as_secs_f64() / new_dur.as_secs_f64();
|
||||
println!(" Speedup: {:.2}x", speedup);
|
||||
}
|
||||
}
|
||||
|
||||
let final_count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM v2_job_completed WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
retention_secs
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"\nFinal eligible count: {} (started with {})",
|
||||
final_count, eligible_count
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_skip_locked_prevents_contention() {
|
||||
// This test verifies that SKIP LOCKED allows concurrent cleanup without deadlocks
|
||||
let database_url = std::env::var("DATABASE_URL")
|
||||
.unwrap_or("postgres://postgres:changeme@localhost:5432/windmill".to_string());
|
||||
async fn test_old_method() {
|
||||
let db = connect().await;
|
||||
let retention_secs: i64 = 600;
|
||||
let batch_size: i64 = 10000;
|
||||
let job_count = 10_000;
|
||||
let tag = "bench_old_solo";
|
||||
|
||||
let db = PgPoolOptions::new()
|
||||
.max_connections(10)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
cleanup_test_data(&db, tag).await;
|
||||
println!("Inserting {} test jobs...", job_count);
|
||||
insert_test_jobs(&db, job_count, tag).await;
|
||||
println!("Running old method...");
|
||||
let (total, dur) = run_old_method(&db, retention_secs, batch_size, tag).await;
|
||||
println!(
|
||||
"Deleted {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
total,
|
||||
dur,
|
||||
total as f64 / dur.as_secs_f64()
|
||||
);
|
||||
cleanup_test_data(&db, tag).await;
|
||||
}
|
||||
|
||||
let retention_secs: i64 = sqlx::query_scalar!(
|
||||
"SELECT COALESCE((SELECT value::bigint FROM global_settings WHERE name = 'retention_period_secs'), 2592000)"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.expect("Failed to get retention period")
|
||||
.unwrap_or(2592000);
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_new_method() {
|
||||
let db = connect().await;
|
||||
let retention_secs: i64 = 600;
|
||||
let batch_size: i64 = 10000;
|
||||
let job_count = 10_000;
|
||||
let tag = "bench_new_solo";
|
||||
|
||||
let eligible: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM v2_job_completed WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
cleanup_test_data(&db, tag).await;
|
||||
println!("Inserting {} test jobs...", job_count);
|
||||
insert_test_jobs(&db, job_count, tag).await;
|
||||
println!("Running new method...");
|
||||
let (total, dur) = run_new_method(&db, retention_secs, batch_size, tag).await;
|
||||
println!(
|
||||
"Deleted {} jobs in {:?} ({:.0} jobs/sec)",
|
||||
total,
|
||||
dur,
|
||||
total as f64 / dur.as_secs_f64()
|
||||
);
|
||||
cleanup_test_data(&db, tag).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_crash_recovery() {
|
||||
let db = connect().await;
|
||||
let retention_secs: i64 = 600;
|
||||
let tag = "bench_crash_recovery";
|
||||
let instance = "bench";
|
||||
|
||||
cleanup_test_data(&db, tag).await;
|
||||
println!("Inserting 1000 test jobs...");
|
||||
insert_test_jobs(&db, 1000, tag).await;
|
||||
|
||||
// Simulate: mark jobs but don't delete them (mimics crash after step 1)
|
||||
let active_roots: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"SELECT q.id FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
WHERE j.parent_job IS NULL
|
||||
AND j.created_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
retention_secs
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
.unwrap();
|
||||
|
||||
println!("Retention period: {} seconds", retention_secs);
|
||||
println!("Jobs eligible for deletion: {}", eligible);
|
||||
let marked = sqlx::query!(
|
||||
"INSERT INTO jobs_pending_deletion (id, marked_by)
|
||||
SELECT jc.id, $4 FROM v2_job_completed jc
|
||||
JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($2)
|
||||
AND j.tag = $3
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT 1000
|
||||
ON CONFLICT DO NOTHING",
|
||||
retention_secs,
|
||||
&active_roots,
|
||||
tag,
|
||||
instance
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if eligible == 0 {
|
||||
println!("No jobs to delete, skipping concurrent test");
|
||||
return;
|
||||
}
|
||||
let marked_count = marked.rows_affected();
|
||||
println!(
|
||||
"Marked {} jobs (simulating crash before deletion)",
|
||||
marked_count
|
||||
);
|
||||
assert!(marked_count > 0, "Should have marked some jobs");
|
||||
|
||||
let batch_size: i64 = 1000;
|
||||
|
||||
// Run 3 concurrent cleanup tasks (simulating multiple server replicas)
|
||||
let handles: Vec<_> = (0..3)
|
||||
.map(|worker_id| {
|
||||
let db = db.clone();
|
||||
tokio::spawn(async move {
|
||||
let start = Instant::now();
|
||||
let mut total = 0u64;
|
||||
|
||||
for _batch in 0..3 {
|
||||
let deleted: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT id FROM v2_job_completed
|
||||
WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
ORDER BY completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
retention_secs,
|
||||
batch_size
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let count = deleted.len();
|
||||
total += count as u64;
|
||||
|
||||
if count > 0 {
|
||||
sqlx::query!("DELETE FROM v2_job WHERE id = ANY($1)", &deleted)
|
||||
.execute(&db)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Worker {}: deleted {} jobs in {:?}",
|
||||
worker_id,
|
||||
total,
|
||||
start.elapsed()
|
||||
);
|
||||
total
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let results: Vec<u64> = futures::future::join_all(handles)
|
||||
// Verify pending_deletion has rows
|
||||
let pending: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM jobs_pending_deletion")
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|r| r.unwrap_or(0))
|
||||
.collect();
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
assert!(pending > 0, "jobs_pending_deletion should have rows");
|
||||
|
||||
let total: u64 = results.iter().sum();
|
||||
println!("\nTotal deleted by all workers: {}", total);
|
||||
println!("Per worker: {:?}", results);
|
||||
println!("\nWith SKIP LOCKED: no deadlocks, work distributed across workers");
|
||||
// Now run the new method — it should detect leftovers and resume
|
||||
println!("Running new method (should resume from crash)...");
|
||||
let (total, dur) = run_new_method(&db, retention_secs, 1000, tag).await;
|
||||
println!("Recovered and deleted {} jobs in {:?}", total, dur);
|
||||
|
||||
// Verify cleanup
|
||||
let remaining: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM jobs_pending_deletion")
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
assert_eq!(
|
||||
remaining, 0,
|
||||
"jobs_pending_deletion should be empty after recovery"
|
||||
);
|
||||
|
||||
cleanup_test_data(&db, tag).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user