dirty_pipeline 0.7.1 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|