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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1d848ae9c9559b571abc4a8561be9f7deb9d7aaa4977f5c5c7993fd698881cd
4
- data.tar.gz: 95d0fe52b224ea34310a102cf2dd1d31cafb298cb8c9a25065b9ee41f819a238
3
+ metadata.gz: 9f3fd5ee73f6187ea39daa3d7b88371c84f3c6ca848b7ba65f58b7c8876c55b5
4
+ data.tar.gz: ab18cf256c02e3851a87c093523bffe204bc25bba6a31be8027a923ddbebaa69
5
5
  SHA512:
6
- metadata.gz: ae206aaf6b7b7cda711467775e912dfa888f3024d4fde8e4516eab8c07a9f6d8d5e983867a415ddb9765506acb1e3a4d7cb64ec9af828bb54b2d31404c587280
7
- data.tar.gz: 64f3e7ca995413684d0b77676c178877b893419775433faf2e855c29538feff1833f39f26671d4b9577ef29e660a9c50417f597cdada71109e51bcce36cf9631
6
+ metadata.gz: a818876d704b6dbb40ec6a0bad6ac45fb38a40ad87fce41859ea5cc2a29ddb8d72067aefdb63d16d888c6cef0fcd840f3b65ca00b0fc810133b081ed10bd25f7
7
+ data.tar.gz: e6e014a6f8b37f55a688c5797c49287642531d4cc3dc2373688936b1e9bc0c2e7263947e9cbdcf6e9e152294cc0cf6c34f24e5835bad782c5bd497c7960f897e
@@ -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"
@@ -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
- require_relative "dirty_pipeline/queue.rb"
12
- require_relative "dirty_pipeline/railway.rb"
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
@@ -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,12 @@
1
+ module DirtyPipeline
2
+ module PG
3
+ module_function
4
+ def multi(pg_result)
5
+ pg_result.first&.values
6
+ end
7
+
8
+ def single(pg_result)
9
+ pg_result.first&.values&.first
10
+ end
11
+ end
12
+ 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
@@ -1,3 +1,3 @@
1
1
  module DirtyPipeline
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.1"
3
3
  end
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.7.1
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-08 00:00:00.000000000 Z
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/queue.rb
148
- - lib/dirty_pipeline/railway.rb
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
@@ -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