inst-jobs 3.0.2 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55850c386085b90bbb9df7ff954d40ed7f2035268860ec8b47dfc11f11ec7e51
4
- data.tar.gz: 53065a1f09e8cd30271bc9b57ab58d3bcfff8cf1b9590568379427093de9cbe9
3
+ metadata.gz: 35f27ce4a29cf01568ecd625274894da1c2675e7d2e7da7d307af2200520140b
4
+ data.tar.gz: 75f5714d9269f601af9097c739fd6cb1948ef7899509c63ad3de2c7991800310
5
5
  SHA512:
6
- metadata.gz: 8f7d579cddb80890c6158a1384f6828ae91272a864308528ad84e6dc47b3e4769062f91de7ebf9641f4b766c21c571a419385ee57bec9b6e5531f39be30a50a8
7
- data.tar.gz: 887648362dec636b3e1f6938e66dd25b97f42a3e606c5d8466768d5b2ed5137ea50b67c68ce9ec817df86f117ef0660f6ddd3456c86e4871965f89704cc161f3
6
+ metadata.gz: 7eb2a9498036a106c8a02a7878094e940b7450bd60947c34bbb9246cc31f25950d16bb8b9c7eba6e7aa6d25bce2b1bbe11d603c9ca573bd87282225d0c36686f
7
+ data.tar.gz: 0fe9a7d4226558e665b8a961dd1fd88b5c7e9d076ed6927925a7a259ddce279077360da42746bfd1f064d1e8ad0f602c43b53fb92064996854e76980035f7cc0
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateAfterDeleteTriggerForSingletonTransitionCases < ActiveRecord::Migration[6.0]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
7
+ DECLARE
8
+ next_strand varchar;
9
+ running_count integer;
10
+ should_lock boolean;
11
+ should_be_precise boolean;
12
+ update_query varchar;
13
+ skip_locked varchar;
14
+ transition boolean;
15
+ BEGIN
16
+ IF OLD.strand IS NOT NULL THEN
17
+ should_lock := true;
18
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
19
+
20
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
21
+ running_count := (SELECT COUNT(*) FROM (
22
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
23
+ ) subquery_for_count);
24
+ should_lock := running_count < OLD.max_concurrent;
25
+ END IF;
26
+
27
+ IF should_lock THEN
28
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
29
+ END IF;
30
+
31
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
32
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
33
+ -- singleton we grab isn't already running (which is a simple existence check, since
34
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
35
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
36
+ SELECT id FROM delayed_jobs j2
37
+ WHERE next_in_strand=false AND
38
+ j2.strand=$1.strand AND
39
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
40
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
41
+ LIMIT ';
42
+
43
+ IF should_be_precise THEN
44
+ running_count := (SELECT COUNT(*) FROM (
45
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
46
+ ) s);
47
+ IF running_count < OLD.max_concurrent THEN
48
+ update_query := update_query || '($1.max_concurrent - $2)';
49
+ ELSE
50
+ -- we have too many running already; just bail
51
+ RETURN OLD;
52
+ END IF;
53
+ ELSE
54
+ update_query := update_query || '1';
55
+
56
+ -- n-strands don't require precise ordering; we can make this query more performant
57
+ IF OLD.max_concurrent > 1 THEN
58
+ skip_locked := ' SKIP LOCKED';
59
+ END IF;
60
+ END IF;
61
+
62
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
63
+ EXECUTE update_query USING OLD, running_count;
64
+ END IF;
65
+
66
+ IF OLD.singleton IS NOT NULL THEN
67
+ transition := EXISTS (SELECT 1 FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL);
68
+
69
+ IF transition THEN
70
+ next_strand := (SELECT j1.strand FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL AND j1.strand IS NOT NULL LIMIT 1);
71
+
72
+ IF next_strand IS NOT NULL THEN
73
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
74
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
75
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
76
+ RETURN OLD;
77
+ END IF;
78
+ END IF;
79
+ ELSIF OLD.strand IS NOT NULL THEN
80
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
81
+ RETURN OLD;
82
+ END IF;
83
+
84
+ -- handles transitioning a singleton from stranded to not stranded --
85
+ -- handles transitioning a singleton from unstranded to stranded --
86
+ -- handles transitioning a singleton from strand A to strand B --
87
+ -- these transitions are a relatively rare case, so we take a shortcut and --
88
+ -- only start the next singleton if its strand does not currently have any running jobs --
89
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
90
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
91
+ SELECT id FROM delayed_jobs j2
92
+ WHERE next_in_strand=false AND
93
+ j2.singleton=OLD.singleton AND
94
+ j2.locked_by IS NULL AND
95
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
96
+ FOR UPDATE
97
+ );
98
+ END IF;
99
+ RETURN OLD;
100
+ END;
101
+ $$ LANGUAGE plpgsql;
102
+ SQL
103
+ end
104
+
105
+ def down
106
+ execute(<<~SQL)
107
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
108
+ DECLARE
109
+ running_count integer;
110
+ should_lock boolean;
111
+ should_be_precise boolean;
112
+ update_query varchar;
113
+ skip_locked varchar;
114
+ BEGIN
115
+ IF OLD.strand IS NOT NULL THEN
116
+ should_lock := true;
117
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
118
+
119
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
120
+ running_count := (SELECT COUNT(*) FROM (
121
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
122
+ ) subquery_for_count);
123
+ should_lock := running_count < OLD.max_concurrent;
124
+ END IF;
125
+
126
+ IF should_lock THEN
127
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
128
+ END IF;
129
+
130
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
131
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
132
+ -- singleton we grab isn't already running (which is a simple existence check, since
133
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
134
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
135
+ SELECT id FROM delayed_jobs j2
136
+ WHERE next_in_strand=false AND
137
+ j2.strand=$1.strand AND
138
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
139
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
140
+ LIMIT ';
141
+
142
+ IF should_be_precise THEN
143
+ running_count := (SELECT COUNT(*) FROM (
144
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
145
+ ) s);
146
+ IF running_count < OLD.max_concurrent THEN
147
+ update_query := update_query || '($1.max_concurrent - $2)';
148
+ ELSE
149
+ -- we have too many running already; just bail
150
+ RETURN OLD;
151
+ END IF;
152
+ ELSE
153
+ update_query := update_query || '1';
154
+
155
+ -- n-strands don't require precise ordering; we can make this query more performant
156
+ IF OLD.max_concurrent > 1 THEN
157
+ skip_locked := ' SKIP LOCKED';
158
+ END IF;
159
+ END IF;
160
+
161
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
162
+ EXECUTE update_query USING OLD, running_count;
163
+ ELSIF OLD.singleton IS NOT NULL THEN
164
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false AND locked_by IS NULL;
165
+ END IF;
166
+ RETURN OLD;
167
+ END;
168
+ $$ LANGUAGE plpgsql;
169
+ SQL
170
+ end
171
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonRaceConditionInsert < ActiveRecord::Migration[5.2]
4
+ def change
5
+ reversible do |direction|
6
+ direction.up do
7
+ execute(<<~SQL)
8
+ CREATE OR REPLACE FUNCTION delayed_jobs_before_insert_row_tr_fn () RETURNS trigger AS $$
9
+ BEGIN
10
+ IF NEW.strand IS NOT NULL THEN
11
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
12
+ IF (SELECT COUNT(*) FROM (
13
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
14
+ ) s) = NEW.max_concurrent THEN
15
+ NEW.next_in_strand := false;
16
+ END IF;
17
+ END IF;
18
+ IF NEW.singleton IS NOT NULL THEN
19
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', NEW.singleton)));
20
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
21
+ -- rather than doing a seq scan
22
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
23
+ IF FOUND THEN
24
+ NEW.next_in_strand := false;
25
+ END IF;
26
+ END IF;
27
+ RETURN NEW;
28
+ END;
29
+ $$ LANGUAGE plpgsql;
30
+ SQL
31
+ end
32
+ direction.down do
33
+ execute(<<~SQL)
34
+ CREATE OR REPLACE FUNCTION delayed_jobs_before_insert_row_tr_fn () RETURNS trigger AS $$
35
+ BEGIN
36
+ IF NEW.strand IS NOT NULL THEN
37
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
38
+ IF (SELECT COUNT(*) FROM (
39
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
40
+ ) s) = NEW.max_concurrent THEN
41
+ NEW.next_in_strand := false;
42
+ END IF;
43
+ END IF;
44
+ IF NEW.singleton IS NOT NULL THEN
45
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
46
+ -- rather than doing a seq scan
47
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
48
+ IF FOUND THEN
49
+ NEW.next_in_strand := false;
50
+ END IF;
51
+ END IF;
52
+ RETURN NEW;
53
+ END;
54
+ $$ LANGUAGE plpgsql;
55
+ SQL
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonRaceConditionDelete < ActiveRecord::Migration[6.0]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
7
+ DECLARE
8
+ next_strand varchar;
9
+ running_count integer;
10
+ should_lock boolean;
11
+ should_be_precise boolean;
12
+ update_query varchar;
13
+ skip_locked varchar;
14
+ transition boolean;
15
+ BEGIN
16
+ IF OLD.strand IS NOT NULL THEN
17
+ should_lock := true;
18
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
19
+
20
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
21
+ running_count := (SELECT COUNT(*) FROM (
22
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
23
+ ) subquery_for_count);
24
+ should_lock := running_count < OLD.max_concurrent;
25
+ END IF;
26
+
27
+ IF should_lock THEN
28
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
29
+ END IF;
30
+
31
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
32
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
33
+ -- singleton we grab isn't already running (which is a simple existence check, since
34
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
35
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
36
+ SELECT id FROM delayed_jobs j2
37
+ WHERE next_in_strand=false AND
38
+ j2.strand=$1.strand AND
39
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
40
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
41
+ LIMIT ';
42
+
43
+ IF should_be_precise THEN
44
+ running_count := (SELECT COUNT(*) FROM (
45
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
46
+ ) s);
47
+ IF running_count < OLD.max_concurrent THEN
48
+ update_query := update_query || '($1.max_concurrent - $2)';
49
+ ELSE
50
+ -- we have too many running already; just bail
51
+ RETURN OLD;
52
+ END IF;
53
+ ELSE
54
+ update_query := update_query || '1';
55
+
56
+ -- n-strands don't require precise ordering; we can make this query more performant
57
+ IF OLD.max_concurrent > 1 THEN
58
+ skip_locked := ' SKIP LOCKED';
59
+ END IF;
60
+ END IF;
61
+
62
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
63
+ EXECUTE update_query USING OLD, running_count;
64
+ END IF;
65
+
66
+ IF OLD.singleton IS NOT NULL THEN
67
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', OLD.singleton)));
68
+
69
+ transition := EXISTS (SELECT 1 FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL);
70
+
71
+ IF transition THEN
72
+ next_strand := (SELECT j1.strand FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL AND j1.strand IS NOT NULL LIMIT 1);
73
+
74
+ IF next_strand IS NOT NULL THEN
75
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
76
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
77
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
78
+ RETURN OLD;
79
+ END IF;
80
+ END IF;
81
+ ELSIF OLD.strand IS NOT NULL THEN
82
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
83
+ RETURN OLD;
84
+ END IF;
85
+
86
+ -- handles transitioning a singleton from stranded to not stranded --
87
+ -- handles transitioning a singleton from unstranded to stranded --
88
+ -- handles transitioning a singleton from strand A to strand B --
89
+ -- these transitions are a relatively rare case, so we take a shortcut and --
90
+ -- only start the next singleton if its strand does not currently have any running jobs --
91
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
92
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
93
+ SELECT id FROM delayed_jobs j2
94
+ WHERE next_in_strand=false AND
95
+ j2.singleton=OLD.singleton AND
96
+ j2.locked_by IS NULL AND
97
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
98
+ FOR UPDATE
99
+ );
100
+ END IF;
101
+ RETURN OLD;
102
+ END;
103
+ $$ LANGUAGE plpgsql;
104
+ SQL
105
+ end
106
+
107
+ def down
108
+ execute(<<~SQL)
109
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
110
+ DECLARE
111
+ next_strand varchar;
112
+ running_count integer;
113
+ should_lock boolean;
114
+ should_be_precise boolean;
115
+ update_query varchar;
116
+ skip_locked varchar;
117
+ transition boolean;
118
+ BEGIN
119
+ IF OLD.strand IS NOT NULL THEN
120
+ should_lock := true;
121
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
122
+
123
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
124
+ running_count := (SELECT COUNT(*) FROM (
125
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
126
+ ) subquery_for_count);
127
+ should_lock := running_count < OLD.max_concurrent;
128
+ END IF;
129
+
130
+ IF should_lock THEN
131
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
132
+ END IF;
133
+
134
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
135
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
136
+ -- singleton we grab isn't already running (which is a simple existence check, since
137
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
138
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
139
+ SELECT id FROM delayed_jobs j2
140
+ WHERE next_in_strand=false AND
141
+ j2.strand=$1.strand AND
142
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
143
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
144
+ LIMIT ';
145
+
146
+ IF should_be_precise THEN
147
+ running_count := (SELECT COUNT(*) FROM (
148
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
149
+ ) s);
150
+ IF running_count < OLD.max_concurrent THEN
151
+ update_query := update_query || '($1.max_concurrent - $2)';
152
+ ELSE
153
+ -- we have too many running already; just bail
154
+ RETURN OLD;
155
+ END IF;
156
+ ELSE
157
+ update_query := update_query || '1';
158
+
159
+ -- n-strands don't require precise ordering; we can make this query more performant
160
+ IF OLD.max_concurrent > 1 THEN
161
+ skip_locked := ' SKIP LOCKED';
162
+ END IF;
163
+ END IF;
164
+
165
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
166
+ EXECUTE update_query USING OLD, running_count;
167
+ END IF;
168
+
169
+ IF OLD.singleton IS NOT NULL THEN
170
+ transition := EXISTS (SELECT 1 FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL);
171
+
172
+ IF transition THEN
173
+ next_strand := (SELECT j1.strand FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL AND j1.strand IS NOT NULL LIMIT 1);
174
+
175
+ IF next_strand IS NOT NULL THEN
176
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
177
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
178
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
179
+ RETURN OLD;
180
+ END IF;
181
+ END IF;
182
+ ELSIF OLD.strand IS NOT NULL THEN
183
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
184
+ RETURN OLD;
185
+ END IF;
186
+
187
+ -- handles transitioning a singleton from stranded to not stranded --
188
+ -- handles transitioning a singleton from unstranded to stranded --
189
+ -- handles transitioning a singleton from strand A to strand B --
190
+ -- these transitions are a relatively rare case, so we take a shortcut and --
191
+ -- only start the next singleton if its strand does not currently have any running jobs --
192
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
193
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
194
+ SELECT id FROM delayed_jobs j2
195
+ WHERE next_in_strand=false AND
196
+ j2.singleton=OLD.singleton AND
197
+ j2.locked_by IS NULL AND
198
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
199
+ FOR UPDATE
200
+ );
201
+ END IF;
202
+ RETURN OLD;
203
+ END;
204
+ $$ LANGUAGE plpgsql;
205
+ SQL
206
+ end
207
+ end
data/exe/inst_jobs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "config/environment"
4
+ require File.expand_path("config/environment")
5
5
  Delayed::CLI.new.run
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delayed/plugin"
4
+
5
+ module Delayed
6
+ class RailsReloaderPlugin < Plugin
7
+ callbacks do |lifecycle|
8
+ app = Rails.application
9
+ if app && !app.config.cache_classes
10
+ lifecycle.around(:perform) do |worker, job, &block|
11
+ reload = !app.config.reload_classes_only_on_change || app.reloaders.any?(&:updated?)
12
+
13
+ if reload
14
+ if defined?(ActiveSupport::Reloader)
15
+ Rails.application.reloader.reload!
16
+ else
17
+ ActionDispatch::Reloader.prepare!
18
+ end
19
+ end
20
+
21
+ begin
22
+ block.call(worker, job)
23
+ ensure
24
+ ActionDispatch::Reloader.cleanup! if reload && !defined?(ActiveSupport::Reloader)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "3.0.2"
4
+ VERSION = "3.0.7"
5
5
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delayed/rails_reloader_plugin"
4
+
3
5
  module Delayed
4
6
  class TimeoutError < RuntimeError; end
5
7
 
@@ -70,27 +72,7 @@ module Delayed
70
72
 
71
73
  @signal_queue = []
72
74
 
73
- app = Rails.application
74
- if app && !app.config.cache_classes
75
- Delayed::Worker.lifecycle.around(:perform) do |worker, job, &block|
76
- reload = app.config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
77
-
78
- if reload
79
- if defined?(ActiveSupport::Reloader)
80
- Rails.application.reloader.reload!
81
- else
82
- ActionDispatch::Reloader.prepare!
83
- end
84
- end
85
-
86
- begin
87
- block.call(worker, job)
88
- ensure
89
- ActionDispatch::Reloader.cleanup! if reload && !defined?(ActiveSupport::Reloader)
90
- end
91
- end
92
- end
93
-
75
+ plugins << Delayed::RailsReloaderPlugin
94
76
  plugins.each(&:inject!)
95
77
  end
96
78
 
@@ -44,7 +44,7 @@ describe Delayed::Worker do
44
44
  expect(output_count).to eq(1)
45
45
  end
46
46
 
47
- it "reloads" do
47
+ it "reloads Rails classes (never more than once)" do
48
48
  fake_application = double("Rails.application",
49
49
  config: double("Rails.application.config",
50
50
  cache_classes: false,
@@ -59,6 +59,11 @@ describe Delayed::Worker do
59
59
  expect(ActionDispatch::Reloader).to receive(:cleanup!).once
60
60
  end
61
61
  job = double(job_attrs)
62
+
63
+ # Create extra workers to make sure we don't reload multiple times
64
+ described_class.new(worker_config.dup)
65
+ described_class.new(worker_config.dup)
66
+
62
67
  subject.perform(job)
63
68
  end
64
69
  end
@@ -389,7 +389,222 @@ shared_examples_for "a backend" do
389
389
  expect(job1.reload.handler).to include("ErrorJob")
390
390
  end
391
391
 
392
+ context "next_in_strand management - deadlocks and race conditions", non_transactional: true do
393
+ # The following unit tests are fairly slow and non-deterministic. It may be
394
+ # easier to make them fail quicker and more consistently by adding a random
395
+ # sleep into the appropriate trigger(s).
396
+
397
+ def loop_secs(val)
398
+ loop_start = Time.now.utc
399
+
400
+ loop do
401
+ break if Time.now.utc >= loop_start + val
402
+
403
+ yield
404
+ end
405
+ end
406
+
407
+ def loop_until_found(params)
408
+ found = false
409
+
410
+ loop_secs(10.seconds) do
411
+ if Delayed::Job.exists?(**params)
412
+ found = true
413
+ break
414
+ end
415
+ end
416
+
417
+ raise "timed out waiting for condition" unless found
418
+ end
419
+
420
+ def thread_body
421
+ yield
422
+ rescue
423
+ Thread.current.thread_variable_set(:fail, true)
424
+ raise
425
+ end
426
+
427
+ it "doesn't orphan the singleton when two are queued consecutively" do
428
+ # In order to reproduce this one efficiently, you'll probably want to add
429
+ # a sleep within delayed_jobs_before_insert_row_tr_fn.
430
+ # IF NEW.singleton IS NOT NULL THEN
431
+ # ...
432
+ # PERFORM pg_sleep(random() * 2);
433
+ # END IF;
434
+
435
+ threads = []
436
+
437
+ threads << Thread.new do
438
+ thread_body do
439
+ loop do
440
+ create_job(singleton: "singleton_job")
441
+ create_job(singleton: "singleton_job")
442
+ end
443
+ end
444
+ end
445
+
446
+ threads << Thread.new do
447
+ thread_body do
448
+ loop do
449
+ Delayed::Job.get_and_lock_next_available("w1")&.destroy
450
+ end
451
+ end
452
+ end
453
+
454
+ threads << Thread.new do
455
+ thread_body do
456
+ loop do
457
+ loop_until_found(singleton: "singleton_job", next_in_strand: true)
458
+ end
459
+ end
460
+ end
461
+
462
+ begin
463
+ loop_secs(60.seconds) do
464
+ if threads.any? { |x| x.thread_variable_get(:fail) }
465
+ raise "at least one job became orphaned or other error"
466
+ end
467
+ end
468
+ ensure
469
+ threads.each(&:kill)
470
+ threads.each(&:join)
471
+ end
472
+ end
473
+
474
+ it "doesn't deadlock when transitioning from strand_a to strand_b" do
475
+ # In order to reproduce this one efficiently, you'll probably want to add
476
+ # a sleep within delayed_jobs_after_delete_row_tr_fn.
477
+ # PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
478
+ # PERFORM pg_sleep(random() * 2);
479
+
480
+ threads = []
481
+
482
+ threads << Thread.new do
483
+ thread_body do
484
+ loop do
485
+ j1 = create_job(singleton: "myjobs", strand: "myjobs2", locked_by: "w1")
486
+ j2 = create_job(singleton: "myjobs", strand: "myjobs")
487
+
488
+ j1.delete
489
+ j2.delete
490
+ end
491
+ end
492
+ end
493
+
494
+ threads << Thread.new do
495
+ thread_body do
496
+ loop do
497
+ j1 = create_job(singleton: "myjobs2", strand: "myjobs", locked_by: "w1")
498
+ j2 = create_job(singleton: "myjobs2", strand: "myjobs2")
499
+
500
+ j1.delete
501
+ j2.delete
502
+ end
503
+ end
504
+ end
505
+
506
+ threads << Thread.new do
507
+ thread_body do
508
+ loop do
509
+ loop_until_found(singleton: "myjobs", next_in_strand: true)
510
+ end
511
+ end
512
+ end
513
+
514
+ threads << Thread.new do
515
+ thread_body do
516
+ loop do
517
+ loop_until_found(singleton: "myjobs2", next_in_strand: true)
518
+ end
519
+ end
520
+ end
521
+
522
+ begin
523
+ loop_secs(60.seconds) do
524
+ if threads.any? { |x| x.thread_variable_get(:fail) }
525
+ raise "at least one thread hit a deadlock or other error"
526
+ end
527
+ end
528
+ ensure
529
+ threads.each(&:kill)
530
+ threads.each(&:join)
531
+ end
532
+ end
533
+ end
534
+
392
535
  context "next_in_strand management" do
536
+ it "handles transitions correctly when going from stranded to not stranded" do
537
+ @job1 = create_job(singleton: "myjobs", strand: "myjobs")
538
+ Delayed::Job.get_and_lock_next_available("w1")
539
+ @job2 = create_job(singleton: "myjobs")
540
+
541
+ expect(@job1.reload.next_in_strand).to eq true
542
+ expect(@job2.reload.next_in_strand).to eq false
543
+
544
+ @job1.destroy
545
+ expect(@job2.reload.next_in_strand).to eq true
546
+ end
547
+
548
+ it "handles transitions correctly when going from not stranded to stranded" do
549
+ @job1 = create_job(singleton: "myjobs2", strand: "myjobs")
550
+ @job2 = create_job(singleton: "myjobs")
551
+ Delayed::Job.get_and_lock_next_available("w1")
552
+ Delayed::Job.get_and_lock_next_available("w1")
553
+ @job3 = create_job(singleton: "myjobs", strand: "myjobs2")
554
+
555
+ expect(@job1.reload.next_in_strand).to eq true
556
+ expect(@job2.reload.next_in_strand).to eq true
557
+ expect(@job3.reload.next_in_strand).to eq false
558
+
559
+ @job2.destroy
560
+ expect(@job1.reload.next_in_strand).to eq true
561
+ expect(@job3.reload.next_in_strand).to eq true
562
+ end
563
+
564
+ it "does not violate n_strand=1 constraints when going from not stranded to stranded" do
565
+ @job1 = create_job(singleton: "myjobs2", strand: "myjobs")
566
+ @job2 = create_job(singleton: "myjobs")
567
+ Delayed::Job.get_and_lock_next_available("w1")
568
+ Delayed::Job.get_and_lock_next_available("w1")
569
+ @job3 = create_job(singleton: "myjobs", strand: "myjobs")
570
+
571
+ expect(@job1.reload.next_in_strand).to eq true
572
+ expect(@job2.reload.next_in_strand).to eq true
573
+ expect(@job3.reload.next_in_strand).to eq false
574
+
575
+ @job2.destroy
576
+ expect(@job1.reload.next_in_strand).to eq true
577
+ expect(@job3.reload.next_in_strand).to eq false
578
+ end
579
+
580
+ it "handles transitions correctly when going from stranded to another strand" do
581
+ @job1 = create_job(singleton: "myjobs", strand: "myjobs")
582
+ Delayed::Job.get_and_lock_next_available("w1")
583
+ @job2 = create_job(singleton: "myjobs", strand: "myjobs2")
584
+
585
+ expect(@job1.reload.next_in_strand).to eq true
586
+ expect(@job2.reload.next_in_strand).to eq false
587
+
588
+ @job1.destroy
589
+ expect(@job2.reload.next_in_strand).to eq true
590
+ end
591
+
592
+ it "does not violate n_strand=1 constraints when going from stranded to another strand" do
593
+ @job1 = create_job(singleton: "myjobs2", strand: "myjobs2")
594
+ @job2 = create_job(singleton: "myjobs", strand: "myjobs")
595
+ Delayed::Job.get_and_lock_next_available("w1")
596
+ Delayed::Job.get_and_lock_next_available("w1")
597
+ @job3 = create_job(singleton: "myjobs", strand: "myjobs2")
598
+
599
+ expect(@job1.reload.next_in_strand).to eq true
600
+ expect(@job2.reload.next_in_strand).to eq true
601
+ expect(@job3.reload.next_in_strand).to eq false
602
+
603
+ @job2.destroy
604
+ expect(@job1.reload.next_in_strand).to eq true
605
+ expect(@job3.reload.next_in_strand).to eq false
606
+ end
607
+
393
608
  it "creates first as true, and second as false, then transitions to second when deleted" do
394
609
  @job1 = create_job(singleton: "myjobs")
395
610
  Delayed::Job.get_and_lock_next_available("w1")
data/spec/spec_helper.rb CHANGED
@@ -54,7 +54,8 @@ connection_config = {
54
54
  host: ENV["TEST_DB_HOST"].presence,
55
55
  encoding: "utf8",
56
56
  username: ENV["TEST_DB_USERNAME"],
57
- database: ENV["TEST_DB_DATABASE"]
57
+ database: ENV["TEST_DB_DATABASE"],
58
+ min_messages: "notice"
58
59
  }
59
60
 
60
61
  def migrate(file)
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - Ethan Vizitei
9
9
  - Jacob Burroughs
10
- autorequire:
10
+ autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2021-11-08 00:00:00.000000000 Z
13
+ date: 2022-01-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -424,7 +424,7 @@ dependencies:
424
424
  - - "~>"
425
425
  - !ruby/object:Gem::Version
426
426
  version: 1.4.0
427
- description:
427
+ description:
428
428
  email:
429
429
  - cody@instructure.com
430
430
  - evizitei@instructure.com
@@ -466,6 +466,9 @@ files:
466
466
  - db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb
467
467
  - db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb
468
468
  - db/migrate/20211101190934_update_after_delete_trigger_for_singleton_index.rb
469
+ - db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb
470
+ - db/migrate/20211220112800_fix_singleton_race_condition_insert.rb
471
+ - db/migrate/20211220113000_fix_singleton_race_condition_delete.rb
469
472
  - exe/inst_jobs
470
473
  - lib/delayed/backend/active_record.rb
471
474
  - lib/delayed/backend/base.rb
@@ -483,6 +486,7 @@ files:
483
486
  - lib/delayed/periodic.rb
484
487
  - lib/delayed/plugin.rb
485
488
  - lib/delayed/pool.rb
489
+ - lib/delayed/rails_reloader_plugin.rb
486
490
  - lib/delayed/server.rb
487
491
  - lib/delayed/server/helpers.rb
488
492
  - lib/delayed/server/public/css/app.css
@@ -531,7 +535,7 @@ files:
531
535
  homepage: https://github.com/instructure/inst-jobs
532
536
  licenses: []
533
537
  metadata: {}
534
- post_install_message:
538
+ post_install_message:
535
539
  rdoc_options: []
536
540
  require_paths:
537
541
  - lib
@@ -546,32 +550,32 @@ required_rubygems_version: !ruby/object:Gem::Requirement
546
550
  - !ruby/object:Gem::Version
547
551
  version: '0'
548
552
  requirements: []
549
- rubygems_version: 3.2.24
550
- signing_key:
553
+ rubygems_version: 3.1.4
554
+ signing_key:
551
555
  specification_version: 4
552
556
  summary: Instructure-maintained fork of delayed_job
553
557
  test_files:
554
- - spec/sample_jobs.rb
555
- - spec/spec_helper.rb
556
- - spec/shared_jobs_specs.rb
557
- - spec/shared/performable_method.rb
558
- - spec/shared/testing.rb
559
- - spec/shared/delayed_batch.rb
560
- - spec/shared/worker.rb
561
- - spec/shared/delayed_method.rb
562
- - spec/shared/shared_backend.rb
563
- - spec/migrate/20140924140513_add_story_table.rb
564
- - spec/delayed/server_spec.rb
558
+ - spec/active_record_job_spec.rb
565
559
  - spec/delayed/cli_spec.rb
566
560
  - spec/delayed/daemon_spec.rb
567
- - spec/delayed/worker_spec.rb
568
- - spec/delayed/periodic_spec.rb
569
561
  - spec/delayed/message_sending_spec.rb
562
+ - spec/delayed/periodic_spec.rb
563
+ - spec/delayed/server_spec.rb
570
564
  - spec/delayed/settings_spec.rb
571
565
  - spec/delayed/work_queue/in_process_spec.rb
572
- - spec/delayed/work_queue/parent_process_spec.rb
573
566
  - spec/delayed/work_queue/parent_process/client_spec.rb
574
567
  - spec/delayed/work_queue/parent_process/server_spec.rb
575
- - spec/delayed/worker/health_check_spec.rb
568
+ - spec/delayed/work_queue/parent_process_spec.rb
576
569
  - spec/delayed/worker/consul_health_check_spec.rb
577
- - spec/active_record_job_spec.rb
570
+ - spec/delayed/worker/health_check_spec.rb
571
+ - spec/delayed/worker_spec.rb
572
+ - spec/migrate/20140924140513_add_story_table.rb
573
+ - spec/sample_jobs.rb
574
+ - spec/shared/delayed_batch.rb
575
+ - spec/shared/delayed_method.rb
576
+ - spec/shared/performable_method.rb
577
+ - spec/shared/shared_backend.rb
578
+ - spec/shared/testing.rb
579
+ - spec/shared/worker.rb
580
+ - spec/shared_jobs_specs.rb
581
+ - spec/spec_helper.rb