dirty_pipeline 0.7.1 → 0.8.1
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/dirty_pipeline.gemspec +1 -0
- data/lib/dirty_pipeline.rb +39 -3
- data/lib/dirty_pipeline/event.rb +5 -4
- data/lib/dirty_pipeline/pg.rb +12 -0
- data/lib/dirty_pipeline/pg/queue.rb +171 -0
- data/lib/dirty_pipeline/pg/railway.rb +143 -0
- data/lib/dirty_pipeline/pg/storage.rb +112 -0
- data/lib/dirty_pipeline/redis/queue.rb +80 -0
- data/lib/dirty_pipeline/redis/railway.rb +86 -0
- data/lib/dirty_pipeline/redis/storage.rb +94 -0
- data/lib/dirty_pipeline/transaction.rb +2 -1
- data/lib/dirty_pipeline/version.rb +1 -1
- metadata +23 -5
- data/lib/dirty_pipeline/queue.rb +0 -78
- data/lib/dirty_pipeline/railway.rb +0 -84
- data/lib/dirty_pipeline/storage.rb +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f3fd5ee73f6187ea39daa3d7b88371c84f3c6ca848b7ba65f58b7c8876c55b5
|
4
|
+
data.tar.gz: ab18cf256c02e3851a87c093523bffe204bc25bba6a31be8027a923ddbebaa69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a818876d704b6dbb40ec6a0bad6ac45fb38a40ad87fce41859ea5cc2a29ddb8d72067aefdb63d16d888c6cef0fcd840f3b65ca00b0fc810133b081ed10bd25f7
|
7
|
+
data.tar.gz: e6e014a6f8b37f55a688c5797c49287642531d4cc3dc2373688936b1e9bc0c2e7263947e9cbdcf6e9e152294cc0cf6c34f24e5835bad782c5bd497c7960f897e
|
data/dirty_pipeline.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
# temporary dependency
|
25
25
|
spec.add_runtime_dependency "sidekiq", "~> 5.0"
|
26
26
|
spec.add_runtime_dependency "redis", "~> 4.0"
|
27
|
+
spec.add_runtime_dependency "pg", "~> 1.0"
|
27
28
|
|
28
29
|
spec.add_development_dependency "bundler", "~> 1.16"
|
29
30
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/dirty_pipeline.rb
CHANGED
@@ -4,12 +4,21 @@ require "securerandom"
|
|
4
4
|
module DirtyPipeline
|
5
5
|
require_relative "dirty_pipeline/ext/camelcase.rb"
|
6
6
|
require_relative "dirty_pipeline/status.rb"
|
7
|
-
require_relative "dirty_pipeline/storage.rb"
|
8
7
|
require_relative "dirty_pipeline/worker.rb"
|
9
8
|
require_relative "dirty_pipeline/transaction.rb"
|
10
9
|
require_relative "dirty_pipeline/event.rb"
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
# Redis
|
12
|
+
require_relative "dirty_pipeline/redis/railway.rb"
|
13
|
+
require_relative "dirty_pipeline/redis/storage.rb"
|
14
|
+
require_relative "dirty_pipeline/redis/queue.rb"
|
15
|
+
|
16
|
+
# Postgres
|
17
|
+
require_relative "dirty_pipeline/pg.rb"
|
18
|
+
require_relative "dirty_pipeline/pg/railway.rb"
|
19
|
+
require_relative "dirty_pipeline/pg/storage.rb"
|
20
|
+
require_relative "dirty_pipeline/pg/queue.rb"
|
21
|
+
|
13
22
|
require_relative "dirty_pipeline/base.rb"
|
14
23
|
require_relative "dirty_pipeline/transition.rb"
|
15
24
|
|
@@ -17,4 +26,31 @@ module DirtyPipeline
|
|
17
26
|
def self.with_redis
|
18
27
|
fail NotImplementedError
|
19
28
|
end
|
29
|
+
|
30
|
+
# This method should yield raw PG connection
|
31
|
+
def self.with_postgres
|
32
|
+
fail NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
# def self.with_postgres
|
36
|
+
# yield(ActiveRecord::Base.connection.raw_connection)
|
37
|
+
# ensure
|
38
|
+
# ActiveRecord::Base.clear_active_connections!
|
39
|
+
# end
|
40
|
+
|
41
|
+
Queue = Redis::Queue
|
42
|
+
Storage = Redis::Storage
|
43
|
+
Railway = Redis::Railway
|
44
|
+
|
45
|
+
def self.create!(conn)
|
46
|
+
Queue.create!(conn) if Queue.respond_to?(:create!)
|
47
|
+
Storage.create!(conn) if Storage.respond_to?(:create!)
|
48
|
+
Railway.create!(conn) if Railway.respond_to?(:create!)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.destroy!(conn)
|
52
|
+
Queue.destroy!(conn) if Queue.respond_to?(:destroy!)
|
53
|
+
Storage.destroy!(conn) if Storage.respond_to?(:destroy!)
|
54
|
+
Railway.destroy!(conn) if Railway.respond_to?(:destroy!)
|
55
|
+
end
|
20
56
|
end
|
data/lib/dirty_pipeline/event.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'time'
|
2
3
|
|
3
4
|
module DirtyPipeline
|
4
5
|
class Event
|
@@ -41,7 +42,7 @@ module DirtyPipeline
|
|
41
42
|
"transaction_uuid" => @tx_id,
|
42
43
|
"transition" => transition,
|
43
44
|
"args" => args,
|
44
|
-
"created_at" => Time.now,
|
45
|
+
"created_at" => Time.now.utc.iso8601,
|
45
46
|
"cache" => {},
|
46
47
|
"attempts_count" => 1,
|
47
48
|
"status" => NEW,
|
@@ -71,7 +72,7 @@ module DirtyPipeline
|
|
71
72
|
@error = {
|
72
73
|
"exception" => exception.class.to_s,
|
73
74
|
"exception_message" => exception.message,
|
74
|
-
"created_at" => Time.now,
|
75
|
+
"created_at" => Time.now.utc.iso8601,
|
75
76
|
}
|
76
77
|
failure!
|
77
78
|
end
|
@@ -81,7 +82,7 @@ module DirtyPipeline
|
|
81
82
|
end
|
82
83
|
|
83
84
|
def attempt_retry!
|
84
|
-
@data["updated_at"] = Time.now
|
85
|
+
@data["updated_at"] = Time.now.utc.iso8601
|
85
86
|
@data["attempts_count"] = attempts_count + 1
|
86
87
|
end
|
87
88
|
|
@@ -89,7 +90,7 @@ module DirtyPipeline
|
|
89
90
|
@data.merge!(
|
90
91
|
"destination" => destination,
|
91
92
|
"changes" => changes,
|
92
|
-
"updated_at" => Time.now,
|
93
|
+
"updated_at" => Time.now.utc.iso8601,
|
93
94
|
"status" => SUCCESS,
|
94
95
|
)
|
95
96
|
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
module PG
|
3
|
+
class Queue
|
4
|
+
# decoder = PG::TextDecoder::Array.new
|
5
|
+
# see https://stackoverflow.com/questions/34886260/how-do-you-decode-a-json-field-using-the-pg-gem
|
6
|
+
def self.create!(connection)
|
7
|
+
connection.exec <<~SQL
|
8
|
+
CREATE TABLE dp_active_events (
|
9
|
+
key TEXT CONSTRAINT primary_event_queues_key PRIMARY KEY,
|
10
|
+
payload TEXT,
|
11
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
12
|
+
);
|
13
|
+
|
14
|
+
CREATE SEQUENCE dp_event_queues_id_seq START 1;
|
15
|
+
CREATE TABLE dp_event_queues (
|
16
|
+
id BIGINT PRIMARY KEY DEFAULT nextval('dp_event_queues_id_seq'),
|
17
|
+
key TEXT NOT NULL,
|
18
|
+
payload TEXT,
|
19
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
20
|
+
);
|
21
|
+
SQL
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.destroy!(connection)
|
25
|
+
connection.exec <<~SQL
|
26
|
+
DROP TABLE IF EXISTS dp_active_events;
|
27
|
+
DROP TABLE IF EXISTS dp_event_queues;
|
28
|
+
DROP SEQUENCE IF EXISTS dp_event_queues_id_seq;
|
29
|
+
SQL
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(operation, subject_class, subject_id, transaction_id)
|
33
|
+
@root = "dirty-pipeline-queue:#{subject_class}:#{subject_id}:" \
|
34
|
+
"op_#{operation}:txid_#{transaction_id}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_postgres(&block)
|
38
|
+
DirtyPipeline.with_postgres(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
DELETE_ACTIVE = <<~SQL
|
42
|
+
DELETE FROM dp_active_events WHERE key = $1;
|
43
|
+
SQL
|
44
|
+
DELETE_EVENTS = <<~SQL
|
45
|
+
DELETE FROM dp_event_queues WHERE key = $1;
|
46
|
+
SQL
|
47
|
+
|
48
|
+
def clear!
|
49
|
+
with_postgres do |c|
|
50
|
+
c.transaction do
|
51
|
+
c.exec(DELETE_ACTIVE, [active_event_key])
|
52
|
+
c.exec(DELETE_EVENTS, [events_queue_key])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
SELECT_ALL_EVENTS = <<~SQL
|
58
|
+
SELECT payload FROM dp_event_queues WHERE key = $1 ORDER BY id DESC;
|
59
|
+
SQL
|
60
|
+
def to_a
|
61
|
+
with_postgres do |c|
|
62
|
+
c.exec(SELECT_ALL_EVENTS, [events_queue_key]).to_a.map! do |row|
|
63
|
+
unpack(row.values.first)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
PUSH_EVENT = <<~SQL
|
69
|
+
INSERT INTO dp_event_queues (id, key, payload)
|
70
|
+
VALUES (-nextval('dp_event_queues_id_seq'), $1, $2);
|
71
|
+
SQL
|
72
|
+
def push(event)
|
73
|
+
with_postgres do |c|
|
74
|
+
c.transaction do
|
75
|
+
c.exec(PUSH_EVENT, [events_queue_key, pack(event)])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
alias :<< :push
|
82
|
+
|
83
|
+
UNSHIFT_EVENT = <<~SQL
|
84
|
+
INSERT INTO dp_event_queues (key, payload) VALUES ($1, $2);
|
85
|
+
SQL
|
86
|
+
def unshift(event)
|
87
|
+
with_postgres do |c|
|
88
|
+
c.transaction do
|
89
|
+
c.exec(UNSHIFT_EVENT, [events_queue_key, pack(event)])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
SELECT_LAST_EVENT = <<~SQL
|
96
|
+
SELECT id, payload FROM dp_event_queues
|
97
|
+
WHERE key = $1
|
98
|
+
ORDER BY id DESC LIMIT 1;
|
99
|
+
SQL
|
100
|
+
DELETE_EVENT = <<~SQL
|
101
|
+
DELETE FROM dp_event_queues WHERE key = $1 AND id = $2;
|
102
|
+
SQL
|
103
|
+
DELETE_ACTIVE_EVENT = <<~SQL
|
104
|
+
DELETE FROM dp_active_events WHERE key = $1;
|
105
|
+
SQL
|
106
|
+
SET_EVENT_ACTIVE = <<~SQL
|
107
|
+
INSERT INTO dp_active_events (key, payload) VALUES ($1, $2)
|
108
|
+
ON CONFLICT (key) DO UPDATE SET payload = EXCLUDED.payload;
|
109
|
+
SQL
|
110
|
+
def pop
|
111
|
+
with_postgres do |c|
|
112
|
+
c.transaction do
|
113
|
+
event_id, raw_event =
|
114
|
+
PG.multi(c.exec(SELECT_LAST_EVENT, [events_queue_key]))
|
115
|
+
if raw_event.nil?
|
116
|
+
c.exec(DELETE_ACTIVE_EVENT, [active_event_key])
|
117
|
+
else
|
118
|
+
c.exec(DELETE_EVENT, [events_queue_key, event_id])
|
119
|
+
c.exec(SET_EVENT_ACTIVE, [active_event_key, raw_event])
|
120
|
+
end
|
121
|
+
unpack(raw_event)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
SELECT_ACTIVE_EVENT = <<~SQL
|
127
|
+
SELECT payload FROM dp_active_events WHERE key = $1;
|
128
|
+
SQL
|
129
|
+
def processing_event
|
130
|
+
with_postgres do |c|
|
131
|
+
raw_event = PG.single(
|
132
|
+
c.exec(SELECT_ACTIVE_EVENT, [active_event_key])
|
133
|
+
)
|
134
|
+
unpack(raw_event)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def pack(event)
|
141
|
+
JSON.dump(
|
142
|
+
"evid" => event.id,
|
143
|
+
"txid" => event.tx_id,
|
144
|
+
"transit" => event.transition,
|
145
|
+
"args" => event.args,
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def unpack(packed_event)
|
150
|
+
return unless packed_event
|
151
|
+
unpacked_event = JSON.load(packed_event)
|
152
|
+
Event.new(
|
153
|
+
data: {
|
154
|
+
"uuid" => unpacked_event["evid"],
|
155
|
+
"transaction_uuid" => unpacked_event["txid"],
|
156
|
+
"transition" => unpacked_event["transit"],
|
157
|
+
"args" => unpacked_event["args"],
|
158
|
+
}
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def events_queue_key
|
163
|
+
"#{@root}:events"
|
164
|
+
end
|
165
|
+
|
166
|
+
def active_event_key
|
167
|
+
"#{@root}:active"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
module PG
|
3
|
+
class Railway
|
4
|
+
DEFAULT_OPERATIONS = %w(call undo finalize)
|
5
|
+
|
6
|
+
def self.create!(connection)
|
7
|
+
connection.exec <<~SQL
|
8
|
+
CREATE TABLE dp_active_operations (
|
9
|
+
key TEXT CONSTRAINT primary_dp_active_operations_key PRIMARY KEY,
|
10
|
+
name TEXT,
|
11
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
12
|
+
);
|
13
|
+
CREATE TABLE dp_active_transactions (
|
14
|
+
key TEXT CONSTRAINT primary_dp_active_tx_key PRIMARY KEY,
|
15
|
+
name TEXT,
|
16
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
17
|
+
);
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.destroy!(connection)
|
22
|
+
connection.exec <<~SQL
|
23
|
+
DROP TABLE IF EXISTS dp_active_operations;
|
24
|
+
DROP TABLE IF EXISTS dp_active_transactions;
|
25
|
+
SQL
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(subject, transaction_id)
|
29
|
+
@tx_id = transaction_id
|
30
|
+
@subject_class = subject.class.to_s
|
31
|
+
@subject_id = subject.id.to_s
|
32
|
+
@root = "dirty-pipeline-rail:#{subject.class}:#{subject.id}:"
|
33
|
+
@queues = Hash[
|
34
|
+
DEFAULT_OPERATIONS.map do |operation|
|
35
|
+
[operation, create_queue(operation)]
|
36
|
+
end
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
DELETE_OPERATION = <<~SQL
|
41
|
+
DELETE FROM dp_active_operations WHERE key = $1;
|
42
|
+
SQL
|
43
|
+
DELETE_TRANSACTION = <<~SQL
|
44
|
+
DELETE FROM dp_active_transactions WHERE key = $1;
|
45
|
+
SQL
|
46
|
+
def clear!
|
47
|
+
@queues.values.each(&:clear!)
|
48
|
+
DirtyPipeline.with_postgres do |c|
|
49
|
+
c.transaction do
|
50
|
+
c.exec DELETE_OPERATION, [active_operation_key]
|
51
|
+
c.exec DELETE_TRANSACTION, [active_transaction_key]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def next
|
57
|
+
return if other_transaction_in_progress?
|
58
|
+
start_transaction! unless running_transaction
|
59
|
+
|
60
|
+
queue.pop.tap { |event| finish_transaction! if event.nil? }
|
61
|
+
end
|
62
|
+
|
63
|
+
def queue(operation_name = active)
|
64
|
+
@queues.fetch(operation_name.to_s) do
|
65
|
+
@queues.store(operation_name, create_queue(operation_name))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias :[] :queue
|
69
|
+
|
70
|
+
SWITCH_OPERATION = <<~SQL
|
71
|
+
INSERT INTO dp_active_operations (key, name) VALUES ($1, $2)
|
72
|
+
ON CONFLICT (key)
|
73
|
+
DO UPDATE SET name = EXCLUDED.name;
|
74
|
+
SQL
|
75
|
+
def switch_to(name)
|
76
|
+
raise ArgumentError unless DEFAULT_OPERATIONS.include?(name.to_s)
|
77
|
+
return if name.to_s == active
|
78
|
+
|
79
|
+
DirtyPipeline.with_postgres do |c|
|
80
|
+
c.transaction do
|
81
|
+
c.exec(SWITCH_OPERATION, [active_operation_key, name])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
SELECT_OPERATION = <<~SQL
|
87
|
+
SELECT name FROM dp_active_operations WHERE key = $1;
|
88
|
+
SQL
|
89
|
+
def active
|
90
|
+
DirtyPipeline.with_postgres do |c|
|
91
|
+
PG.single c.exec(SELECT_OPERATION, [active_operation_key])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias :operation :active
|
95
|
+
|
96
|
+
SELECT_TRANSACTION = <<~SQL
|
97
|
+
SELECT name FROM dp_active_transactions WHERE key = $1;
|
98
|
+
SQL
|
99
|
+
def running_transaction
|
100
|
+
DirtyPipeline.with_postgres do |c|
|
101
|
+
PG.single c.exec(SELECT_TRANSACTION, [active_transaction_key])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def other_transaction_in_progress?
|
106
|
+
return false if running_transaction.nil?
|
107
|
+
running_transaction != @tx_id
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def create_queue(operation_name)
|
113
|
+
Queue.new(operation_name, @subject_class, @subject_id, @tx_id)
|
114
|
+
end
|
115
|
+
|
116
|
+
def active_transaction_key
|
117
|
+
"#{@root}:active_transaction"
|
118
|
+
end
|
119
|
+
|
120
|
+
def active_operation_key
|
121
|
+
"#{@root}:active_operation"
|
122
|
+
end
|
123
|
+
|
124
|
+
SWITCH_TRANSACTION = <<~SQL
|
125
|
+
INSERT INTO dp_active_transactions (key, name) VALUES ($1, $2)
|
126
|
+
ON CONFLICT (key)
|
127
|
+
DO UPDATE SET name = EXCLUDED.name;
|
128
|
+
SQL
|
129
|
+
def start_transaction!
|
130
|
+
switch_to(DEFAULT_OPERATIONS.first) unless active
|
131
|
+
DirtyPipeline.with_postgres do |c|
|
132
|
+
c.transaction do
|
133
|
+
c.exec(SWITCH_TRANSACTION, [active_transaction_key, @tx_id])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def finish_transaction!
|
139
|
+
clear! if running_transaction == @tx_id
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
# Storage structure
|
3
|
+
# {
|
4
|
+
# status: :errored,
|
5
|
+
# state: {
|
6
|
+
# field: "value",
|
7
|
+
# }
|
8
|
+
# }
|
9
|
+
module PG
|
10
|
+
class Storage
|
11
|
+
class InvalidPipelineStorage < StandardError; end
|
12
|
+
|
13
|
+
def self.create!(connection)
|
14
|
+
connection.exec <<~SQL
|
15
|
+
CREATE TABLE dp_events_store (
|
16
|
+
uuid TEXT CONSTRAINT primary_active_operations_key PRIMARY KEY,
|
17
|
+
context TEXT NOT NULL,
|
18
|
+
data TEXT,
|
19
|
+
error TEXT,
|
20
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
21
|
+
);
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.destroy!(connection)
|
26
|
+
connection.exec <<~SQL
|
27
|
+
DROP TABLE IF EXISTS dp_events_store;
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :subject, :field, :store, :subject_key
|
32
|
+
alias :to_h :store
|
33
|
+
def initialize(subject, field)
|
34
|
+
@subject = subject
|
35
|
+
@field = field
|
36
|
+
@store = subject.send(@field).to_h
|
37
|
+
reset if @store.empty?
|
38
|
+
@subject_key = "#{subject.class}:#{subject.id}"
|
39
|
+
raise InvalidPipelineStorage, store unless valid_store?
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset!
|
43
|
+
reset
|
44
|
+
save!
|
45
|
+
end
|
46
|
+
|
47
|
+
def status
|
48
|
+
store["status"]
|
49
|
+
end
|
50
|
+
|
51
|
+
SAVE_EVENT = <<~SQL
|
52
|
+
INSERT INTO dp_events_store (uuid, context, data, error)
|
53
|
+
VALUES ($1, $2, $3, $4)
|
54
|
+
ON CONFLICT (uuid)
|
55
|
+
DO UPDATE SET data = EXCLUDED.data, error = EXCLUDED.error;
|
56
|
+
SQL
|
57
|
+
def commit!(event)
|
58
|
+
store["status"] = event.destination if event.destination
|
59
|
+
store["state"].merge!(event.changes) unless event.changes.to_h.empty?
|
60
|
+
DirtyPipeline.with_postgres do |c|
|
61
|
+
data, error = {}, {}
|
62
|
+
data = event.data.to_h if event.data.respond_to?(:to_h)
|
63
|
+
error = event.error.to_h if event.error.respond_to?(:to_h)
|
64
|
+
c.transaction do
|
65
|
+
c.exec(
|
66
|
+
SAVE_EVENT,
|
67
|
+
[event.id, subject_key, JSON.dump(data), JSON.dump(error)]
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
save!
|
72
|
+
end
|
73
|
+
|
74
|
+
FIND_EVENT = <<-SQL
|
75
|
+
SELECT data, error FROM dp_events_store
|
76
|
+
WHERE uuid = $1 AND context = $2;
|
77
|
+
SQL
|
78
|
+
def find_event(event_id)
|
79
|
+
DirtyPipeline.with_postgres do |c|
|
80
|
+
found_event, found_error =
|
81
|
+
PG.multi(c.exec(FIND_EVENT, [event_id, subject_key]))
|
82
|
+
return unless found_event
|
83
|
+
Event.new(
|
84
|
+
data: JSON.parse(found_event), error: JSON.parse(found_error)
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def valid_store?
|
92
|
+
(store.keys & %w(status state)).size.eql?(2)
|
93
|
+
end
|
94
|
+
|
95
|
+
# FIXME: save! - configurable method
|
96
|
+
def save!
|
97
|
+
subject.send("#{field}=", store)
|
98
|
+
subject.save!
|
99
|
+
end
|
100
|
+
|
101
|
+
def reset
|
102
|
+
@store = subject.send(
|
103
|
+
"#{field}=",
|
104
|
+
{
|
105
|
+
"status" => nil,
|
106
|
+
"state" => {},
|
107
|
+
}
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
module Redis
|
3
|
+
class Queue
|
4
|
+
def initialize(operation, subject_class, subject_id, transaction_id)
|
5
|
+
@root = "dirty-pipeline-queue:#{subject_class}:#{subject_id}:" \
|
6
|
+
"op_#{operation}:txid_#{transaction_id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear!
|
10
|
+
DirtyPipeline.with_redis do |r|
|
11
|
+
r.del active_event_key
|
12
|
+
r.del events_queue_key
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
DirtyPipeline.with_redis do |r|
|
18
|
+
r.lrange(events_queue_key, 0, -1).map! do |packed_event|
|
19
|
+
unpack(packed_event)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def push(event)
|
25
|
+
DirtyPipeline.with_redis { |r| r.rpush(events_queue_key, pack(event)) }
|
26
|
+
self
|
27
|
+
end
|
28
|
+
alias :<< :push
|
29
|
+
|
30
|
+
def unshift(event)
|
31
|
+
DirtyPipeline.with_redis { |r| r.lpush(events_queue_key, pack(event)) }
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def pop
|
36
|
+
DirtyPipeline.with_redis do |r|
|
37
|
+
data = r.lpop(events_queue_key)
|
38
|
+
data.nil? ? r.del(active_event_key) : r.set(active_event_key, data)
|
39
|
+
unpack(data)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def processing_event
|
44
|
+
DirtyPipeline.with_redis { |r| unpack(r.get(active_event_key)) }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def pack(event)
|
50
|
+
JSON.dump(
|
51
|
+
"evid" => event.id,
|
52
|
+
"txid" => event.tx_id,
|
53
|
+
"transit" => event.transition,
|
54
|
+
"args" => event.args,
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def unpack(packed_event)
|
59
|
+
return unless packed_event
|
60
|
+
unpacked_event = JSON.load(packed_event)
|
61
|
+
Event.new(
|
62
|
+
data: {
|
63
|
+
"uuid" => unpacked_event["evid"],
|
64
|
+
"transaction_uuid" => unpacked_event["txid"],
|
65
|
+
"transition" => unpacked_event["transit"],
|
66
|
+
"args" => unpacked_event["args"],
|
67
|
+
}
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def events_queue_key
|
72
|
+
"#{@root}:events"
|
73
|
+
end
|
74
|
+
|
75
|
+
def active_event_key
|
76
|
+
"#{@root}:active"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
module Redis
|
3
|
+
class Railway
|
4
|
+
DEFAULT_OPERATIONS = %w(call undo finalize)
|
5
|
+
|
6
|
+
def initialize(subject, transaction_id)
|
7
|
+
@tx_id = transaction_id
|
8
|
+
@subject_class = subject.class.to_s
|
9
|
+
@subject_id = subject.id.to_s
|
10
|
+
@root = "dirty-pipeline-rail:#{subject.class}:#{subject.id}:"
|
11
|
+
@queues = Hash[
|
12
|
+
DEFAULT_OPERATIONS.map do |operation|
|
13
|
+
[operation, create_queue(operation)]
|
14
|
+
end
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear!
|
19
|
+
@queues.values.each(&:clear!)
|
20
|
+
DirtyPipeline.with_redis do |r|
|
21
|
+
r.multi do |mr|
|
22
|
+
mr.del(active_operation_key)
|
23
|
+
mr.del(active_transaction_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def next
|
29
|
+
return if other_transaction_in_progress?
|
30
|
+
start_transaction! unless running_transaction
|
31
|
+
|
32
|
+
queue.pop.tap { |event| finish_transaction! if event.nil? }
|
33
|
+
end
|
34
|
+
|
35
|
+
def queue(operation_name = active)
|
36
|
+
@queues.fetch(operation_name.to_s) do
|
37
|
+
@queues.store(operation_name, create_queue(operation_name))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias :[] :queue
|
41
|
+
|
42
|
+
def switch_to(name)
|
43
|
+
raise ArgumentError unless DEFAULT_OPERATIONS.include?(name.to_s)
|
44
|
+
return if name.to_s == active
|
45
|
+
DirtyPipeline.with_redis { |r| r.set(active_operation_key, name) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def active
|
49
|
+
DirtyPipeline.with_redis { |r| r.get(active_operation_key) }
|
50
|
+
end
|
51
|
+
alias :operation :active
|
52
|
+
|
53
|
+
def running_transaction
|
54
|
+
DirtyPipeline.with_redis { |r| r.get(active_transaction_key) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def other_transaction_in_progress?
|
58
|
+
return false if running_transaction.nil?
|
59
|
+
running_transaction != @tx_id
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create_queue(operation_name)
|
65
|
+
Queue.new(operation_name, @subject_class, @subject_id, @tx_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def active_transaction_key
|
69
|
+
"#{@root}:active_transaction"
|
70
|
+
end
|
71
|
+
|
72
|
+
def active_operation_key
|
73
|
+
"#{@root}:active_operation"
|
74
|
+
end
|
75
|
+
|
76
|
+
def start_transaction!
|
77
|
+
switch_to(DEFAULT_OPERATIONS.first) unless active
|
78
|
+
DirtyPipeline.with_redis { |r| r.set(active_transaction_key, @tx_id) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def finish_transaction!
|
82
|
+
clear! if running_transaction == @tx_id
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
# Storage structure
|
3
|
+
# {
|
4
|
+
# status: :errored,
|
5
|
+
# state: {
|
6
|
+
# field: "value",
|
7
|
+
# },
|
8
|
+
# errors: {
|
9
|
+
# "<event_id>": {
|
10
|
+
# error: "RuPost::API::Error",
|
11
|
+
# error_message: "Timeout error",
|
12
|
+
# created_at: 2018-01-01T13:22Z
|
13
|
+
# },
|
14
|
+
# },
|
15
|
+
# events: {
|
16
|
+
# <event_id>: {
|
17
|
+
# transition: "Create",
|
18
|
+
# args: ...,
|
19
|
+
# changes: ...,
|
20
|
+
# created_at: ...,
|
21
|
+
# updated_at: ...,
|
22
|
+
# attempts_count: 2,
|
23
|
+
# },
|
24
|
+
# <event_id>: {...},
|
25
|
+
# }
|
26
|
+
# }
|
27
|
+
module Redis
|
28
|
+
class Storage
|
29
|
+
class InvalidPipelineStorage < StandardError; end
|
30
|
+
|
31
|
+
attr_reader :subject, :field, :store
|
32
|
+
alias :to_h :store
|
33
|
+
def initialize(subject, field)
|
34
|
+
@subject = subject
|
35
|
+
@field = field
|
36
|
+
@store = subject.send(@field).to_h
|
37
|
+
reset if @store.empty?
|
38
|
+
raise InvalidPipelineStorage, store unless valid_store?
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset!
|
42
|
+
reset
|
43
|
+
save!
|
44
|
+
end
|
45
|
+
|
46
|
+
def status
|
47
|
+
store["status"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def commit!(event)
|
51
|
+
store["status"] = event.destination if event.destination
|
52
|
+
store["state"].merge!(event.changes) unless event.changes.to_h.empty?
|
53
|
+
|
54
|
+
error = {}
|
55
|
+
error = event.error.to_h unless event.error.to_h.empty?
|
56
|
+
store["errors"][event.id] = error
|
57
|
+
|
58
|
+
data = {}
|
59
|
+
data = event.data.to_h unless event.data.to_h.empty?
|
60
|
+
store["events"][event.id] = data
|
61
|
+
save!
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_event(event_id)
|
65
|
+
return unless (found_event = store.dig("events", event_id))
|
66
|
+
Event.new(data: found_event, error: store.dig("errors", event_id))
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def valid_store?
|
72
|
+
(store.keys & %w(status events errors state)).size.eql?(4)
|
73
|
+
end
|
74
|
+
|
75
|
+
# FIXME: save! - configurable method
|
76
|
+
def save!
|
77
|
+
subject.send("#{field}=", store)
|
78
|
+
subject.save!
|
79
|
+
end
|
80
|
+
|
81
|
+
def reset
|
82
|
+
@store = subject.send(
|
83
|
+
"#{field}=",
|
84
|
+
{
|
85
|
+
"status" => nil,
|
86
|
+
"state" => {},
|
87
|
+
"events" => {},
|
88
|
+
"errors" => {}
|
89
|
+
}
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -19,7 +19,8 @@ module DirtyPipeline
|
|
19
19
|
storage.commit!(event)
|
20
20
|
|
21
21
|
# FIXME: make configurable, now - hardcoded to AR API
|
22
|
-
subject.transaction(requires_new: true) do
|
22
|
+
# subject.transaction(requires_new: true) do
|
23
|
+
subject.transaction do
|
23
24
|
with_abort_handling { yield(destination, action, *event.args) }
|
24
25
|
end
|
25
26
|
rescue => exception
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dirty_pipeline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Dolganov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pg
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,10 +158,14 @@ files:
|
|
144
158
|
- lib/dirty_pipeline/base.rb
|
145
159
|
- lib/dirty_pipeline/event.rb
|
146
160
|
- lib/dirty_pipeline/ext/camelcase.rb
|
147
|
-
- lib/dirty_pipeline/
|
148
|
-
- lib/dirty_pipeline/
|
161
|
+
- lib/dirty_pipeline/pg.rb
|
162
|
+
- lib/dirty_pipeline/pg/queue.rb
|
163
|
+
- lib/dirty_pipeline/pg/railway.rb
|
164
|
+
- lib/dirty_pipeline/pg/storage.rb
|
165
|
+
- lib/dirty_pipeline/redis/queue.rb
|
166
|
+
- lib/dirty_pipeline/redis/railway.rb
|
167
|
+
- lib/dirty_pipeline/redis/storage.rb
|
149
168
|
- lib/dirty_pipeline/status.rb
|
150
|
-
- lib/dirty_pipeline/storage.rb
|
151
169
|
- lib/dirty_pipeline/transaction.rb
|
152
170
|
- lib/dirty_pipeline/transition.rb
|
153
171
|
- lib/dirty_pipeline/version.rb
|
data/lib/dirty_pipeline/queue.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
module DirtyPipeline
|
2
|
-
class Queue
|
3
|
-
def initialize(operation, subject_class, subject_id, transaction_id)
|
4
|
-
@root = "dirty-pipeline-queue:#{subject_class}:#{subject_id}:" \
|
5
|
-
"op_#{operation}:txid_#{transaction_id}"
|
6
|
-
end
|
7
|
-
|
8
|
-
def clear!
|
9
|
-
DirtyPipeline.with_redis do |r|
|
10
|
-
r.del active_event_key
|
11
|
-
r.del events_queue_key
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def to_a
|
16
|
-
DirtyPipeline.with_redis do |r|
|
17
|
-
r.lrange(events_queue_key, 0, -1).map! do |packed_event|
|
18
|
-
unpack(packed_event)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def push(event)
|
24
|
-
DirtyPipeline.with_redis { |r| r.rpush(events_queue_key, pack(event)) }
|
25
|
-
self
|
26
|
-
end
|
27
|
-
alias :<< :push
|
28
|
-
|
29
|
-
def unshift(event)
|
30
|
-
DirtyPipeline.with_redis { |r| r.lpush(events_queue_key, pack(event)) }
|
31
|
-
self
|
32
|
-
end
|
33
|
-
|
34
|
-
def pop
|
35
|
-
DirtyPipeline.with_redis do |r|
|
36
|
-
data = r.lpop(events_queue_key)
|
37
|
-
data.nil? ? r.del(active_event_key) : r.set(active_event_key, data)
|
38
|
-
unpack(data)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def processing_event
|
43
|
-
DirtyPipeline.with_redis { |r| unpack(r.get(active_event_key)) }
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def pack(event)
|
49
|
-
JSON.dump(
|
50
|
-
"evid" => event.id,
|
51
|
-
"txid" => event.tx_id,
|
52
|
-
"transit" => event.transition,
|
53
|
-
"args" => event.args,
|
54
|
-
)
|
55
|
-
end
|
56
|
-
|
57
|
-
def unpack(packed_event)
|
58
|
-
return unless packed_event
|
59
|
-
unpacked_event = JSON.load(packed_event)
|
60
|
-
Event.new(
|
61
|
-
data: {
|
62
|
-
"uuid" => unpacked_event["evid"],
|
63
|
-
"transaction_uuid" => unpacked_event["txid"],
|
64
|
-
"transition" => unpacked_event["transit"],
|
65
|
-
"args" => unpacked_event["args"],
|
66
|
-
}
|
67
|
-
)
|
68
|
-
end
|
69
|
-
|
70
|
-
def events_queue_key
|
71
|
-
"#{@root}:events"
|
72
|
-
end
|
73
|
-
|
74
|
-
def active_event_key
|
75
|
-
"#{@root}:active"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
module DirtyPipeline
|
2
|
-
class Railway
|
3
|
-
DEFAULT_OPERATIONS = %w(call undo finalize)
|
4
|
-
|
5
|
-
def initialize(subject, transaction_id)
|
6
|
-
@tx_id = transaction_id
|
7
|
-
@subject_class = subject.class.to_s
|
8
|
-
@subject_id = subject.id.to_s
|
9
|
-
@root = "dirty-pipeline-rail:#{subject.class}:#{subject.id}:"
|
10
|
-
@queues = Hash[
|
11
|
-
DEFAULT_OPERATIONS.map do |operation|
|
12
|
-
[operation, create_queue(operation)]
|
13
|
-
end
|
14
|
-
]
|
15
|
-
end
|
16
|
-
|
17
|
-
def clear!
|
18
|
-
@queues.values.each(&:clear!)
|
19
|
-
DirtyPipeline.with_redis do |r|
|
20
|
-
r.multi do |mr|
|
21
|
-
mr.del(active_operation_key)
|
22
|
-
mr.del(active_transaction_key)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def next
|
28
|
-
return if other_transaction_in_progress?
|
29
|
-
start_transaction! unless running_transaction
|
30
|
-
|
31
|
-
queue.pop.tap { |event| finish_transaction! if event.nil? }
|
32
|
-
end
|
33
|
-
|
34
|
-
def queue(operation_name = active)
|
35
|
-
@queues.fetch(operation_name.to_s) do
|
36
|
-
@queues.store(operation_name, create_queue(operation_name))
|
37
|
-
end
|
38
|
-
end
|
39
|
-
alias :[] :queue
|
40
|
-
|
41
|
-
def switch_to(name)
|
42
|
-
raise ArgumentError unless DEFAULT_OPERATIONS.include?(name.to_s)
|
43
|
-
return if name.to_s == active
|
44
|
-
DirtyPipeline.with_redis { |r| r.set(active_operation_key, name) }
|
45
|
-
end
|
46
|
-
|
47
|
-
def active
|
48
|
-
DirtyPipeline.with_redis { |r| r.get(active_operation_key) }
|
49
|
-
end
|
50
|
-
alias :operation :active
|
51
|
-
|
52
|
-
def running_transaction
|
53
|
-
DirtyPipeline.with_redis { |r| r.get(active_transaction_key) }
|
54
|
-
end
|
55
|
-
|
56
|
-
def other_transaction_in_progress?
|
57
|
-
return false if running_transaction.nil?
|
58
|
-
running_transaction != @tx_id
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def create_queue(operation_name)
|
64
|
-
Queue.new(operation_name, @subject_class, @subject_id, @tx_id)
|
65
|
-
end
|
66
|
-
|
67
|
-
def active_transaction_key
|
68
|
-
"#{@root}:active_transaction"
|
69
|
-
end
|
70
|
-
|
71
|
-
def active_operation_key
|
72
|
-
"#{@root}:active_operation"
|
73
|
-
end
|
74
|
-
|
75
|
-
def start_transaction!
|
76
|
-
switch_to(DEFAULT_OPERATIONS.first) unless active
|
77
|
-
DirtyPipeline.with_redis { |r| r.set(active_transaction_key, @tx_id) }
|
78
|
-
end
|
79
|
-
|
80
|
-
def finish_transaction!
|
81
|
-
clear! if running_transaction == @tx_id
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
@@ -1,86 +0,0 @@
|
|
1
|
-
module DirtyPipeline
|
2
|
-
# Storage structure
|
3
|
-
# {
|
4
|
-
# status: :errored,
|
5
|
-
# state: {
|
6
|
-
# field: "value",
|
7
|
-
# },
|
8
|
-
# errors: {
|
9
|
-
# "<event_id>": {
|
10
|
-
# error: "RuPost::API::Error",
|
11
|
-
# error_message: "Timeout error",
|
12
|
-
# created_at: 2018-01-01T13:22Z
|
13
|
-
# },
|
14
|
-
# },
|
15
|
-
# events: {
|
16
|
-
# <event_id>: {
|
17
|
-
# transition: "Create",
|
18
|
-
# args: ...,
|
19
|
-
# changes: ...,
|
20
|
-
# created_at: ...,
|
21
|
-
# updated_at: ...,
|
22
|
-
# attempts_count: 2,
|
23
|
-
# },
|
24
|
-
# <event_id>: {...},
|
25
|
-
# }
|
26
|
-
# }
|
27
|
-
class Storage
|
28
|
-
class InvalidPipelineStorage < StandardError; end
|
29
|
-
|
30
|
-
attr_reader :subject, :field, :store
|
31
|
-
alias :to_h :store
|
32
|
-
def initialize(subject, field)
|
33
|
-
@subject = subject
|
34
|
-
@field = field
|
35
|
-
@store = subject.send(@field).to_h
|
36
|
-
reset if @store.empty?
|
37
|
-
raise InvalidPipelineStorage, store unless valid_store?
|
38
|
-
end
|
39
|
-
|
40
|
-
def reset!
|
41
|
-
reset
|
42
|
-
save!
|
43
|
-
end
|
44
|
-
|
45
|
-
def status
|
46
|
-
store["status"]
|
47
|
-
end
|
48
|
-
|
49
|
-
def commit!(event)
|
50
|
-
store["status"] = event.destination if event.destination
|
51
|
-
store["state"].merge!(event.changes) unless event.changes.to_h.empty?
|
52
|
-
store["errors"][event.id] = event.error unless event.error.to_h.empty?
|
53
|
-
store["events"][event.id] = event.data unless event.data.to_h.empty?
|
54
|
-
save!
|
55
|
-
end
|
56
|
-
|
57
|
-
def find_event(event_id)
|
58
|
-
return unless (found_event = store.dig("events", event_id))
|
59
|
-
Event.new(data: found_event, error: store.dig("errors", event_id))
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def valid_store?
|
65
|
-
(store.keys & %w(status events errors state)).size.eql?(4)
|
66
|
-
end
|
67
|
-
|
68
|
-
# FIXME: save! - configurable method
|
69
|
-
def save!
|
70
|
-
subject.send("#{field}=", store)
|
71
|
-
subject.save!
|
72
|
-
end
|
73
|
-
|
74
|
-
def reset
|
75
|
-
@store = subject.send(
|
76
|
-
"#{field}=",
|
77
|
-
{
|
78
|
-
"status" => nil,
|
79
|
-
"state" => {},
|
80
|
-
"events" => {},
|
81
|
-
"errors" => {}
|
82
|
-
}
|
83
|
-
)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|