inst-jobs 3.0.3 → 3.0.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb38d77d225501a3f9ca85561266adf7f2b873d187d0ee9e820e30f773fc5846
4
- data.tar.gz: 0bdbb7e6609d0c228de6906b68ff41b7a5218b89832dd9f8958ed261d7a39fd2
3
+ metadata.gz: f318e6929f6bf9ab751c02abddf2e89f118eec259bd88597812cc6c71e74aa67
4
+ data.tar.gz: a142ca5ca379e7278f5746becf058e7ae6e7de5351a0d4ba9ec8e8524ca3ce4f
5
5
  SHA512:
6
- metadata.gz: 76c59051987d523e8465c98a0aa03edfa7ee6df1fc090ebeb8238f827d3f5487dccd61f92c20acf87df9b53153de03bb866d577f44eb3d0db18bd3cd9fa44f66
7
- data.tar.gz: 0262e34514a1919ff9715b1707d7c99dab2ddf4f3ef0c98e06441b61709ddb2bb36c763b73cc5a5512a03e2b2a253b507818da313213a621c40879d086fa9333
6
+ metadata.gz: 4f819854ff0690e7f3eeb22fe098b1e0f69f06d0447540e40ce66ff4fa1db4c1e5d87fbe134121c8c795782825e2007d02349445b5d315d0afe8b10cf331cdb4
7
+ data.tar.gz: 880295f1cabdc681f022655a5a481476b204c08d6a21a4f2b36b01544d2d9b2bdac55354c3d972d88760e22d5603b876ed4d41252f1bb146f9399c295ff918c8
@@ -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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonUniqueConstraint < ActiveRecord::Migration[5.2]
4
+ disable_ddl_transaction!
5
+
6
+ def up
7
+ rename_index :delayed_jobs, "index_delayed_jobs_on_singleton_not_running", "index_delayed_jobs_on_singleton_not_running_old"
8
+ rename_index :delayed_jobs, "index_delayed_jobs_on_singleton_running", "index_delayed_jobs_on_singleton_running_old"
9
+
10
+ # only one job can be queued in a singleton
11
+ add_index :delayed_jobs,
12
+ :singleton,
13
+ where: "singleton IS NOT NULL AND (locked_by IS NULL OR locked_by = '#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}')",
14
+ unique: true,
15
+ name: "index_delayed_jobs_on_singleton_not_running",
16
+ algorithm: :concurrently
17
+
18
+ # only one job can be running for a singleton
19
+ add_index :delayed_jobs,
20
+ :singleton,
21
+ where: "singleton IS NOT NULL AND locked_by IS NOT NULL AND locked_by <> '#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}'",
22
+ unique: true,
23
+ name: "index_delayed_jobs_on_singleton_running",
24
+ algorithm: :concurrently
25
+ end
26
+
27
+ def down
28
+ remove_index :delayed_jobs, name: "index_delayed_jobs_on_singleton_not_running_old"
29
+ remove_index :delayed_jobs, name: "index_delayed_jobs_on_singleton_running_old"
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateInsertTriggerForSingletonUniqueConstraintChange < 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 = '#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}' OR locked_by <> '#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}');
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
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', NEW.singleton)));
46
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
47
+ -- rather than doing a seq scan
48
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
49
+ IF FOUND THEN
50
+ NEW.next_in_strand := false;
51
+ END IF;
52
+ END IF;
53
+ RETURN NEW;
54
+ END;
55
+ $$ LANGUAGE plpgsql;
56
+ SQL
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateDeleteTriggerForSingletonUniqueConstraintChange < 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 = ''#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}'' OR j3.locked_by <> ''#{::Delayed::Backend::Base::ON_HOLD_LOCKED_BY}'')))
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
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', OLD.singleton)));
171
+
172
+ 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);
173
+
174
+ IF transition THEN
175
+ 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);
176
+
177
+ IF next_strand IS NOT NULL THEN
178
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
179
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
180
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
181
+ RETURN OLD;
182
+ END IF;
183
+ END IF;
184
+ ELSIF OLD.strand IS NOT NULL THEN
185
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
186
+ RETURN OLD;
187
+ END IF;
188
+
189
+ -- handles transitioning a singleton from stranded to not stranded --
190
+ -- handles transitioning a singleton from unstranded to stranded --
191
+ -- handles transitioning a singleton from strand A to strand B --
192
+ -- these transitions are a relatively rare case, so we take a shortcut and --
193
+ -- only start the next singleton if its strand does not currently have any running jobs --
194
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
195
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
196
+ SELECT id FROM delayed_jobs j2
197
+ WHERE next_in_strand=false AND
198
+ j2.singleton=OLD.singleton AND
199
+ j2.locked_by IS NULL AND
200
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
201
+ FOR UPDATE
202
+ );
203
+ END IF;
204
+ RETURN OLD;
205
+ END;
206
+ $$ LANGUAGE plpgsql;
207
+ SQL
208
+ end
209
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveOldSingletonIndex < ActiveRecord::Migration[5.2]
4
+ disable_ddl_transaction!
5
+
6
+ def up
7
+ remove_index :delayed_jobs, name: "index_delayed_jobs_on_singleton_not_running_old"
8
+ remove_index :delayed_jobs, name: "index_delayed_jobs_on_singleton_running_old"
9
+ end
10
+
11
+ def down
12
+ rename_index :delayed_jobs, "index_delayed_jobs_on_singleton_not_running", "index_delayed_jobs_on_singleton_not_running_old"
13
+ rename_index :delayed_jobs, "index_delayed_jobs_on_singleton_running", "index_delayed_jobs_on_singleton_running_old"
14
+
15
+ # only one job can be queued in a singleton
16
+ add_index :delayed_jobs,
17
+ :singleton,
18
+ where: "singleton IS NOT NULL AND locked_by IS NULL",
19
+ unique: true,
20
+ name: "index_delayed_jobs_on_singleton_not_running",
21
+ algorithm: :concurrently
22
+
23
+ # only one job can be running for a singleton
24
+ add_index :delayed_jobs,
25
+ :singleton,
26
+ where: "singleton IS NOT NULL AND locked_by IS NOT NULL",
27
+ unique: true,
28
+ name: "index_delayed_jobs_on_singleton_running",
29
+ algorithm: :concurrently
30
+ end
31
+ 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
@@ -12,6 +12,7 @@ module Delayed
12
12
  end
13
13
 
14
14
  module Base
15
+ ON_HOLD_BLOCKER = "blocker job"
15
16
  ON_HOLD_LOCKED_BY = "on hold"
16
17
  ON_HOLD_COUNT = 50
17
18
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "3.0.3"
4
+ VERSION = "3.0.8"
5
5
  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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2021-11-30 00:00:00.000000000 Z
13
+ date: 2022-02-03 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -466,6 +466,13 @@ 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
472
+ - db/migrate/20220127091200_fix_singleton_unique_constraint.rb
473
+ - db/migrate/20220128084800_update_insert_trigger_for_singleton_unique_constraint_change.rb
474
+ - db/migrate/20220128084900_update_delete_trigger_for_singleton_unique_constraint_change.rb
475
+ - db/migrate/20220203063200_remove_old_singleton_index.rb
469
476
  - exe/inst_jobs
470
477
  - lib/delayed/backend/active_record.rb
471
478
  - lib/delayed/backend/base.rb
@@ -547,7 +554,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
547
554
  - !ruby/object:Gem::Version
548
555
  version: '0'
549
556
  requirements: []
550
- rubygems_version: 3.2.15
557
+ rubygems_version: 3.1.4
551
558
  signing_key:
552
559
  specification_version: 4
553
560
  summary: Instructure-maintained fork of delayed_job