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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a3300767c58d38551a2844322cece40fcfdbd2df
4
- data.tar.gz: 0c7d4bae4ef6276563e64e15d0a2f7a05cc6dff4
3
+ metadata.gz: b9efdd612a791bf02b6a1f3f809ffffca7c7841b
4
+ data.tar.gz: 8596080a556b2d9639534923c00589cb55cc9235
5
5
  SHA512:
6
- metadata.gz: 4a9dd96e797d670fc8b11e53202f12a786258a9e266cd6b988d7568a3a297a4ae552ce2a66d79170a1bc7156ff10f934317e6c760c1f805001edb7153693138e
7
- data.tar.gz: fd00e8748edfe21a30fcf6f548c499d8b0d071e15ed46dea43002c1413dc1dd9d51f6c61ad5e3e696713c09caae73df3252030915795b2a8018988bb782477f4
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
- @@last_id[id] ||= Projection.find(id).last_id
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
- broken_projections = []
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
- @@broken[id] ||= Projection.find(id).solid
24
+ Projection.find(id).solid
32
25
  end
33
26
 
34
- def self.create_or_get(projection_class)
35
- projection = Projection.where(class_name: projection_class).first
36
- if projection.nil?
37
- projection = Projection.create! class_name: projection_class, last_id: 0, solid: true
38
- else
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 = options
9
- instance.start
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
- event_connection.start
14
- sync_projections
15
- listen_for_events
16
- request_missing_events
17
- event_channel.work_pool.join
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 e
32
+ raise
24
33
  end
25
34
 
26
- def sync_projections
27
- ProjectionTypeRegistry.sync_projections
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 listen_for_events
31
- subscribe_to event_queue do |delivery_info, properties, body|
32
- event_received properties, body
33
- send_browser_notification properties.headers[:id]
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 request_missing_events
38
- listen_for_replayed_events
39
- send_request_for min_last_id
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
- subscribe_to replay_queue do |delivery_info, properties, body|
44
- event_received properties, body unless replay_done? body
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 send_browser_notification(id)
49
- server_side_events_exchange.publish id.to_s
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?(body)
57
- if 'replay_done' == body
58
- LOGGER.debug 'All replayed events received'
59
- broken_projections = ProjectionRepository.get_all_broken
60
- LOGGER.error "These projections are still broken: #{broken_projections.join(", ")}" unless broken_projections.empty?
61
- replay_queue.unbind(resend_exchange)
62
- return true
63
- end
64
- false
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
- ProjectionTypeRegistry.process properties.headers.deep_symbolize_keys!, build_event(properties.type, JSON.parse(body))
71
- end
72
-
73
- def build_event(type, data)
74
- Object.const_get(type).new(data.deep_symbolize_keys)
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
- if solid?
58
- update_last_id event_id
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.create_or_get(self.class.name).id
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
- def self.process(headers, event)
12
- instance.process headers, event
15
+ # @return an enumerable with all projections
16
+ def self.projections
17
+ instance.projections.each
13
18
  end
14
19
 
15
- def process(headers, event)
16
- projections.each do |projection|
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 :registry
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
@@ -12,7 +12,6 @@ module ActiveProjection
12
12
  end
13
13
 
14
14
  def run
15
- ActiveRecord::Base.establish_connection options[:projection_database]
16
15
  EventClient.start options
17
16
  end
18
17
 
@@ -1,7 +1,7 @@
1
1
  module ActiveProjection
2
2
  # Returns the version of the currently loaded ActiveProjection as a Gem::Version
3
3
  def self.version
4
- Gem::Version.new '0.5.0.rc3'
4
+ Gem::Version.new '0.5.0.rc4'
5
5
  end
6
6
 
7
7
  module VERSION #:nodoc:
@@ -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(:create_or_get).with('TestProjection')
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 'create_or_get' do
10
+ describe 'ensure_exists' do
11
11
  it 'creates a projection, if classname is not present' do
12
- expect(ActiveProjection::ProjectionRepository.create_or_get('TestProjection5').id).to eq 5
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.create_or_get('TestProjection3').id).to eq 3
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.rc3
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-03-24 00:00:00.000000000 Z
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.rc3
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.rc3
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