active_projection 0.5.2 → 0.5.3

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
  SHA1:
3
- metadata.gz: 0250b8914b32ef0061eaf8a9f32683d12182e536
4
- data.tar.gz: 21ed583c9e15ae256996bacd4230c9ae4b3f5d15
3
+ metadata.gz: b77647433fbea824ae8677e63891935e731f517c
4
+ data.tar.gz: 739f2e01e001a89ec1ecffbd96be77c9067b45ec
5
5
  SHA512:
6
- metadata.gz: 7b107890a963279d3d1884a5b0bbe109240dc1f1fcda356cd13d401e311beacb62c41cd1e5ae4914e12fc6f127d6e3c8fe59ecf762700f6dacf961bf17016c09
7
- data.tar.gz: a6b4677d68994653d8ba0839e59e315dcfd660f70cecb2619ed7ee418ffdfd0e732302082923b49b44f6baab12da253bcdda07103a4293bd43b932283ed4604c
6
+ metadata.gz: 4f3712e937fc43d98700dd7e14fd713b34c2e27081de26dbb16e0a9818c1f2341ebe86275d0c7d3be2a1602e6d210c126845f70d0d931fd8a385acd1fd764de1
7
+ data.tar.gz: e64c810df7a0c3668b6000e61c385ba465dd94241540aed0f42cb108a3bf0cfe70ffcccd55db95fd353857cd058a4c18477ef676918870adcd758590a489a7fc
@@ -1,20 +1,20 @@
1
- Copyright (c) 2013 Andreas Reischuck
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
1
+ Copyright (c) 2013 Andreas Reischuck
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # Active Projection
2
-
3
- Contains everything necessary to build and run projection servers for Rails Disco.
4
-
5
- Projections process events into database models that are optimized for the Rails views.
6
-
7
- Have a look at the [rails-disco](https://github.com/hicknhack-software/rails-disco/wiki) documentation on Github for more details.
1
+ # Active Projection
2
+
3
+ Contains everything necessary to build and run projection servers for Rails Disco.
4
+
5
+ Projections process events into database models that are optimized for the Rails views.
6
+
7
+ Have a look at the [rails-disco](https://github.com/hicknhack-software/rails-disco/wiki) documentation on Github for more details.
@@ -8,7 +8,7 @@ module ActiveProjection
8
8
  projections.values.map(&:last_id)
9
9
  end
10
10
 
11
- def self.get_last_id(id)
11
+ def self.last_id(id)
12
12
  projections[id].last_id
13
13
  end
14
14
 
@@ -16,11 +16,11 @@ module ActiveProjection
16
16
  projections[id].update! last_id: last_id
17
17
  end
18
18
 
19
- def self.set_broken(id)
19
+ def self.mark_broken(id)
20
20
  projections[id].update! solid: false
21
21
  end
22
22
 
23
- def self.get_all_broken
23
+ def self.all_broken
24
24
  projections.values.reject(&:solid).map(&:class_name)
25
25
  end
26
26
 
@@ -45,7 +45,7 @@ module ActiveProjection
45
45
 
46
46
  private
47
47
 
48
- def self.initialize
48
+ def initialize
49
49
  end
50
50
 
51
51
  cattr_accessor :projections do
@@ -1,5 +1,5 @@
1
- module ActiveProjection
2
- class Projection < ActiveRecord::Base
3
- self.table_name = 'projections'
4
- end
5
- end
1
+ module ActiveProjection
2
+ class Projection < ActiveRecord::Base
3
+ self.table_name = 'projections'
4
+ end
5
+ end
@@ -1,45 +1,45 @@
1
- module ActiveProjection
2
- class ProjectionRepository
3
- def self.last_ids
4
- Projection.all.to_a.map { |p| p.last_id }
5
- end
6
-
7
- def self.get_last_id(id)
8
- Projection.find(id).last_id
9
- end
10
-
11
- def self.set_last_id(id, last_id)
12
- Projection.find(id).update! last_id: last_id
13
- end
14
-
15
- def self.set_broken(id)
16
- Projection.find(id).update! solid: false
17
- end
18
-
19
- def self.get_all_broken
20
- Projection.where(solid: false).to_a.map { |p| p.class_name }
21
- end
22
-
23
- def self.solid?(id)
24
- Projection.find(id).solid
25
- end
26
-
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
36
- projection.update! solid: true
37
- end
38
- end
39
-
40
- private
41
-
42
- def self.initialize
43
- end
44
- end
45
- end
1
+ module ActiveProjection
2
+ class ProjectionRepository
3
+ def self.last_ids
4
+ Projection.all.to_a.map { |p| p.last_id }
5
+ end
6
+
7
+ def self.last_id(id)
8
+ Projection.find(id).last_id
9
+ end
10
+
11
+ def self.set_last_id(id, last_id)
12
+ Projection.find(id).update! last_id: last_id
13
+ end
14
+
15
+ def self.mark_broken(id)
16
+ Projection.find(id).update! solid: false
17
+ end
18
+
19
+ def self.all_broken
20
+ Projection.where(solid: false).to_a.map { |p| p.class_name }
21
+ end
22
+
23
+ def self.solid?(id)
24
+ Projection.find(id).solid
25
+ end
26
+
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
36
+ projection.update! solid: true
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def initialize
43
+ end
44
+ end
45
+ end
@@ -1,10 +1,10 @@
1
- class CreateProjections < ActiveRecord::Migration
2
- def change
3
- create_table :projections do |t|
4
- t.string :class_name
5
- t.integer :last_id, default: 0
6
- t.boolean :solid, default: true
7
- end
8
- add_index :projections, :class_name
9
- end
10
- end
1
+ class CreateProjections < ActiveRecord::Migration
2
+ def change
3
+ create_table :projections do |t|
4
+ t.string :class_name
5
+ t.integer :last_id, default: 0
6
+ t.boolean :solid, default: true
7
+ end
8
+ add_index :projections, :class_name
9
+ end
10
+ end
@@ -1,17 +1,17 @@
1
- require 'active_event'
2
- require 'active_support/core_ext'
3
- require 'active_projection/version'
4
-
5
- module ActiveProjection
6
- extend ActiveSupport::Autoload
7
-
8
- autoload :Autoload
9
- autoload :EventClient
10
- autoload :ProjectionType
11
- autoload :ProjectionTypeRegistry
12
- autoload :Server
13
-
14
- autoload :Projection, (File.expand_path '../../app/models/active_projection/projection', __FILE__)
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__)
17
- end
1
+ require 'active_event'
2
+ require 'active_support/core_ext'
3
+ require 'active_projection/version'
4
+
5
+ module ActiveProjection
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Autoload
9
+ autoload :EventClient
10
+ autoload :ProjectionType
11
+ autoload :ProjectionTypeRegistry
12
+ autoload :Server
13
+
14
+ autoload :Projection, (File.expand_path '../../app/models/active_projection/projection', __FILE__)
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__)
17
+ end
@@ -1,9 +1,11 @@
1
- module ActiveProjection
2
- module Autoload
3
- include ActiveEvent::Support::Autoload
4
- private
5
- def self.dir_names
6
- %W(app/models app/projections)
7
- end
8
- end
9
- end
1
+ module ActiveProjection
2
+ module Autoload
3
+ include ActiveEvent::Support::Autoload
4
+
5
+ private
6
+
7
+ def self.dir_names
8
+ %w(app/models app/projections)
9
+ end
10
+ end
11
+ end
@@ -1,171 +1,171 @@
1
- require 'singleton'
2
- require 'bunny'
3
-
4
- module ActiveProjection
5
- class EventClient
6
- include Singleton
7
-
8
- def self.start(options)
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
16
- end
17
-
18
- def start
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
27
- rescue Interrupt
28
- LOGGER.info 'Catching Interrupt'
29
- rescue Exception => e
30
- LOGGER.error e.message
31
- LOGGER.error e.backtrace.join("\n")
32
- raise
33
- end
34
-
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
48
- end
49
-
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)
63
- end
64
- end
65
-
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
78
- end
79
-
80
- def listen_for_replayed_events
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
87
- end
88
- end
89
-
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.class.name}: #{error.message}", backtrace: error.backtrace if error
97
- server_side_events_exchange.publish message.to_json
98
- end
99
-
100
- def send_request_for(id)
101
- resend_request_exchange.publish id.to_s, routing_key: 'resend_request'
102
- end
103
-
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 = []
116
- end
117
-
118
- def event_received(properties, body)
119
- RELOADER.execute_if_updated
120
- LOGGER.debug "Received #{properties.type} with #{body}"
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
137
- end
138
-
139
- def replay_queue
140
- @replay_queue ||= event_channel.queue('', auto_delete: true).bind(resend_exchange)
141
- end
142
-
143
- def event_queue
144
- @event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
145
- end
146
-
147
- def event_connection
148
- @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
149
- end
150
-
151
- def event_channel
152
- @event_channel ||= event_connection.create_channel
153
- end
154
-
155
- def event_exchange
156
- @event_exchange ||= event_channel.fanout options[:event_exchange]
157
- end
158
-
159
- def resend_exchange
160
- @resend_exchange ||= event_channel.fanout "resend_#{options[:event_exchange]}"
161
- end
162
-
163
- def resend_request_exchange
164
- @resend_request_exchange ||= event_channel.direct "resend_request_#{options[:event_exchange]}"
165
- end
166
-
167
- def server_side_events_exchange
168
- @server_side_events_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
169
- end
170
- end
171
- end
1
+ require 'singleton'
2
+ require 'bunny'
3
+
4
+ module ActiveProjection
5
+ class EventClient
6
+ include Singleton
7
+
8
+ def self.start(options)
9
+ instance.configure(options).start
10
+ end
11
+
12
+ def configure(options)
13
+ fail 'Unsupported! Cannot configure running client' if running
14
+ self.options = options
15
+ self
16
+ end
17
+
18
+ def start
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
27
+ rescue Interrupt
28
+ LOGGER.info 'Catching Interrupt'
29
+ rescue => e
30
+ LOGGER.error e.message
31
+ LOGGER.error e.backtrace.join("\n")
32
+ raise
33
+ end
34
+
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
+ fail 'Unsupported! Connot start a running client' if running
44
+ self.running = true
45
+ yield
46
+ ensure
47
+ self.running = false
48
+ end
49
+
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)
63
+ end
64
+ end
65
+
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
78
+ end
79
+
80
+ def listen_for_replayed_events
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
87
+ end
88
+ end
89
+
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.class.name}: #{error.message}", backtrace: error.backtrace if error
97
+ server_side_events_exchange.publish message.to_json
98
+ end
99
+
100
+ def send_request_for(id)
101
+ resend_request_exchange.publish id.to_s, routing_key: 'resend_request'
102
+ end
103
+
104
+ def replay_done
105
+ LOGGER.debug 'All replayed events received'
106
+ broken_projections = CachedProjectionRepository.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 = []
116
+ end
117
+
118
+ def event_received(properties, body)
119
+ RELOADER.execute_if_updated
120
+ LOGGER.debug "Received #{properties.type} with #{body}"
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 => e
134
+ send_projection_notification headers[:id], projection, e
135
+ end
136
+ end
137
+ end
138
+
139
+ def replay_queue
140
+ @replay_queue ||= event_channel.queue('', auto_delete: true).bind(resend_exchange)
141
+ end
142
+
143
+ def event_queue
144
+ @event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
145
+ end
146
+
147
+ def event_connection
148
+ @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
149
+ end
150
+
151
+ def event_channel
152
+ @event_channel ||= event_connection.create_channel
153
+ end
154
+
155
+ def event_exchange
156
+ @event_exchange ||= event_channel.fanout options[:event_exchange]
157
+ end
158
+
159
+ def resend_exchange
160
+ @resend_exchange ||= event_channel.fanout "resend_#{options[:event_exchange]}"
161
+ end
162
+
163
+ def resend_request_exchange
164
+ @resend_request_exchange ||= event_channel.direct "resend_request_#{options[:event_exchange]}"
165
+ end
166
+
167
+ def server_side_events_exchange
168
+ @server_side_events_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
169
+ end
170
+ end
171
+ end