postjob 0.2.2 → 0.3.0

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/postjob/cli/db.rb +3 -0
  3. data/lib/postjob/cli/job.rb +15 -7
  4. data/lib/postjob/cli/ps.rb +31 -4
  5. data/lib/postjob/cli/run.rb +4 -2
  6. data/lib/postjob/migrations/002_statuses.rb +18 -0
  7. data/lib/postjob/migrations/003_postjobs.sql +41 -0
  8. data/lib/postjob/migrations/003a_processing.sql +3 -0
  9. data/lib/postjob/migrations/003b_processing_columns.sql +31 -0
  10. data/lib/postjob/migrations/004_tokens.sql +9 -0
  11. data/lib/postjob/migrations/005_helpers.sql +28 -0
  12. data/lib/postjob/migrations/006_enqueue.sql +44 -0
  13. data/lib/postjob/migrations/006a_processing.sql +48 -0
  14. data/lib/postjob/migrations/007_job_results.sql +157 -0
  15. data/lib/postjob/migrations/008_checkout_runnable.sql +78 -0
  16. data/lib/postjob/migrations/008a_childjobs.sql +82 -0
  17. data/lib/postjob/migrations/009_tokens.sql +25 -0
  18. data/lib/postjob/migrations.rb +30 -77
  19. data/lib/postjob/queue/encoder.rb +1 -1
  20. data/lib/postjob/queue/notifications.rb +6 -21
  21. data/lib/postjob/queue/search.rb +4 -4
  22. data/lib/postjob/queue.rb +47 -209
  23. data/lib/postjob/runner.rb +22 -13
  24. data/lib/postjob.rb +21 -19
  25. data/spec/postjob/enqueue_spec.rb +26 -14
  26. data/spec/postjob/job_control/error_status_spec.rb +2 -2
  27. data/spec/postjob/job_control/manual_spec.rb +4 -6
  28. data/spec/postjob/job_control/max_attempts_spec.rb +3 -1
  29. data/spec/postjob/process_job_spec.rb +3 -2
  30. data/spec/postjob/queue/encoder_spec.rb +4 -0
  31. data/spec/postjob/run_spec.rb +1 -1
  32. data/spec/postjob/step_spec.rb +2 -2
  33. data/spec/postjob/{sub_workflow_spec.rb → workflows/child_workflow_spec.rb} +2 -2
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/support/configure_database.rb +1 -0
  36. data/spec/support/test_helper.rb +4 -4
  37. metadata +19 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1ab40be9d1e5d2a5dce344ed912d837bf3ae567
4
- data.tar.gz: 09ee97d4ce95a137525d8efee7e0f7c24a21cb95
3
+ metadata.gz: 586b34b133d67462d64f913cf678ae14ef7954b4
4
+ data.tar.gz: 4ecb46ab647b4337c56af775110d64cc16a36cb5
5
5
  SHA512:
6
- metadata.gz: 2930f208eb34cda94e2570a823f4d32132e6ba58c078a24d0f0803c7bbccf8068b51d3832779a1e1b7eb4c40c0034f1e951970e847bcd1f73806582f3031c735
7
- data.tar.gz: 15ee51d44d1341b538808e4cb875a1c98cb518c34e68b355ef678860de8d1737ee20197fe49dd627d84b4b8803b928714df9a73e60b0a0074548cc930db4d1db
6
+ metadata.gz: a0a946eed383baa9e3c23a0bacc0a52175d917f7f976d6dc6f10b1ffe7e87ab8d3da7add93145f426f2e637613c3a0417dfd5a11404e6ac227d57a765e26a08b
7
+ data.tar.gz: 132c173dc2da7630433511078c8eb7b75766fa00da4d3e52bb88e8a05c84254f1ae27490e7d3dc9ab762af962d6f2e813d42440d1f6e204cd1305e2e16225f48
@@ -48,6 +48,9 @@ module Postjob::CLI
48
48
  USE_ACTIVE_RECORD = false
49
49
 
50
50
  def connect_to_database!
51
+ return if @connected_to_database
52
+
53
+ @connected_to_database = true
51
54
  if USE_ACTIVE_RECORD
52
55
  require "active_record"
53
56
  abc = ::Simple::SQL::Config.read_database_yml
@@ -1,5 +1,3 @@
1
- # rubocop:disable Metrics/MethodLength
2
-
3
1
  module Postjob::CLI
4
2
  # Enqueues a workflow
5
3
  #
@@ -37,15 +35,11 @@ module Postjob::CLI
37
35
  Simple::SQL.ask <<~SQL, job_ids
38
36
  UPDATE postjob.postjobs
39
37
  SET
40
- status='ready', next_run_at=now(),
38
+ status='ready', next_run_at=(now() at time zone 'utc'),
41
39
  results=null, failed_attempts=0, error=NULL, error_message=NULL, error_backtrace=NULL
42
40
  WHERE id = ANY($1);
43
41
  SQL
44
42
 
45
- Simple::SQL.ask <<~SQL
46
- NOTIFY postjob_notifications
47
- SQL
48
-
49
43
  logger.warn "The following jobs have been reset: #{job_ids.join(', ')}"
50
44
  end
51
45
 
@@ -66,6 +60,20 @@ module Postjob::CLI
66
60
  SQL
67
61
  end
68
62
 
63
+ # runs a job as soon as possible
64
+ def job_force(job_id)
65
+ connect_to_database!
66
+
67
+ job_id = Integer(job_id)
68
+
69
+ Simple::SQL.ask <<~SQL, job_id
70
+ UPDATE postjob.postjobs
71
+ SET
72
+ next_run_at=now() at time zone 'utc'
73
+ WHERE id = $1 AND status IN ('ready', 'err', 'sleep')
74
+ SQL
75
+ end
76
+
69
77
  private
70
78
 
71
79
  # parses "foo:bar,baz:quibble" into { "foo" => "bar", "baz" => "quibble"}
@@ -1,3 +1,6 @@
1
+ # rubocop:disable Lint/HandleExceptions
2
+ # rubocop:disable Metrics/ModuleLength
3
+
1
4
  module Postjob::CLI
2
5
  private
3
6
 
@@ -24,7 +27,16 @@ module Postjob::CLI
24
27
  error,
25
28
  COALESCE((results->0)::varchar, error_message) AS result,
26
29
  next_run_at,
27
- (now() at time zone 'utc') - created_at AS age,
30
+ next_run_at - (now() at time zone 'utc') AS next_run_in,
31
+ to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc') - created_at), '999999999.99') AS age,
32
+ CASE WHEN processing_started_at IS NOT NULL THEN
33
+ format(
34
+ '%s/%s',
35
+ to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc') - processing_started_at), '999999999.99'),
36
+ processing_max_duration
37
+ )
38
+ END AS processing,
39
+ COALESCE(processing_client, '') || COALESCE('/' || processing_client_identifier, '') AS worker,
28
40
  tags
29
41
  FROM postjob.postjobs
30
42
  WHERE #{condition_fragment}
@@ -56,6 +68,11 @@ module Postjob::CLI
56
68
  expect! limit => /\A\d+\z/
57
69
  limit = Integer(limit)
58
70
 
71
+ connect_to_database!
72
+
73
+ # check for timed out and zombie processes
74
+ # ::Postjob::Queue.checkout(nil)
75
+
59
76
  unless ids.empty?
60
77
  ps_full(*ids, limit: limit, tags: tags)
61
78
  return
@@ -70,11 +87,20 @@ module Postjob::CLI
70
87
  print_sql limit: limit, query: query
71
88
  end
72
89
 
90
+ def ps_top(*ids, limit: "100", tags: nil)
91
+ loop do
92
+ system "clear"
93
+ ps(*ids, limit: limit, tags: tags)
94
+ sleep 1
95
+ end
96
+ rescue Interrupt
97
+ end
98
+
73
99
  # Show all information about this job
74
100
  def ps_show(id, *ids)
75
101
  ids = ([id] + ids).map { |s| Integer(s) }
76
102
 
77
- jobs = Simple::SQL.all <<~SQL, ids, into: Postjob::Job
103
+ jobs = Simple::SQL.records <<~SQL, ids, into: Postjob::Job
78
104
  SELECT * FROM postjob.postjobs WHERE id = ANY($1)
79
105
  SQL
80
106
 
@@ -84,6 +110,8 @@ module Postjob::CLI
84
110
  end
85
111
 
86
112
  def ps_full(*ids, limit: 100, tags: nil)
113
+ connect_to_database!
114
+
87
115
  conditions = []
88
116
  conditions << tags_condition(tags)
89
117
  conditions << ids_condition(ids)
@@ -110,8 +138,7 @@ module Postjob::CLI
110
138
  end
111
139
 
112
140
  def print_sql(limit:, query:)
113
- connect_to_database!
114
- records = Simple::SQL.all("#{query} LIMIT $1+1", limit, into: Hash)
141
+ records = Simple::SQL.records("#{query} LIMIT $1+1", limit)
115
142
 
116
143
  tp records[0, limit]
117
144
  if records.length > limit
@@ -20,8 +20,10 @@ module Postjob::CLI
20
20
 
21
21
  connect_to_database!
22
22
 
23
- processed = Postjob.run(count: count) do |job|
24
- logger.info "Processed job w/id #{job.id}" if job
23
+ logger.success "Starting runner with pid #{$$}"
24
+
25
+ processed = Postjob.run(count: count) do |job_id|
26
+ logger.info "Processed job w/id #{job_id}" if job_id
25
27
  STDERR.print "." unless quiet
26
28
  end
27
29
 
@@ -0,0 +1,18 @@
1
+ pg_types = <<~SQL
2
+ SELECT pg_namespace.nspname AS schema, pg_type.typname AS name
3
+ FROM pg_type
4
+ LEFT JOIN pg_namespace on pg_namespace.oid=pg_type.typnamespace
5
+ SQL
6
+
7
+ unless SQL.ask("SELECT 1 FROM (#{pg_types}) sq WHERE (schema,name) = ($1, $2)", SCHEMA_NAME, "statuses")
8
+ SQL.exec <<~SQL
9
+ CREATE TYPE #{SCHEMA_NAME}.statuses AS ENUM (
10
+ 'ready', -- process can run
11
+ 'sleep', -- process has external dependencies to wait for.
12
+ 'failed', -- process failed, with nonrecoverable error
13
+ 'err', -- process errored (with recoverable error)
14
+ 'timeout', -- process timed out
15
+ 'ok' -- process succeeded
16
+ );
17
+ SQL
18
+ end
@@ -0,0 +1,41 @@
1
+ CREATE TABLE IF NOT EXISTS {SCHEMA_NAME}.postjobs (
2
+ -- id values, readonly once created
3
+ id BIGSERIAL PRIMARY KEY, -- process id
4
+ parent_id BIGINT REFERENCES {SCHEMA_NAME}.postjobs ON DELETE CASCADE, -- parent process id
5
+ full_id VARCHAR, -- full process id
6
+ root_id BIGINT, -- root process id
7
+
8
+ created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- creation timestamp
9
+ updated_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- update timestamp
10
+
11
+ queue VARCHAR, -- queue name. (readonly)
12
+ workflow VARCHAR NOT NULL, -- e.g. "MyJobModule" (readonly)
13
+ workflow_method VARCHAR NOT NULL DEFAULT 'run', -- e.g. "run" (readonly)
14
+ workflow_version VARCHAR NOT NULL DEFAULT '', -- e.g. "1.0"
15
+ args JSONB, -- args
16
+
17
+ -- process state ----------------------------------------------------
18
+
19
+ status {SCHEMA_NAME}.statuses DEFAULT 'ready',
20
+ next_run_at timestamp DEFAULT (now() at time zone 'utc'), -- when possible to run next?
21
+ timing_out_at timestamp, -- job times out after this timestamp
22
+ failed_attempts INTEGER NOT NULL DEFAULT 0, -- failed how often?
23
+ max_attempts INTEGER NOT NULL DEFAULT 1, -- maximum attempts before failing
24
+
25
+ -- process result ---------------------------------------------------
26
+
27
+ results JSONB,
28
+ error VARCHAR,
29
+ error_message VARCHAR,
30
+ error_backtrace JSONB,
31
+
32
+ -- custom fields
33
+ workflow_status VARCHAR,
34
+ tags JSONB
35
+ );
36
+
37
+ -- [TODO] check indices
38
+ CREATE INDEX IF NOT EXISTS postjobs_tags_idx
39
+ ON {SCHEMA_NAME}.postjobs USING GIN (tags jsonb_path_ops);
40
+ CREATE INDEX IF NOT EXISTS postjobs_parent_id_idx
41
+ ON {SCHEMA_NAME}.postjobs(parent_id);
@@ -0,0 +1,3 @@
1
+ -- Apparently this line must be kept in a file of its own.
2
+ ALTER TYPE {SCHEMA_NAME}.statuses
3
+ ADD value IF NOT EXISTS 'processing' AFTER 'ready';
@@ -0,0 +1,31 @@
1
+ DO $$
2
+ BEGIN
3
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_client varchar;
4
+ EXCEPTION
5
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_client already exists';
6
+ END;
7
+ $$;
8
+
9
+ DO $$
10
+ BEGIN
11
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_client_identifier varchar;
12
+ EXCEPTION
13
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_client_identifier already exists';
14
+ END;
15
+ $$;
16
+
17
+ DO $$
18
+ BEGIN
19
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_started_at timestamp;
20
+ EXCEPTION
21
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_started_at already exists';
22
+ END;
23
+ $$;
24
+
25
+ DO $$
26
+ BEGIN
27
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_max_duration float;
28
+ EXCEPTION
29
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_max_duration already exists';
30
+ END;
31
+ $$;
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS {SCHEMA_NAME}.tokens (
2
+ id BIGSERIAL PRIMARY KEY,
3
+ postjob_id BIGINT REFERENCES {SCHEMA_NAME}.postjobs ON DELETE CASCADE,
4
+ token UUID NOT NULL,
5
+ created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc')
6
+ );
7
+
8
+ CREATE INDEX IF NOT EXISTS tokens_postjob_id_idx ON {SCHEMA_NAME}.tokens(postjob_id);
9
+ CREATE INDEX IF NOT EXISTS tokens_token_idx ON {SCHEMA_NAME}.tokens(token);
@@ -0,0 +1,28 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._wakeup_runners() RETURNS TRIGGER AS $$
2
+ BEGIN
3
+ NOTIFY {CHANNEL};
4
+ RETURN NEW;
5
+ END;
6
+ $$ LANGUAGE plpgsql;
7
+
8
+ BEGIN;
9
+ DROP TRIGGER IF EXISTS _wakeup_runners ON {TABLE_NAME};
10
+
11
+ CREATE TRIGGER _wakeup_runners AFTER INSERT OR UPDATE
12
+ ON {TABLE_NAME}
13
+ FOR EACH STATEMENT
14
+ EXECUTE PROCEDURE {SCHEMA_NAME}._wakeup_runners();
15
+ COMMIT;
16
+
17
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._wakeup_parent_job(job_id BIGINT) RETURNS VOID AS $$
18
+ BEGIN
19
+ UPDATE postjobs
20
+ SET
21
+ status='ready',
22
+ next_run_at=(now() at time zone 'utc'),
23
+ updated_at=(now() at time zone 'utc')
24
+ WHERE
25
+ status='sleep'
26
+ AND id=(SELECT parent_id FROM postjobs WHERE id=job_id);
27
+ END;
28
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,44 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.enqueue(
2
+ queue VARCHAR,
3
+ workflow VARCHAR,
4
+ workflow_method VARCHAR,
5
+ workflow_version VARCHAR,
6
+ args JSONB,
7
+ parent_id BIGINT,
8
+ tags JSONB,
9
+ max_attempts INTEGER,
10
+ timeout DOUBLE PRECISION)
11
+ RETURNS SETOF {SCHEMA_NAME}.postjobs
12
+ AS $$
13
+ DECLARE
14
+ job_id BIGINT;
15
+ BEGIN
16
+ -- set defaults ---------------------------------------------------
17
+ workflow_version := COALESCE(workflow_version, '');
18
+ queue := COALESCE(queue, 'q');
19
+ max_attempts := COALESCE(max_attempts, 5);
20
+
21
+ -- create postjobs entry ------------------------------------------
22
+ INSERT INTO {SCHEMA_NAME}.postjobs (
23
+ queue, workflow, workflow_method, workflow_version, args,
24
+ parent_id, tags, max_attempts,
25
+ timing_out_at
26
+ )
27
+ VALUES(
28
+ queue, workflow, workflow_method, workflow_version, args,
29
+ parent_id, tags, max_attempts,
30
+ (now() at time zone 'utc') + timeout * interval '1 second'
31
+ ) RETURNING {SCHEMA_NAME}.postjobs.id INTO job_id;
32
+
33
+ -- fill in root_id and full_id ------------------------------------
34
+ UPDATE postjobs
35
+ SET
36
+ root_id=COALESCE((SELECT root_id FROM postjobs s WHERE s.id=postjobs.parent_id), id),
37
+ full_id=COALESCE((SELECT full_id FROM postjobs s WHERE s.id=postjobs.parent_id) || '.' || id, id::varchar)
38
+ WHERE id=job_id;
39
+
40
+ -- return the job -------------------------------------------------
41
+ RETURN QUERY
42
+ SELECT * FROM postjobs WHERE id=job_id;
43
+ END;
44
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,48 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_client_identifier(client_identifier varchar) RETURNS VOID AS $$
2
+ BEGIN
3
+ PERFORM set_config('{SCHEMA_NAME}.client_identifier', client_identifier, false);
4
+ END;
5
+ $$ LANGUAGE plpgsql;
6
+
7
+ --SELECT {SCHEMA_NAME}.set_client_identifier('nope');
8
+
9
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._client_identifier() RETURNS varchar AS $$
10
+ BEGIN
11
+ RETURN current_setting('{SCHEMA_NAME}.client_identifier');
12
+ EXCEPTION
13
+ WHEN OTHERS THEN RETURN NULL;
14
+ END;
15
+ $$ LANGUAGE plpgsql;
16
+
17
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_processing(job_id BIGINT) RETURNS VOID AS $$
18
+ DECLARE
19
+ v_pid int;
20
+ BEGIN
21
+ v_pid := pg_backend_pid();
22
+
23
+ UPDATE postjobs
24
+ SET
25
+ status='processing',
26
+ processing_client=(SELECT client_addr || ':' || client_port FROM pg_stat_activity WHERE pid = v_pid),
27
+ processing_client_identifier={SCHEMA_NAME}._client_identifier(),
28
+ processing_started_at=now() at time zone 'utc',
29
+ processing_max_duration=30 * 60, -- default max duration of processing: 30 minutes.
30
+ error=NULL,
31
+ error_message=NULL,
32
+ error_backtrace=NULL,
33
+ next_run_at=NULL
34
+ WHERE id=job_id;
35
+ END;
36
+ $$ LANGUAGE plpgsql;
37
+
38
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._reset_job_processing(job_id BIGINT) RETURNS VOID AS $$
39
+ BEGIN
40
+ UPDATE postjobs
41
+ SET
42
+ processing_client=NULL,
43
+ processing_client_identifier=NULL,
44
+ processing_started_at=NULL,
45
+ processing_max_duration=NULL
46
+ WHERE id=job_id;
47
+ END;
48
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,157 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._update_job_version(job_id BIGINT, p_version VARCHAR) RETURNS VOID AS $$
2
+ BEGIN
3
+ IF p_version IS NOT NULL THEN
4
+ UPDATE postjobs
5
+ SET
6
+ workflow_version=p_version,
7
+ updated_at=(now() at time zone 'utc')
8
+ WHERE id=job_id;
9
+ END IF;
10
+ END;
11
+ $$ LANGUAGE plpgsql;
12
+
13
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_result(job_id BIGINT, p_results JSONB, p_version VARCHAR) RETURNS VOID AS $$
14
+ BEGIN
15
+ PERFORM _update_job_version(job_id, p_version);
16
+ PERFORM _reset_job_processing(job_id);
17
+
18
+ UPDATE postjobs
19
+ SET
20
+ results=p_results,
21
+ error=NULL,
22
+ error_message=NULL,
23
+ error_backtrace=NULL,
24
+ status='ok',
25
+ next_run_at=NULL
26
+ WHERE id=job_id;
27
+
28
+ PERFORM _wakeup_parent_job(job_id);
29
+ END;
30
+ $$ LANGUAGE plpgsql;
31
+
32
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_pending(job_id BIGINT, p_version VARCHAR) RETURNS VOID AS $$
33
+ BEGIN
34
+ PERFORM _update_job_version(job_id, p_version);
35
+ PERFORM _reset_job_processing(job_id);
36
+
37
+ UPDATE postjobs
38
+ SET status='sleep', next_run_at=NULL
39
+ WHERE id=job_id;
40
+ END;
41
+ $$ LANGUAGE plpgsql;
42
+
43
+ -- If this is a recoverable error and if we have another run possible
44
+ -- we'll set next_run_at, and the status to "err", otherwise
45
+ -- next_run_at will be NULL and the status would be "failed" or "timeout"
46
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._prepare_next_run(
47
+ job_id BIGINT,
48
+ p_status {SCHEMA_NAME}.statuses,
49
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
50
+ DECLARE
51
+ next_run_at_basetime DOUBLE PRECISION;
52
+ BEGIN
53
+ IF p_status NOT IN ('err', 'timeout', 'failed') THEN
54
+ RAISE 'Invalid status value %', p_status;
55
+ END IF;
56
+
57
+ -- If this is a recoverable error we check if we have any remaining attempts.
58
+ IF p_status = 'err' THEN
59
+ IF (SELECT max_attempts - failed_attempts FROM postjobs WHERE id=job_id) > 0 THEN
60
+ p_status = 'err';
61
+ ELSE
62
+ p_status = 'failed';
63
+ END IF;
64
+ END IF;
65
+
66
+ -- set status, clear next_run_at
67
+ UPDATE postjobs
68
+ SET
69
+ status=p_status,
70
+ next_run_at=NULL
71
+ WHERE id=job_id;
72
+
73
+ -- fill in next_run_at
74
+ IF p_status != 'failed' AND p_status != 'timeout' THEN
75
+ next_run_at_basetime := CASE WHEN p_fast_mode THEN 0.01 ELSE 10 END;
76
+
77
+ UPDATE postjobs
78
+ SET
79
+ next_run_at=(now() at time zone 'utc') + next_run_at_basetime * pow(1.5, failed_attempts) * interval '1 second'
80
+ WHERE id=job_id;
81
+ END IF;
82
+ END;
83
+ $$ LANGUAGE plpgsql;
84
+
85
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_error(
86
+ job_id BIGINT,
87
+ p_error VARCHAR,
88
+ p_error_message VARCHAR,
89
+ p_error_backtrace JSONB,
90
+ p_status {SCHEMA_NAME}.statuses,
91
+ p_version VARCHAR,
92
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
93
+ BEGIN
94
+ PERFORM _update_job_version(job_id, p_version);
95
+ PERFORM _reset_job_processing(job_id);
96
+
97
+ -- write error info
98
+ UPDATE postjobs
99
+ SET
100
+ error=p_error,
101
+ error_message=p_error_message,
102
+ error_backtrace=p_error_backtrace,
103
+ failed_attempts=failed_attempts+1,
104
+ next_run_at=NULL
105
+ WHERE id=job_id;
106
+
107
+ -- prepare next run, if any
108
+ PERFORM _prepare_next_run(job_id, p_status, p_fast_mode);
109
+ PERFORM _wakeup_parent_job(job_id);
110
+ END;
111
+ $$ LANGUAGE plpgsql;
112
+
113
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_timeout(
114
+ job_id BIGINT,
115
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
116
+ BEGIN
117
+ PERFORM _reset_job_processing(job_id);
118
+
119
+ -- write error info
120
+ UPDATE postjobs
121
+ SET
122
+ error='Timeout',
123
+ error_message='timeout',
124
+ error_backtrace=NULL,
125
+ failed_attempts=failed_attempts+1,
126
+ next_run_at=NULL
127
+ WHERE id=job_id;
128
+
129
+ -- prepare next run, if any
130
+ PERFORM _prepare_next_run(job_id, 'timeout', p_fast_mode);
131
+ PERFORM _wakeup_parent_job(job_id);
132
+ END;
133
+ $$ LANGUAGE plpgsql;
134
+
135
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_zombie(
136
+ job_id BIGINT,
137
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
138
+ BEGIN
139
+ PERFORM _reset_job_processing(job_id);
140
+
141
+ RAISE NOTICE 'job % is a zombie', job_id;
142
+
143
+ -- write error info
144
+ UPDATE postjobs
145
+ SET
146
+ error='Zombie',
147
+ error_message='zombie',
148
+ error_backtrace=NULL,
149
+ failed_attempts=failed_attempts+1,
150
+ next_run_at=NULL
151
+ WHERE id=job_id;
152
+
153
+ -- prepare next run, if any
154
+ PERFORM _prepare_next_run(job_id, 'err', p_fast_mode);
155
+ PERFORM _wakeup_parent_job(job_id);
156
+ END;
157
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,78 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.time_to_next_job(workflows_with_versions varchar[])
2
+ RETURNS float
3
+ AS $$
4
+ DECLARE
5
+ p_processable_at timestamp;
6
+ BEGIN
7
+ SELECT MIN(processable_at) INTO p_processable_at FROM (
8
+ SELECT MIN(processing_started_at + processing_max_duration * interval '1 second') AS processable_at
9
+ FROM {TABLE_NAME}
10
+ WHERE status IN ('processing')
11
+ UNION
12
+ SELECT MIN(timing_out_at) AS processable_at
13
+ FROM {TABLE_NAME}
14
+ WHERE status IN ('ready', 'err', 'sleep')
15
+ UNION
16
+ SELECT MIN(next_run_at) AS processable_at
17
+ FROM {TABLE_NAME}
18
+ WHERE status IN ('ready', 'err')
19
+ AND workflow || workflow_version = ANY ($1)
20
+ ) sq;
21
+
22
+ RETURN EXTRACT(EPOCH FROM p_processable_at - (now() at time zone 'utc'));
23
+ END;
24
+ $$ LANGUAGE plpgsql;
25
+
26
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.checkout(workflows_with_versions varchar[], p_fast_mode BOOLEAN)
27
+ RETURNS SETOF {TABLE_NAME}
28
+ AS $$
29
+ DECLARE
30
+ job {TABLE_NAME};
31
+ BEGIN
32
+ LOOP
33
+ -- try to checkout a job. Each of the conditions here is matching
34
+ -- one of the CASE .. WHEN clauses below.
35
+ SELECT INTO job *
36
+ FROM {TABLE_NAME} s
37
+ WHERE
38
+ (
39
+ s.status IN ('processing')
40
+ AND (s.processing_started_at + s.processing_max_duration * interval '1 second') <= (now() at time zone 'utc')
41
+ )
42
+ OR
43
+ (
44
+ s.status IN ('ready', 'err', 'sleep')
45
+ AND s.timing_out_at <= (now() at time zone 'utc')
46
+ )
47
+ OR
48
+ (
49
+ s.status IN ('ready', 'err')
50
+ AND s.next_run_at <= (now() at time zone 'utc')
51
+ AND s.workflow || s.workflow_version = ANY ($1)
52
+ )
53
+ ORDER BY (LEAST(s.next_run_at, s.timing_out_at, (s.processing_started_at + s.processing_max_duration * interval '1 second')))
54
+ FOR UPDATE SKIP LOCKED
55
+ LIMIT 1;
56
+
57
+ CASE
58
+ WHEN job.id IS NULL THEN
59
+ -- couldn't find a job?
60
+ EXIT;
61
+ WHEN job.status='processing' AND
62
+ (job.processing_started_at + job.processing_max_duration * interval '1 second') <= (now() at time zone 'utc') THEN
63
+ -- a zombie: the worker probably died.
64
+ PERFORM {SCHEMA_NAME}._set_job_zombie(job.id, p_fast_mode);
65
+ CONTINUE;
66
+ WHEN job.status IN ('ready', 'err', 'sleep') AND job.timing_out_at <= (now() at time zone 'utc') THEN
67
+ -- job timed out? mark it as such, and try next one.
68
+ PERFORM {SCHEMA_NAME}._set_job_timeout(job.id, p_fast_mode);
69
+ CONTINUE;
70
+ ELSE
71
+ -- set job to processing
72
+ PERFORM _set_job_processing(job.id);
73
+ RETURN QUERY SELECT * FROM {TABLE_NAME} WHERE id=job.id;
74
+ EXIT;
75
+ END CASE;
76
+ END LOOP;
77
+ END;
78
+ $$ LANGUAGE plpgsql;