oban 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oban/__init__.py +22 -0
- oban/__main__.py +12 -0
- oban/_backoff.py +87 -0
- oban/_config.py +171 -0
- oban/_executor.py +188 -0
- oban/_extensions.py +16 -0
- oban/_leader.py +118 -0
- oban/_lifeline.py +77 -0
- oban/_notifier.py +324 -0
- oban/_producer.py +334 -0
- oban/_pruner.py +93 -0
- oban/_query.py +409 -0
- oban/_recorded.py +34 -0
- oban/_refresher.py +88 -0
- oban/_scheduler.py +359 -0
- oban/_stager.py +115 -0
- oban/_worker.py +78 -0
- oban/cli.py +436 -0
- oban/decorators.py +218 -0
- oban/job.py +315 -0
- oban/oban.py +1084 -0
- oban/py.typed +0 -0
- oban/queries/__init__.py +0 -0
- oban/queries/ack_job.sql +11 -0
- oban/queries/all_jobs.sql +25 -0
- oban/queries/cancel_many_jobs.sql +37 -0
- oban/queries/cleanup_expired_leaders.sql +4 -0
- oban/queries/cleanup_expired_producers.sql +2 -0
- oban/queries/delete_many_jobs.sql +5 -0
- oban/queries/delete_producer.sql +2 -0
- oban/queries/elect_leader.sql +10 -0
- oban/queries/fetch_jobs.sql +44 -0
- oban/queries/get_job.sql +23 -0
- oban/queries/insert_job.sql +28 -0
- oban/queries/insert_producer.sql +2 -0
- oban/queries/install.sql +113 -0
- oban/queries/prune_jobs.sql +18 -0
- oban/queries/reelect_leader.sql +12 -0
- oban/queries/refresh_producers.sql +3 -0
- oban/queries/rescue_jobs.sql +18 -0
- oban/queries/reset.sql +5 -0
- oban/queries/resign_leader.sql +4 -0
- oban/queries/retry_many_jobs.sql +13 -0
- oban/queries/stage_jobs.sql +34 -0
- oban/queries/uninstall.sql +4 -0
- oban/queries/update_job.sql +54 -0
- oban/queries/update_producer.sql +3 -0
- oban/queries/verify_structure.sql +9 -0
- oban/schema.py +115 -0
- oban/telemetry/__init__.py +10 -0
- oban/telemetry/core.py +170 -0
- oban/telemetry/logger.py +147 -0
- oban/testing.py +439 -0
- oban-0.5.0.dist-info/METADATA +290 -0
- oban-0.5.0.dist-info/RECORD +59 -0
- oban-0.5.0.dist-info/WHEEL +5 -0
- oban-0.5.0.dist-info/entry_points.txt +2 -0
- oban-0.5.0.dist-info/licenses/LICENSE.txt +201 -0
- oban-0.5.0.dist-info/top_level.txt +1 -0
oban/py.typed
ADDED
|
File without changes
|
oban/queries/__init__.py
ADDED
|
File without changes
|
oban/queries/ack_job.sql
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
UPDATE oban_jobs
|
|
2
|
+
SET state = %(state)s::oban_job_state,
|
|
3
|
+
cancelled_at = CASE WHEN %(state)s::oban_job_state = 'cancelled' THEN timezone('UTC', now()) ELSE cancelled_at END,
|
|
4
|
+
completed_at = CASE WHEN %(state)s::oban_job_state = 'completed' THEN timezone('UTC', now()) ELSE completed_at END,
|
|
5
|
+
discarded_at = CASE WHEN %(state)s::oban_job_state = 'discarded' THEN timezone('UTC', now()) ELSE discarded_at END,
|
|
6
|
+
scheduled_at = CASE WHEN %(schedule_in)s::int IS NULL THEN scheduled_at ELSE timezone('UTC', now()) + make_interval(secs => %(schedule_in)s::int) END,
|
|
7
|
+
attempt = COALESCE(%(attempt_change)s::int + attempt, attempt),
|
|
8
|
+
errors = CASE WHEN %(error)s::jsonb IS NULL THEN errors ELSE errors || %(error)s::jsonb END,
|
|
9
|
+
meta = CASE WHEN %(meta)s::jsonb IS NULL THEN meta ELSE meta || %(meta)s::jsonb END
|
|
10
|
+
WHERE id = %(id)s
|
|
11
|
+
RETURNING id;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
id,
|
|
3
|
+
state,
|
|
4
|
+
queue,
|
|
5
|
+
worker,
|
|
6
|
+
attempt,
|
|
7
|
+
max_attempts,
|
|
8
|
+
priority,
|
|
9
|
+
args,
|
|
10
|
+
meta,
|
|
11
|
+
errors,
|
|
12
|
+
tags,
|
|
13
|
+
attempted_by,
|
|
14
|
+
inserted_at,
|
|
15
|
+
attempted_at,
|
|
16
|
+
cancelled_at,
|
|
17
|
+
completed_at,
|
|
18
|
+
discarded_at,
|
|
19
|
+
scheduled_at
|
|
20
|
+
FROM
|
|
21
|
+
oban_jobs
|
|
22
|
+
WHERE
|
|
23
|
+
state = ANY(%(states)s)
|
|
24
|
+
ORDER BY
|
|
25
|
+
id DESC
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
WITH locked_jobs AS (
|
|
2
|
+
SELECT
|
|
3
|
+
id, state
|
|
4
|
+
FROM
|
|
5
|
+
oban_jobs
|
|
6
|
+
WHERE
|
|
7
|
+
id = ANY(%(ids)s)
|
|
8
|
+
AND state IN ('executing', 'available', 'scheduled', 'retryable', 'suspended')
|
|
9
|
+
FOR UPDATE
|
|
10
|
+
),
|
|
11
|
+
updated_jobs AS (
|
|
12
|
+
UPDATE
|
|
13
|
+
oban_jobs oj
|
|
14
|
+
SET
|
|
15
|
+
state = CASE
|
|
16
|
+
WHEN oj.state = 'executing' THEN oj.state
|
|
17
|
+
ELSE 'cancelled'::oban_job_state
|
|
18
|
+
END,
|
|
19
|
+
cancelled_at = CASE
|
|
20
|
+
WHEN oj.state = 'executing' THEN oj.cancelled_at
|
|
21
|
+
ELSE timezone('UTC', now())
|
|
22
|
+
END,
|
|
23
|
+
meta = CASE
|
|
24
|
+
WHEN oj.state = 'executing' THEN oj.meta
|
|
25
|
+
ELSE oj.meta || jsonb_build_object('cancel_attempted_at', timezone('UTC', now()))
|
|
26
|
+
END
|
|
27
|
+
FROM
|
|
28
|
+
locked_jobs
|
|
29
|
+
WHERE
|
|
30
|
+
oj.id = locked_jobs.id
|
|
31
|
+
RETURNING
|
|
32
|
+
oj.id, locked_jobs.state AS original_state
|
|
33
|
+
)
|
|
34
|
+
SELECT
|
|
35
|
+
id, original_state
|
|
36
|
+
FROM
|
|
37
|
+
updated_jobs
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
WITH locked_jobs AS (
|
|
2
|
+
SELECT
|
|
3
|
+
priority, scheduled_at, id
|
|
4
|
+
FROM
|
|
5
|
+
oban_jobs
|
|
6
|
+
WHERE
|
|
7
|
+
state = 'available'
|
|
8
|
+
AND queue = %(queue)s
|
|
9
|
+
ORDER BY
|
|
10
|
+
priority ASC, scheduled_at ASC, id ASC
|
|
11
|
+
LIMIT
|
|
12
|
+
%(demand)s
|
|
13
|
+
FOR UPDATE SKIP LOCKED
|
|
14
|
+
)
|
|
15
|
+
UPDATE
|
|
16
|
+
oban_jobs oj
|
|
17
|
+
SET
|
|
18
|
+
attempt = oj.attempt + 1,
|
|
19
|
+
attempted_at = timezone('UTC', now()),
|
|
20
|
+
attempted_by = %(attempted_by)s,
|
|
21
|
+
state = 'executing'
|
|
22
|
+
FROM
|
|
23
|
+
locked_jobs
|
|
24
|
+
WHERE
|
|
25
|
+
oj.id = locked_jobs.id
|
|
26
|
+
RETURNING
|
|
27
|
+
oj.id,
|
|
28
|
+
oj.state,
|
|
29
|
+
oj.queue,
|
|
30
|
+
oj.worker,
|
|
31
|
+
oj.attempt,
|
|
32
|
+
oj.max_attempts,
|
|
33
|
+
oj.priority,
|
|
34
|
+
oj.args,
|
|
35
|
+
oj.meta,
|
|
36
|
+
oj.errors,
|
|
37
|
+
oj.tags,
|
|
38
|
+
oj.attempted_by,
|
|
39
|
+
oj.inserted_at,
|
|
40
|
+
oj.attempted_at,
|
|
41
|
+
oj.cancelled_at,
|
|
42
|
+
oj.completed_at,
|
|
43
|
+
oj.discarded_at,
|
|
44
|
+
oj.scheduled_at
|
oban/queries/get_job.sql
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
id,
|
|
3
|
+
state,
|
|
4
|
+
queue,
|
|
5
|
+
worker,
|
|
6
|
+
attempt,
|
|
7
|
+
max_attempts,
|
|
8
|
+
priority,
|
|
9
|
+
args,
|
|
10
|
+
meta,
|
|
11
|
+
errors,
|
|
12
|
+
tags,
|
|
13
|
+
attempted_by,
|
|
14
|
+
inserted_at,
|
|
15
|
+
attempted_at,
|
|
16
|
+
cancelled_at,
|
|
17
|
+
completed_at,
|
|
18
|
+
discarded_at,
|
|
19
|
+
scheduled_at
|
|
20
|
+
FROM
|
|
21
|
+
oban_jobs
|
|
22
|
+
WHERE
|
|
23
|
+
id = %s
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
INSERT INTO oban_jobs(
|
|
2
|
+
args,
|
|
3
|
+
inserted_at,
|
|
4
|
+
max_attempts,
|
|
5
|
+
meta,
|
|
6
|
+
priority,
|
|
7
|
+
queue,
|
|
8
|
+
scheduled_at,
|
|
9
|
+
state,
|
|
10
|
+
tags,
|
|
11
|
+
worker
|
|
12
|
+
) VALUES (
|
|
13
|
+
%(args)s,
|
|
14
|
+
coalesce(%(inserted_at)s, timezone('UTC', now())),
|
|
15
|
+
%(max_attempts)s,
|
|
16
|
+
%(meta)s,
|
|
17
|
+
%(priority)s,
|
|
18
|
+
%(queue)s,
|
|
19
|
+
coalesce(%(scheduled_at)s, timezone('UTC', now())),
|
|
20
|
+
CASE
|
|
21
|
+
WHEN %(state)s = 'available' AND %(scheduled_at)s IS NOT NULL
|
|
22
|
+
THEN 'scheduled'::oban_job_state
|
|
23
|
+
ELSE %(state)s::oban_job_state
|
|
24
|
+
END,
|
|
25
|
+
%(tags)s,
|
|
26
|
+
%(worker)s
|
|
27
|
+
)
|
|
28
|
+
RETURNING id, inserted_at, queue, scheduled_at, state;
|
oban/queries/install.sql
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
-- Types
|
|
2
|
+
|
|
3
|
+
DO $$
|
|
4
|
+
BEGIN
|
|
5
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'oban_job_state') THEN
|
|
6
|
+
CREATE TYPE oban_job_state AS ENUM (
|
|
7
|
+
'available',
|
|
8
|
+
'scheduled',
|
|
9
|
+
'suspended',
|
|
10
|
+
'executing',
|
|
11
|
+
'retryable',
|
|
12
|
+
'completed',
|
|
13
|
+
'discarded',
|
|
14
|
+
'cancelled'
|
|
15
|
+
);
|
|
16
|
+
END IF;
|
|
17
|
+
END
|
|
18
|
+
$$;
|
|
19
|
+
|
|
20
|
+
-- Tables
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS oban_jobs (
|
|
23
|
+
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
|
24
|
+
state oban_job_state NOT NULL DEFAULT 'available',
|
|
25
|
+
queue text NOT NULL DEFAULT 'default',
|
|
26
|
+
worker text NOT NULL,
|
|
27
|
+
attempt smallint NOT NULL DEFAULT 0,
|
|
28
|
+
max_attempts smallint NOT NULL DEFAULT 20,
|
|
29
|
+
priority smallint NOT NULL DEFAULT 0,
|
|
30
|
+
args jsonb NOT NULL DEFAULT '{}',
|
|
31
|
+
meta jsonb NOT NULL DEFAULT '{}',
|
|
32
|
+
tags jsonb NOT NULL DEFAULT '[]',
|
|
33
|
+
errors jsonb NOT NULL DEFAULT '[]',
|
|
34
|
+
attempted_by text[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
|
35
|
+
inserted_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
|
|
36
|
+
scheduled_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
|
|
37
|
+
attempted_at timestamp WITHOUT TIME ZONE,
|
|
38
|
+
cancelled_at timestamp WITHOUT TIME ZONE,
|
|
39
|
+
completed_at timestamp WITHOUT TIME ZONE,
|
|
40
|
+
discarded_at timestamp WITHOUT TIME ZONE,
|
|
41
|
+
|
|
42
|
+
CONSTRAINT attempt_range CHECK (attempt >= 0 AND attempt <= max_attempts),
|
|
43
|
+
CONSTRAINT queue_length CHECK (char_length(queue) > 0),
|
|
44
|
+
CONSTRAINT worker_length CHECK (char_length(worker) > 0),
|
|
45
|
+
CONSTRAINT positive_max_attempts CHECK (max_attempts > 0),
|
|
46
|
+
CONSTRAINT non_negative_priority CHECK (priority >= 0)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS oban_leaders (
|
|
50
|
+
name text PRIMARY KEY DEFAULT 'oban',
|
|
51
|
+
node text NOT NULL,
|
|
52
|
+
elected_at timestamp WITHOUT TIME ZONE NOT NULL,
|
|
53
|
+
expires_at timestamp WITHOUT TIME ZONE NOT NULL
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS oban_producers (
|
|
57
|
+
uuid uuid PRIMARY KEY,
|
|
58
|
+
name text NOT NULL DEFAULT 'oban',
|
|
59
|
+
node text NOT NULL,
|
|
60
|
+
queue text NOT NULL,
|
|
61
|
+
meta jsonb NOT NULL DEFAULT '{}',
|
|
62
|
+
started_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
|
|
63
|
+
updated_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now())
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Indexes
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS oban_jobs_state_queue_priority_scheduled_at_id_index
|
|
69
|
+
ON oban_jobs (state, queue, priority, scheduled_at, id)
|
|
70
|
+
WITH (fillfactor = 90);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS oban_jobs_staging_index
|
|
73
|
+
ON oban_jobs (scheduled_at, id)
|
|
74
|
+
WHERE state IN ('scheduled', 'retryable');
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS oban_jobs_completed_at_index
|
|
77
|
+
ON oban_jobs (completed_at)
|
|
78
|
+
WHERE state = 'completed';
|
|
79
|
+
|
|
80
|
+
CREATE INDEX IF NOT EXISTS oban_jobs_cancelled_at_index
|
|
81
|
+
ON oban_jobs (cancelled_at)
|
|
82
|
+
WHERE state = 'cancelled';
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS oban_jobs_discarded_at_index
|
|
85
|
+
ON oban_jobs (discarded_at)
|
|
86
|
+
WHERE state = 'discarded';
|
|
87
|
+
|
|
88
|
+
-- Autovacuum
|
|
89
|
+
|
|
90
|
+
ALTER TABLE oban_jobs SET (
|
|
91
|
+
-- Vacuum earlier on large tables
|
|
92
|
+
autovacuum_vacuum_scale_factor = 0.02,
|
|
93
|
+
autovacuum_vacuum_threshold = 50,
|
|
94
|
+
|
|
95
|
+
-- Keep stats fresh for the planner
|
|
96
|
+
autovacuum_analyze_scale_factor = 0.02,
|
|
97
|
+
autovacuum_analyze_threshold = 100,
|
|
98
|
+
|
|
99
|
+
-- Make autovacuum push harder with little/no sleeping
|
|
100
|
+
autovacuum_vacuum_cost_limit = 2000,
|
|
101
|
+
autovacuum_vacuum_cost_delay = 1,
|
|
102
|
+
|
|
103
|
+
-- Handle insert-heavy spikes (PG13+)
|
|
104
|
+
autovacuum_vacuum_insert_scale_factor = 0.02,
|
|
105
|
+
autovacuum_vacuum_insert_threshold = 1000,
|
|
106
|
+
|
|
107
|
+
-- Leave headroom on pages for locality and fewer page splits
|
|
108
|
+
fillfactor = 85
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
-- Version
|
|
112
|
+
|
|
113
|
+
COMMENT ON TABLE oban_jobs IS '1';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
WITH jobs_to_delete AS (
|
|
2
|
+
SELECT
|
|
3
|
+
id
|
|
4
|
+
FROM
|
|
5
|
+
oban_jobs
|
|
6
|
+
WHERE
|
|
7
|
+
(state = 'completed' AND completed_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s)) OR
|
|
8
|
+
(state = 'cancelled' AND cancelled_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s)) OR
|
|
9
|
+
(state = 'discarded' AND discarded_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s))
|
|
10
|
+
ORDER BY
|
|
11
|
+
id ASC
|
|
12
|
+
LIMIT
|
|
13
|
+
%(limit)s
|
|
14
|
+
)
|
|
15
|
+
DELETE FROM
|
|
16
|
+
oban_jobs
|
|
17
|
+
WHERE
|
|
18
|
+
id IN (SELECT id FROM jobs_to_delete)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
INSERT INTO oban_leaders (name, node, elected_at, expires_at)
|
|
2
|
+
VALUES (
|
|
3
|
+
%(name)s,
|
|
4
|
+
%(node)s,
|
|
5
|
+
timezone('UTC', now()),
|
|
6
|
+
timezone('UTC', now()) + interval '%(ttl)s seconds'
|
|
7
|
+
)
|
|
8
|
+
ON CONFLICT (name) DO UPDATE SET
|
|
9
|
+
expires_at = EXCLUDED.expires_at
|
|
10
|
+
WHERE
|
|
11
|
+
oban_leaders.node = EXCLUDED.node
|
|
12
|
+
RETURNING node
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
UPDATE
|
|
2
|
+
oban_jobs
|
|
3
|
+
SET
|
|
4
|
+
state = CASE
|
|
5
|
+
WHEN attempt >= max_attempts THEN 'discarded'::oban_job_state
|
|
6
|
+
ELSE 'available'::oban_job_state
|
|
7
|
+
END,
|
|
8
|
+
discarded_at = CASE
|
|
9
|
+
WHEN attempt >= max_attempts THEN timezone('UTC', now())
|
|
10
|
+
ELSE discarded_at
|
|
11
|
+
END,
|
|
12
|
+
meta = CASE
|
|
13
|
+
WHEN attempt >= max_attempts THEN meta
|
|
14
|
+
ELSE meta || jsonb_build_object('rescued', coalesce((meta->>'rescued')::int, 0) + 1)
|
|
15
|
+
END
|
|
16
|
+
WHERE
|
|
17
|
+
state = 'executing'
|
|
18
|
+
AND attempted_at < timezone('UTC', now()) - make_interval(secs => %(rescue_after)s)
|
oban/queries/reset.sql
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
UPDATE
|
|
2
|
+
oban_jobs
|
|
3
|
+
SET
|
|
4
|
+
state = 'available'::oban_job_state,
|
|
5
|
+
max_attempts = GREATEST(max_attempts, attempt + 1),
|
|
6
|
+
scheduled_at = timezone('UTC', now()),
|
|
7
|
+
completed_at = NULL,
|
|
8
|
+
cancelled_at = NULL,
|
|
9
|
+
discarded_at = NULL,
|
|
10
|
+
meta = jsonb_set(meta, '{uniq_bmp}', '[]'::jsonb)
|
|
11
|
+
WHERE
|
|
12
|
+
id = ANY(%(ids)s)
|
|
13
|
+
AND state NOT IN ('available', 'executing')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
WITH locked_jobs AS (
|
|
2
|
+
SELECT
|
|
3
|
+
id
|
|
4
|
+
FROM
|
|
5
|
+
oban_jobs
|
|
6
|
+
WHERE
|
|
7
|
+
state = ANY('{scheduled,retryable}')
|
|
8
|
+
AND scheduled_at <= coalesce(%(before)s, timezone('UTC', now()))
|
|
9
|
+
ORDER BY
|
|
10
|
+
scheduled_at ASC, id ASC
|
|
11
|
+
LIMIT
|
|
12
|
+
%(limit)s
|
|
13
|
+
FOR UPDATE SKIP LOCKED
|
|
14
|
+
),
|
|
15
|
+
updated_jobs AS (
|
|
16
|
+
UPDATE
|
|
17
|
+
oban_jobs
|
|
18
|
+
SET
|
|
19
|
+
state = 'available'::oban_job_state
|
|
20
|
+
FROM
|
|
21
|
+
locked_jobs
|
|
22
|
+
WHERE
|
|
23
|
+
oban_jobs.id = locked_jobs.id
|
|
24
|
+
)
|
|
25
|
+
SELECT DISTINCT
|
|
26
|
+
q.queue
|
|
27
|
+
FROM
|
|
28
|
+
unnest(%(queues)s::text[]) AS q(queue)
|
|
29
|
+
WHERE
|
|
30
|
+
EXISTS (
|
|
31
|
+
SELECT 1
|
|
32
|
+
FROM oban_jobs
|
|
33
|
+
WHERE state = 'available' AND queue = q.queue
|
|
34
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
WITH raw_job_data AS (
|
|
2
|
+
SELECT
|
|
3
|
+
unnest(%(ids)s::bigint[]) AS id,
|
|
4
|
+
unnest(%(args)s::jsonb[]) AS args,
|
|
5
|
+
unnest(%(max_attempts)s::smallint[]) AS max_attempts,
|
|
6
|
+
unnest(%(meta)s::jsonb[]) AS meta,
|
|
7
|
+
unnest(%(priority)s::smallint[]) AS priority,
|
|
8
|
+
unnest(%(queue)s::text[]) AS queue,
|
|
9
|
+
unnest(%(scheduled_at)s::timestamptz[]) AS scheduled_at,
|
|
10
|
+
unnest(%(tags)s::jsonb[]) AS tags,
|
|
11
|
+
unnest(%(worker)s::text[]) AS worker
|
|
12
|
+
),
|
|
13
|
+
locked_jobs AS (
|
|
14
|
+
SELECT
|
|
15
|
+
id
|
|
16
|
+
FROM
|
|
17
|
+
oban_jobs
|
|
18
|
+
WHERE
|
|
19
|
+
id = ANY(%(ids)s)
|
|
20
|
+
FOR UPDATE
|
|
21
|
+
)
|
|
22
|
+
UPDATE
|
|
23
|
+
oban_jobs oj
|
|
24
|
+
SET
|
|
25
|
+
args = rjd.args,
|
|
26
|
+
max_attempts = rjd.max_attempts,
|
|
27
|
+
meta = rjd.meta,
|
|
28
|
+
priority = rjd.priority,
|
|
29
|
+
queue = rjd.queue,
|
|
30
|
+
scheduled_at = rjd.scheduled_at,
|
|
31
|
+
state = CASE
|
|
32
|
+
WHEN oj.state = 'available' AND rjd.scheduled_at > timezone('UTC', now())
|
|
33
|
+
THEN 'scheduled'::oban_job_state
|
|
34
|
+
WHEN oj.state = 'scheduled' AND rjd.scheduled_at <= timezone('UTC', now())
|
|
35
|
+
THEN 'available'::oban_job_state
|
|
36
|
+
ELSE oj.state
|
|
37
|
+
END,
|
|
38
|
+
tags = rjd.tags,
|
|
39
|
+
worker = rjd.worker
|
|
40
|
+
FROM
|
|
41
|
+
raw_job_data rjd
|
|
42
|
+
INNER JOIN locked_jobs lj ON rjd.id = lj.id
|
|
43
|
+
WHERE
|
|
44
|
+
oj.id = rjd.id
|
|
45
|
+
RETURNING
|
|
46
|
+
oj.args,
|
|
47
|
+
oj.max_attempts,
|
|
48
|
+
oj.meta,
|
|
49
|
+
oj.priority,
|
|
50
|
+
oj.queue,
|
|
51
|
+
oj.scheduled_at,
|
|
52
|
+
oj.state,
|
|
53
|
+
oj.tags,
|
|
54
|
+
oj.worker;
|
oban/schema.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Database schema installation for Oban."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ._query import Query
|
|
6
|
+
|
|
7
|
+
INITIAL_VERSION = 1
|
|
8
|
+
CURRENT_VERSION = 1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def install_sql(prefix: str = "public") -> str:
|
|
12
|
+
"""Get the SQL for installing Oban.
|
|
13
|
+
|
|
14
|
+
Returns the raw SQL statements for creating Oban types, tables, and indexes.
|
|
15
|
+
This is intended for integration with migration frameworks like Django or Alembic.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
prefix: PostgreSQL schema where Oban tables will be located (default: "public")
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
SQL string for schema installation
|
|
22
|
+
|
|
23
|
+
Example (Alembic):
|
|
24
|
+
>>> from alembic import op
|
|
25
|
+
>>> from oban.schema import install_sql
|
|
26
|
+
>>>
|
|
27
|
+
>>> def upgrade():
|
|
28
|
+
... op.execute(install_sql())
|
|
29
|
+
|
|
30
|
+
Example (Django):
|
|
31
|
+
>>> from django.db import migrations
|
|
32
|
+
>>> from oban.schema import install_sql
|
|
33
|
+
>>>
|
|
34
|
+
>>> class Migration(migrations.Migration):
|
|
35
|
+
... operations = [
|
|
36
|
+
... migrations.RunSQL(install_sql()),
|
|
37
|
+
... ]
|
|
38
|
+
"""
|
|
39
|
+
return Query._load_file("install.sql", prefix)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def uninstall_sql(prefix: str = "public") -> str:
|
|
43
|
+
"""Get the SQL for uninstalling Oban.
|
|
44
|
+
|
|
45
|
+
Returns the raw SQL statements for dropping Oban tables and types.
|
|
46
|
+
Useful for integration with migration frameworks like Alembic or Django.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
prefix: PostgreSQL schema where Oban tables are located (default: "public")
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
SQL string for schema uninstallation
|
|
53
|
+
|
|
54
|
+
Example (Alembic):
|
|
55
|
+
>>> from alembic import op
|
|
56
|
+
>>> from oban.schema import uninstall_sql
|
|
57
|
+
>>>
|
|
58
|
+
>>> def downgrade():
|
|
59
|
+
... op.execute(uninstall_sql())
|
|
60
|
+
|
|
61
|
+
Example (Django):
|
|
62
|
+
>>> from django.db import migrations
|
|
63
|
+
>>> from oban.schema import uninstall_sql
|
|
64
|
+
>>>
|
|
65
|
+
>>> class Migration(migrations.Migration):
|
|
66
|
+
... operations = [
|
|
67
|
+
... migrations.RunSQL(uninstall_sql()),
|
|
68
|
+
... ]
|
|
69
|
+
"""
|
|
70
|
+
return Query._load_file("uninstall.sql", prefix)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def install(pool: Any, prefix: str = "public") -> None:
|
|
74
|
+
"""Install Oban in the specified database.
|
|
75
|
+
|
|
76
|
+
Creates all necessary types, tables, and indexes for Oban to function. The
|
|
77
|
+
installation is wrapped in a DDL transaction to ensure the operation is
|
|
78
|
+
atomic.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
pool: A database connection pool (e.g., AsyncConnectionPool)
|
|
82
|
+
prefix: PostgreSQL schema where Oban tables will be located (default: "public")
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> from psycopg_pool import AsyncConnectionPool
|
|
86
|
+
>>> from oban.schema import install
|
|
87
|
+
>>>
|
|
88
|
+
>>> pool = AsyncConnectionPool(conninfo=DATABASE_URL, open=False)
|
|
89
|
+
>>> await pool.open()
|
|
90
|
+
>>> await install(pool)
|
|
91
|
+
"""
|
|
92
|
+
async with pool.connection() as conn:
|
|
93
|
+
await conn.execute(install_sql(prefix))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def uninstall(pool: Any, prefix: str = "public") -> None:
|
|
97
|
+
"""Uninstall Oban from the specified database.
|
|
98
|
+
|
|
99
|
+
Drops all Oban tables and types. The uninstallation is wrapped in a DDL
|
|
100
|
+
transaction to ensure the operation is atomic.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
pool: A database connection pool (e.g., AsyncConnectionPool)
|
|
104
|
+
prefix: PostgreSQL schema where Oban tables are located (default: "public")
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> from psycopg_pool import AsyncConnectionPool
|
|
108
|
+
>>> from oban.schema import uninstall
|
|
109
|
+
>>>
|
|
110
|
+
>>> pool = AsyncConnectionPool(conninfo=DATABASE_URL, open=False)
|
|
111
|
+
>>> await pool.open()
|
|
112
|
+
>>> await uninstall(pool)
|
|
113
|
+
"""
|
|
114
|
+
async with pool.connection() as conn:
|
|
115
|
+
await conn.execute(uninstall_sql(prefix))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight telemetry tooling for agnostic instrumentation.
|
|
3
|
+
|
|
4
|
+
Provides event emission and handler attachment for instrumentation,
|
|
5
|
+
similar to Elixir's `:telemetry` library, but tailored to Oban's needs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import Collector, attach, detach, execute, span
|
|
9
|
+
|
|
10
|
+
__all__ = ["Collector", "attach", "detach", "execute", "span"]
|