active_projection 0.5.0.rc3 → 0.5.0.rc4
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/app/models/active_projection/cached_projection_repository.rb +55 -0
- data/app/models/active_projection/projection_repository.rb +14 -20
- data/db/migrate/01_create_projections.rb +2 -2
- data/lib/active_projection/autoload.rb +2 -35
- data/lib/active_projection/event_client.rb +98 -50
- data/lib/active_projection/projection_type.rb +4 -5
- data/lib/active_projection/projection_type_registry.rb +11 -31
- data/lib/active_projection/server.rb +0 -1
- data/lib/active_projection/version.rb +1 -1
- data/lib/active_projection.rb +1 -0
- data/spec/lib/projection_type_registry_spec.rb +1 -1
- data/spec/models/projection_repository_spec.rb +3 -3
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9efdd612a791bf02b6a1f3f809ffffca7c7841b
|
4
|
+
data.tar.gz: 8596080a556b2d9639534923c00589cb55cc9235
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11fe3536a9b6318bbd5a34970cf2a949c5c0c9ab13f5c0e2436c03d19e1183289737567de9b4c96dcbddd026c0b69710a2ff9321e126eb3ac4ba64c5c9954629
|
7
|
+
data.tar.gz: f32df5da5a87329fe6e5c47a9027788460bf28c56b78245eb0bd9fc7843ac26c86d3dbcdaeb474dbba7807b036f98cc23a88c391660ed0d3a0c7b743cd64569e
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ActiveProjection
|
2
|
+
#
|
3
|
+
# This implements a fully cached projection repository.
|
4
|
+
#
|
5
|
+
# notice: NOT thread safe!
|
6
|
+
class CachedProjectionRepository
|
7
|
+
def self.last_ids
|
8
|
+
projections.values.map(&:last_id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.get_last_id(id)
|
12
|
+
projections[id].last_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.set_last_id(id, last_id)
|
16
|
+
projections[id].update! last_id: last_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.set_broken(id)
|
20
|
+
projections[id].update! solid: false
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.get_all_broken
|
24
|
+
projections.values.reject(&:solid).map(&:class_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.solid?(id)
|
28
|
+
projections[id].solid
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.ensure_exists(projection_class)
|
32
|
+
Projection.transaction do
|
33
|
+
projection = Projection.find_or_create_by! class_name: projection_class
|
34
|
+
projections[projection.id] = projection
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.ensure_solid(projection_class)
|
39
|
+
Projection.transaction do
|
40
|
+
projection = Projection.find_or_initialize_by class_name: projection_class
|
41
|
+
projection.update! solid: true
|
42
|
+
projections[projection.id] = projection
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def self.initialize
|
49
|
+
end
|
50
|
+
|
51
|
+
cattr_accessor :projections do
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,51 +1,45 @@
|
|
1
1
|
module ActiveProjection
|
2
2
|
class ProjectionRepository
|
3
|
-
|
4
3
|
def self.last_ids
|
5
4
|
Projection.all.to_a.map { |p| p.last_id }
|
6
5
|
end
|
7
6
|
|
8
7
|
def self.get_last_id(id)
|
9
|
-
|
8
|
+
Projection.find(id).last_id
|
10
9
|
end
|
11
10
|
|
12
11
|
def self.set_last_id(id, last_id)
|
13
12
|
Projection.find(id).update! last_id: last_id
|
14
|
-
@@last_id[id] += 1
|
15
13
|
end
|
16
14
|
|
17
15
|
def self.set_broken(id)
|
18
|
-
@@broken[id] = false
|
19
16
|
Projection.find(id).update! solid: false
|
20
17
|
end
|
21
18
|
|
22
19
|
def self.get_all_broken
|
23
|
-
|
24
|
-
Projection.all.to_a.each do |p|
|
25
|
-
broken_projections << p.class_name unless p.solid?
|
26
|
-
end
|
27
|
-
broken_projections
|
20
|
+
Projection.where(solid: false).to_a.map { |p| p.class_name }
|
28
21
|
end
|
29
22
|
|
30
23
|
def self.solid?(id)
|
31
|
-
|
24
|
+
Projection.find(id).solid
|
32
25
|
end
|
33
26
|
|
34
|
-
def self.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
27
|
+
def self.ensure_exists(projection_class)
|
28
|
+
Projection.transaction do
|
29
|
+
Projection.find_or_create_by! class_name: projection_class
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.ensure_solid(projection_class)
|
34
|
+
Projection.transaction do
|
35
|
+
projection = Projection.find_or_initialize_by class_name: projection_class
|
39
36
|
projection.update! solid: true
|
40
37
|
end
|
41
|
-
projection
|
42
38
|
end
|
43
39
|
|
44
40
|
private
|
41
|
+
|
45
42
|
def self.initialize
|
46
43
|
end
|
47
|
-
|
48
|
-
@@last_id = {}
|
49
|
-
@@broken = {}
|
50
44
|
end
|
51
|
-
end
|
45
|
+
end
|
@@ -2,8 +2,8 @@ class CreateProjections < ActiveRecord::Migration
|
|
2
2
|
def change
|
3
3
|
create_table :projections do |t|
|
4
4
|
t.string :class_name
|
5
|
-
t.integer :last_id
|
6
|
-
t.boolean :solid
|
5
|
+
t.integer :last_id, default: 0
|
6
|
+
t.boolean :solid, default: true
|
7
7
|
end
|
8
8
|
add_index :projections, :class_name
|
9
9
|
end
|
@@ -3,40 +3,7 @@ module ActiveProjection
|
|
3
3
|
include ActiveEvent::Support::Autoload
|
4
4
|
private
|
5
5
|
def self.dir_names
|
6
|
-
%W(app/models)
|
7
|
-
end
|
8
|
-
|
9
|
-
def self.reload
|
10
|
-
ActiveEvent::Support::Autoloader.reload_from pdirs
|
11
|
-
super
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.worker_config=(config)
|
15
|
-
set_pdirs config
|
16
|
-
ActiveEvent::Support::Autoloader.load_from pdirs
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.watchable_dirs
|
20
|
-
watchable_dirs = super
|
21
|
-
watchable_dirs['app/projections'] = [:rb]
|
22
|
-
watchable_dirs
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
def self.set_pdirs(config)
|
27
|
-
@pdirs = []
|
28
|
-
projections_dir = "#{config[:path]}/app/projections/**/*.rb"
|
29
|
-
if config[:count] == 1
|
30
|
-
@pdirs << projections_dir
|
31
|
-
else
|
32
|
-
Dir[*projections_dir].each_with_index.map { |item, i| i % config[:count] == config[:number] ? item : nil }.compact.each do |dir|
|
33
|
-
@pdirs << dir
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.pdirs
|
39
|
-
@pdirs ||= []
|
6
|
+
%W(app/models app/projections)
|
40
7
|
end
|
41
8
|
end
|
42
|
-
end
|
9
|
+
end
|
@@ -1,77 +1,139 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
require 'bunny'
|
3
|
+
|
3
4
|
module ActiveProjection
|
4
5
|
class EventClient
|
5
6
|
include Singleton
|
6
7
|
|
7
8
|
def self.start(options)
|
8
|
-
instance.options
|
9
|
-
|
9
|
+
instance.configure(options).start
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure(options)
|
13
|
+
raise 'Unsupported! Cannot configure running client' if running
|
14
|
+
self.options = options
|
15
|
+
self
|
10
16
|
end
|
11
17
|
|
12
18
|
def start
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
run_once do
|
20
|
+
prepare
|
21
|
+
sync_projections
|
22
|
+
listen_for_events
|
23
|
+
listen_for_replayed_events
|
24
|
+
request_missing_events
|
25
|
+
event_channel.work_pool.join
|
26
|
+
end
|
18
27
|
rescue Interrupt
|
19
28
|
LOGGER.info 'Catching Interrupt'
|
20
29
|
rescue Exception => e
|
21
30
|
LOGGER.error e.message
|
22
31
|
LOGGER.error e.backtrace.join("\n")
|
23
|
-
raise
|
32
|
+
raise
|
24
33
|
end
|
25
34
|
|
26
|
-
|
27
|
-
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :options
|
38
|
+
attr_accessor :running # true once start was called
|
39
|
+
attr_accessor :current # true after missing events are processed
|
40
|
+
attr_accessor :delay_queue # stores events while processing missing events
|
41
|
+
|
42
|
+
def run_once
|
43
|
+
raise 'Unsupported! Connot start a running client' if running
|
44
|
+
self.running = true
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
self.running = false
|
28
48
|
end
|
29
49
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
50
|
+
def prepare
|
51
|
+
self.delay_queue = []
|
52
|
+
init_database_connection
|
53
|
+
event_connection.start
|
54
|
+
end
|
55
|
+
|
56
|
+
def init_database_connection
|
57
|
+
ActiveRecord::Base.establish_connection options[:projection_database]
|
58
|
+
end
|
59
|
+
|
60
|
+
def sync_projections
|
61
|
+
server_projections.each do |projection|
|
62
|
+
CachedProjectionRepository.ensure_solid(projection.class.name)
|
34
63
|
end
|
35
64
|
end
|
36
65
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
66
|
+
def server_projections
|
67
|
+
@server_projections ||= ProjectionTypeRegistry.projections.drop(WORKER_NUMBER).each_slice(WORKER_COUNT).map(&:first)
|
68
|
+
end
|
69
|
+
|
70
|
+
def listen_for_events
|
71
|
+
event_queue.subscribe do |delivery_info, properties, body|
|
72
|
+
if current
|
73
|
+
event_received properties, body
|
74
|
+
else
|
75
|
+
delay_queue << [properties, body]
|
76
|
+
end
|
77
|
+
end
|
40
78
|
end
|
41
79
|
|
42
80
|
def listen_for_replayed_events
|
43
|
-
|
44
|
-
|
81
|
+
replay_queue.subscribe do |delivery_info, properties, body|
|
82
|
+
if 'replay_done' == body
|
83
|
+
replay_done
|
84
|
+
else
|
85
|
+
event_received properties, body
|
86
|
+
end
|
45
87
|
end
|
46
88
|
end
|
47
89
|
|
48
|
-
def
|
49
|
-
|
90
|
+
def request_missing_events
|
91
|
+
send_request_for(CachedProjectionRepository.last_ids.min || 0)
|
92
|
+
end
|
93
|
+
|
94
|
+
def send_projection_notification(event_id, projection, error = nil)
|
95
|
+
message = {event: event_id, projection: projection.class.name}
|
96
|
+
message.merge error: error.message, backtrace: error.backtrace if error
|
97
|
+
server_side_events_exchange.publish message.to_json
|
50
98
|
end
|
51
99
|
|
52
100
|
def send_request_for(id)
|
53
101
|
resend_request_exchange.publish id.to_s, routing_key: 'resend_request'
|
54
102
|
end
|
55
103
|
|
56
|
-
def replay_done
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
104
|
+
def replay_done
|
105
|
+
LOGGER.debug 'All replayed events received'
|
106
|
+
broken_projections = CachedProjectionRepository.get_all_broken
|
107
|
+
LOGGER.error "These projections are still broken: #{broken_projections.join(", ")}" unless broken_projections.empty?
|
108
|
+
replay_queue.unbind(resend_exchange)
|
109
|
+
self.current = true
|
110
|
+
flush_delay_queue
|
111
|
+
end
|
112
|
+
|
113
|
+
def flush_delay_queue
|
114
|
+
delay_queue.each { |properties, body| event_received properties, body }
|
115
|
+
self.delay_queue = []
|
65
116
|
end
|
66
117
|
|
67
118
|
def event_received(properties, body)
|
68
119
|
RELOADER.execute_if_updated
|
69
120
|
LOGGER.debug "Received #{properties.type} with #{body}"
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
121
|
+
headers = properties.headers.deep_symbolize_keys!
|
122
|
+
event = ActiveEvent::EventType.create_instance properties.type, JSON.parse(body).deep_symbolize_keys!
|
123
|
+
process_event headers, event
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_event(headers, event)
|
127
|
+
server_projections.select { |p| p.evaluate headers }.each do |projection|
|
128
|
+
begin
|
129
|
+
ActiveRecord::Base.transaction do
|
130
|
+
projection.invoke event, headers
|
131
|
+
end
|
132
|
+
send_projection_notification headers[:id], projection
|
133
|
+
rescue Exception => e
|
134
|
+
send_projection_notification headers[:id], projection, e
|
135
|
+
end
|
136
|
+
end
|
75
137
|
end
|
76
138
|
|
77
139
|
def replay_queue
|
@@ -82,14 +144,6 @@ module ActiveProjection
|
|
82
144
|
@event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
|
83
145
|
end
|
84
146
|
|
85
|
-
def min_last_id
|
86
|
-
ProjectionRepository.last_ids.min || 0
|
87
|
-
end
|
88
|
-
|
89
|
-
def subscribe_to(queue, &block)
|
90
|
-
queue.subscribe(&block)
|
91
|
-
end
|
92
|
-
|
93
147
|
def event_connection
|
94
148
|
@event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
|
95
149
|
end
|
@@ -113,11 +167,5 @@ module ActiveProjection
|
|
113
167
|
def server_side_events_exchange
|
114
168
|
@server_side_events_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
|
115
169
|
end
|
116
|
-
|
117
|
-
def options
|
118
|
-
@options
|
119
|
-
end
|
120
|
-
|
121
|
-
attr_writer :options
|
122
170
|
end
|
123
|
-
end
|
171
|
+
end
|
@@ -52,12 +52,11 @@ module ActiveProjection
|
|
52
52
|
rescue Exception => e
|
53
53
|
LOGGER.error "[#{self.class.name}]: error processing #{event_type}[#{event_id}]\n#{e.message}\n#{e.backtrace}"
|
54
54
|
set_broken
|
55
|
+
raise
|
55
56
|
end
|
56
57
|
end
|
57
|
-
|
58
|
-
|
59
|
-
LOGGER.debug "[#{self.class.name}]: sucessfully processed #{event_type}[#{event_id}]"
|
60
|
-
end
|
58
|
+
update_last_id event_id
|
59
|
+
LOGGER.debug "[#{self.class.name}]: sucessfully processed #{event_type}[#{event_id}]"
|
61
60
|
end
|
62
61
|
|
63
62
|
private
|
@@ -69,7 +68,7 @@ module ActiveProjection
|
|
69
68
|
end
|
70
69
|
|
71
70
|
def projection_id
|
72
|
-
@projection_id ||= ProjectionRepository.
|
71
|
+
@projection_id ||= ProjectionRepository.ensure_exists(self.class.name).id
|
73
72
|
end
|
74
73
|
|
75
74
|
def solid?
|
@@ -4,45 +4,25 @@ module ActiveProjection
|
|
4
4
|
class ProjectionTypeRegistry
|
5
5
|
include Singleton
|
6
6
|
|
7
|
+
# register a new projection class
|
8
|
+
#
|
9
|
+
# The best way to create a new projection is using the ProjectionType module
|
10
|
+
# This module automatically registers each class
|
7
11
|
def self.register(projection)
|
8
12
|
self.registry << projection
|
9
13
|
end
|
10
14
|
|
11
|
-
|
12
|
-
|
15
|
+
# @return an enumerable with all projections
|
16
|
+
def self.projections
|
17
|
+
instance.projections.each
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
projections.
|
17
|
-
ActiveRecord::Base.transaction do
|
18
|
-
projection.invoke(event, headers) if projection.evaluate headers
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.sync_projections
|
24
|
-
instance.sync_projections
|
25
|
-
end
|
26
|
-
|
27
|
-
def sync_projections
|
28
|
-
projections.each do |projection|
|
29
|
-
ProjectionRepository.create_or_get(projection.class.name)
|
30
|
-
end
|
20
|
+
def projections
|
21
|
+
@projections ||= self.class.registry.freeze.map { |projection_class| projection_class.new }.freeze
|
31
22
|
end
|
32
23
|
|
33
24
|
private
|
34
25
|
|
35
|
-
cattr_accessor
|
36
|
-
attr_accessor :projections
|
37
|
-
|
38
|
-
def initialize
|
39
|
-
self.projections = []
|
40
|
-
self.class.registry.freeze.each do |projection|
|
41
|
-
projections << projection.new
|
42
|
-
end
|
43
|
-
projections.freeze
|
44
|
-
end
|
45
|
-
|
46
|
-
self.registry = []
|
26
|
+
cattr_accessor(:registry) { [] }
|
47
27
|
end
|
48
|
-
end
|
28
|
+
end
|
data/lib/active_projection.rb
CHANGED
@@ -13,4 +13,5 @@ module ActiveProjection
|
|
13
13
|
|
14
14
|
autoload :Projection, (File.expand_path '../../app/models/active_projection/projection', __FILE__)
|
15
15
|
autoload :ProjectionRepository, (File.expand_path '../../app/models/active_projection/projection_repository', __FILE__)
|
16
|
+
autoload :CachedProjectionRepository, (File.expand_path '../../app/models/active_projection/cached_projection_repository', __FILE__)
|
16
17
|
end
|
@@ -22,7 +22,7 @@ describe ActiveProjection::ProjectionTypeRegistry do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'synchronizes the known projections with the db' do
|
25
|
-
expect(ActiveProjection::ProjectionRepository).to receive(:
|
25
|
+
expect(ActiveProjection::ProjectionRepository).to receive(:ensure_exists).with('TestProjection')
|
26
26
|
ActiveProjection::ProjectionTypeRegistry.sync_projections
|
27
27
|
end
|
28
28
|
end
|
@@ -7,14 +7,14 @@ describe ActiveProjection::ProjectionRepository do
|
|
7
7
|
FactoryGirl.create :projection, id: i, class_name: "TestProjection#{i}", last_id: 5 - i
|
8
8
|
end
|
9
9
|
end
|
10
|
-
describe '
|
10
|
+
describe 'ensure_exists' do
|
11
11
|
it 'creates a projection, if classname is not present' do
|
12
|
-
expect(ActiveProjection::ProjectionRepository.
|
12
|
+
expect(ActiveProjection::ProjectionRepository.ensure_exists('TestProjection5').id).to eq 5
|
13
13
|
expect(ActiveProjection::Projection.all.to_a.length).to eq 6
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'does not create a projection, if classname is present' do
|
17
|
-
expect(ActiveProjection::ProjectionRepository.
|
17
|
+
expect(ActiveProjection::ProjectionRepository.ensure_exists('TestProjection3').id).to eq 3
|
18
18
|
expect(ActiveProjection::Projection.all.to_a.length).to eq 5
|
19
19
|
end
|
20
20
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_projection
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.0.
|
4
|
+
version: 0.5.0.rc4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- HicknHack Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: active_event
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.5.0.
|
19
|
+
version: 0.5.0.rc4
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.5.0.
|
26
|
+
version: 0.5.0.rc4
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -159,6 +159,7 @@ executables: []
|
|
159
159
|
extensions: []
|
160
160
|
extra_rdoc_files: []
|
161
161
|
files:
|
162
|
+
- app/models/active_projection/cached_projection_repository.rb
|
162
163
|
- app/models/active_projection/projection.rb
|
163
164
|
- app/models/active_projection/projection_repository.rb
|
164
165
|
- db/migrate/01_create_projections.rb
|