inst-jobs 3.0.1 → 3.0.6

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: c9d86b7b6161b9397c885a82d687f85d84e7a17db5261bfc5ef1bb7e9f25e2ac
4
- data.tar.gz: 4a75f8ac73b5f0a0b19d8073bcbfe87f654bbbdb90e147ddf02066916653d4e0
3
+ metadata.gz: 245e01e85640f50351b2bca5ae7b7391c5373c0f8bc5128f1294d9e7566b5346
4
+ data.tar.gz: 38622bfe41a62682e198119a6d86e10210e06341d1a045f30cdabd6281600566
5
5
  SHA512:
6
- metadata.gz: cd3f9df169146f34f7366e02830575bf27e2fd33e97d190bed34dd8be44e34431261111eef133368cd342668cabeb529af6e16c8985fcf15c99807ea7a07fbab
7
- data.tar.gz: 3924f7b2f6f4eee37c4d92bfedfd683304dfd0c035a65f47ea9dae607c0e81b03c35860420f39a7dadf890d203c85fe24de68c90c80a48839f23b6193311b3e3
6
+ metadata.gz: ced7dd5a9cfe21b545d1ade7d05efabd53de6eff97589626dd38ce650b1da641d93811b48848339bed3057f3e873c77f6c6b4d5d3d29640a68694d40f20402b2
7
+ data.tar.gz: 82f0bfad222bef95dec0c154e6a649c184ba4f09c122db0a52e6f5d17f30ce836987def104d4b49fab4b293838e0ba37034d1cad3fdb4e2df2ebe6c7a0556ec3
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateAfterDeleteTriggerForSingletonIndex < 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
+ running_count integer;
9
+ should_lock boolean;
10
+ should_be_precise boolean;
11
+ update_query varchar;
12
+ skip_locked varchar;
13
+ BEGIN
14
+ IF OLD.strand IS NOT NULL THEN
15
+ should_lock := true;
16
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
17
+
18
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
19
+ running_count := (SELECT COUNT(*) FROM (
20
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
21
+ ) subquery_for_count);
22
+ should_lock := running_count < OLD.max_concurrent;
23
+ END IF;
24
+
25
+ IF should_lock THEN
26
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
27
+ END IF;
28
+
29
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
30
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
31
+ -- singleton we grab isn't already running (which is a simple existence check, since
32
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
33
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
34
+ SELECT id FROM delayed_jobs j2
35
+ WHERE next_in_strand=false AND
36
+ j2.strand=$1.strand AND
37
+ (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)))
38
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
39
+ LIMIT ';
40
+
41
+ IF should_be_precise THEN
42
+ running_count := (SELECT COUNT(*) FROM (
43
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
44
+ ) s);
45
+ IF running_count < OLD.max_concurrent THEN
46
+ update_query := update_query || '($1.max_concurrent - $2)';
47
+ ELSE
48
+ -- we have too many running already; just bail
49
+ RETURN OLD;
50
+ END IF;
51
+ ELSE
52
+ update_query := update_query || '1';
53
+
54
+ -- n-strands don't require precise ordering; we can make this query more performant
55
+ IF OLD.max_concurrent > 1 THEN
56
+ skip_locked := ' SKIP LOCKED';
57
+ END IF;
58
+ END IF;
59
+
60
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
61
+ EXECUTE update_query USING OLD, running_count;
62
+ ELSIF OLD.singleton IS NOT NULL THEN
63
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false AND locked_by IS NULL;
64
+ END IF;
65
+ RETURN OLD;
66
+ END;
67
+ $$ LANGUAGE plpgsql;
68
+ SQL
69
+ end
70
+
71
+ def down
72
+ execute(<<~SQL)
73
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
74
+ DECLARE
75
+ running_count integer;
76
+ should_lock boolean;
77
+ should_be_precise boolean;
78
+ update_query varchar;
79
+ skip_locked varchar;
80
+ BEGIN
81
+ IF OLD.strand IS NOT NULL THEN
82
+ should_lock := true;
83
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
84
+
85
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
86
+ running_count := (SELECT COUNT(*) FROM (
87
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
88
+ ) subquery_for_count);
89
+ should_lock := running_count < OLD.max_concurrent;
90
+ END IF;
91
+
92
+ IF should_lock THEN
93
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
94
+ END IF;
95
+
96
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
97
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
98
+ -- singleton we grab isn't already running (which is a simple existence check, since
99
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
100
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
101
+ SELECT id FROM delayed_jobs j2
102
+ WHERE next_in_strand=false AND
103
+ j2.strand=$1.strand AND
104
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id))
105
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
106
+ LIMIT ';
107
+
108
+ IF should_be_precise THEN
109
+ running_count := (SELECT COUNT(*) FROM (
110
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
111
+ ) s);
112
+ IF running_count < OLD.max_concurrent THEN
113
+ update_query := update_query || '($1.max_concurrent - $2)';
114
+ ELSE
115
+ -- we have too many running already; just bail
116
+ RETURN OLD;
117
+ END IF;
118
+ ELSE
119
+ update_query := update_query || '1';
120
+
121
+ -- n-strands don't require precise ordering; we can make this query more performant
122
+ IF OLD.max_concurrent > 1 THEN
123
+ skip_locked := ' SKIP LOCKED';
124
+ END IF;
125
+ END IF;
126
+
127
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
128
+ EXECUTE update_query USING OLD, running_count;
129
+ ELSIF OLD.singleton IS NOT NULL THEN
130
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false;
131
+ END IF;
132
+ RETURN OLD;
133
+ END;
134
+ $$ LANGUAGE plpgsql;
135
+ SQL
136
+ end
137
+ end
@@ -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
@@ -309,7 +309,7 @@ module Delayed
309
309
  if Settings.silence_periodic_log
310
310
  ::ActiveRecord::Base.logger.silence(&block)
311
311
  else
312
- block.call
312
+ yield
313
313
  end
314
314
  end
315
315
 
@@ -548,7 +548,7 @@ module Delayed
548
548
 
549
549
  def fail!
550
550
  attrs = attributes
551
- attrs["original_job_id"] = attrs.delete("id")
551
+ attrs["original_job_id"] = attrs.delete("id") if Failed.columns_hash.key?("original_job_id")
552
552
  attrs["failed_at"] ||= self.class.db_time_now
553
553
  attrs.delete("next_in_strand")
554
554
  attrs.delete("max_concurrent")
@@ -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.1"
4
+ VERSION = "3.0.6"
5
5
  end
@@ -5,10 +5,10 @@ module Delayed
5
5
  module ProcessHelper
6
6
  STAT_LINUX = "stat --format=%%Y /proc/$WORKER_PID"
7
7
  STAT_MAC = "ps -o lstart -p $WORKER_PID"
8
- STAT = RUBY_PLATFORM =~ /darwin/ ? STAT_MAC : STAT_LINUX
8
+ STAT = RUBY_PLATFORM.include?("darwin") ? STAT_MAC : STAT_LINUX
9
9
  ALIVE_CHECK_LINUX = '[ -d "/proc/$WORKER_PID" ]'
10
10
  ALIVE_CHECK_MAC = "ps -p $WORKER_PID > /dev/null"
11
- ALIVE_CHECK = RUBY_PLATFORM =~ /darwin/ ? ALIVE_CHECK_MAC : ALIVE_CHECK_LINUX
11
+ ALIVE_CHECK = RUBY_PLATFORM.include?("darwin") ? ALIVE_CHECK_MAC : ALIVE_CHECK_LINUX
12
12
  SCRIPT_TEMPLATE = <<-BASH
13
13
  WORKER_PID="%<pid>d" # an example, filled from ruby when the check is created
14
14
  ORIGINAL_MTIME="%<mtime>s" # an example, filled from ruby when the check is created
@@ -29,7 +29,7 @@ module Delayed
29
29
  BASH
30
30
 
31
31
  def self.mtime(pid)
32
- if RUBY_PLATFORM =~ /darwin/
32
+ if RUBY_PLATFORM.include?("darwin")
33
33
  `ps -o lstart -p #{pid}`.sub(/\n$/, "").presence
34
34
  else
35
35
  File::Stat.new("/proc/#{pid}").mtime.to_i.to_s rescue nil
@@ -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
 
@@ -219,14 +219,14 @@ describe "Delayed::Backed::ActiveRecord::Job" do
219
219
  end
220
220
 
221
221
  it "gets process ids from locked_by" do
222
- 3.times.map { Delayed::Job.create payload_object: SimpleJob.new }
222
+ Array.new(3) { Delayed::Job.create payload_object: SimpleJob.new }
223
223
  Delayed::Job.get_and_lock_next_available(["job42:2", "job42:9001"])
224
224
  expect(Delayed::Job.processes_locked_locally(name: "job42").sort).to eq [2, 9001]
225
225
  expect(Delayed::Job.processes_locked_locally(name: "jobnotme")).to be_empty
226
226
  end
227
227
 
228
228
  it "allows fetching multiple jobs at once" do
229
- jobs = 3.times.map { Delayed::Job.create payload_object: SimpleJob.new }
229
+ jobs = Array.new(3) { Delayed::Job.create payload_object: SimpleJob.new }
230
230
  locked_jobs = Delayed::Job.get_and_lock_next_available(%w[worker1 worker2])
231
231
  expect(locked_jobs.length).to eq(2)
232
232
  expect(locked_jobs.keys).to eq(%w[worker1 worker2])
@@ -235,7 +235,7 @@ describe "Delayed::Backed::ActiveRecord::Job" do
235
235
  end
236
236
 
237
237
  it "allows fetching extra jobs" do
238
- jobs = 5.times.map { Delayed::Job.create payload_object: SimpleJob.new }
238
+ jobs = Array.new(5) { Delayed::Job.create payload_object: SimpleJob.new }
239
239
  locked_jobs = Delayed::Job.get_and_lock_next_available(["worker1"],
240
240
  prefetch: 2,
241
241
  prefetch_owner: "work_queue")
@@ -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,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.1
4
+ version: 3.0.6
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-10-20 00:00:00.000000000 Z
13
+ date: 2021-12-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -298,6 +298,20 @@ dependencies:
298
298
  - - "~>"
299
299
  - !ruby/object:Gem::Version
300
300
  version: '1.19'
301
+ - !ruby/object:Gem::Dependency
302
+ name: rubocop-performance
303
+ requirement: !ruby/object:Gem::Requirement
304
+ requirements:
305
+ - - "~>"
306
+ - !ruby/object:Gem::Version
307
+ version: 1.12.0
308
+ type: :development
309
+ prerelease: false
310
+ version_requirements: !ruby/object:Gem::Requirement
311
+ requirements:
312
+ - - "~>"
313
+ - !ruby/object:Gem::Version
314
+ version: 1.12.0
301
315
  - !ruby/object:Gem::Dependency
302
316
  name: rubocop-rails
303
317
  requirement: !ruby/object:Gem::Requirement
@@ -451,6 +465,10 @@ files:
451
465
  - db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb
452
466
  - db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb
453
467
  - db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb
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
454
472
  - exe/inst_jobs
455
473
  - lib/delayed/backend/active_record.rb
456
474
  - lib/delayed/backend/base.rb
@@ -468,6 +486,7 @@ files:
468
486
  - lib/delayed/periodic.rb
469
487
  - lib/delayed/plugin.rb
470
488
  - lib/delayed/pool.rb
489
+ - lib/delayed/rails_reloader_plugin.rb
471
490
  - lib/delayed/server.rb
472
491
  - lib/delayed/server/helpers.rb
473
492
  - lib/delayed/server/public/css/app.css
@@ -531,32 +550,32 @@ required_rubygems_version: !ruby/object:Gem::Requirement
531
550
  - !ruby/object:Gem::Version
532
551
  version: '0'
533
552
  requirements: []
534
- rubygems_version: 3.0.3
553
+ rubygems_version: 3.1.4
535
554
  signing_key:
536
555
  specification_version: 4
537
556
  summary: Instructure-maintained fork of delayed_job
538
557
  test_files:
539
- - spec/sample_jobs.rb
540
- - spec/spec_helper.rb
541
- - spec/shared_jobs_specs.rb
542
- - spec/shared/performable_method.rb
543
- - spec/shared/testing.rb
544
- - spec/shared/delayed_batch.rb
545
- - spec/shared/worker.rb
546
- - spec/shared/delayed_method.rb
547
- - spec/shared/shared_backend.rb
548
- - spec/migrate/20140924140513_add_story_table.rb
549
- - spec/delayed/server_spec.rb
558
+ - spec/active_record_job_spec.rb
550
559
  - spec/delayed/cli_spec.rb
551
560
  - spec/delayed/daemon_spec.rb
552
- - spec/delayed/worker_spec.rb
553
- - spec/delayed/periodic_spec.rb
554
561
  - spec/delayed/message_sending_spec.rb
562
+ - spec/delayed/periodic_spec.rb
563
+ - spec/delayed/server_spec.rb
555
564
  - spec/delayed/settings_spec.rb
556
565
  - spec/delayed/work_queue/in_process_spec.rb
557
- - spec/delayed/work_queue/parent_process_spec.rb
558
566
  - spec/delayed/work_queue/parent_process/client_spec.rb
559
567
  - spec/delayed/work_queue/parent_process/server_spec.rb
560
- - spec/delayed/worker/health_check_spec.rb
568
+ - spec/delayed/work_queue/parent_process_spec.rb
561
569
  - spec/delayed/worker/consul_health_check_spec.rb
562
- - 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