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 +4 -4
- data/db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb +171 -0
- data/db/migrate/20211220112800_fix_singleton_race_condition_insert.rb +59 -0
- data/db/migrate/20211220113000_fix_singleton_race_condition_delete.rb +207 -0
- data/db/migrate/20220127091200_fix_singleton_unique_constraint.rb +31 -0
- data/db/migrate/20220128084800_update_insert_trigger_for_singleton_unique_constraint_change.rb +60 -0
- data/db/migrate/20220128084900_update_delete_trigger_for_singleton_unique_constraint_change.rb +209 -0
- data/db/migrate/20220203063200_remove_old_singleton_index.rb +31 -0
- data/exe/inst_jobs +1 -1
- data/lib/delayed/backend/base.rb +1 -0
- data/lib/delayed/version.rb +1 -1
- data/spec/shared/shared_backend.rb +215 -0
- data/spec/spec_helper.rb +2 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f318e6929f6bf9ab751c02abddf2e89f118eec259bd88597812cc6c71e74aa67
|
4
|
+
data.tar.gz: a142ca5ca379e7278f5746becf058e7ae6e7de5351a0d4ba9ec8e8524ca3ce4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/db/migrate/20220128084800_update_insert_trigger_for_singleton_unique_constraint_change.rb
ADDED
@@ -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
|
data/db/migrate/20220128084900_update_delete_trigger_for_singleton_unique_constraint_change.rb
ADDED
@@ -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
data/lib/delayed/backend/base.rb
CHANGED
data/lib/delayed/version.rb
CHANGED
@@ -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
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.
|
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:
|
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.
|
557
|
+
rubygems_version: 3.1.4
|
551
558
|
signing_key:
|
552
559
|
specification_version: 4
|
553
560
|
summary: Instructure-maintained fork of delayed_job
|