entity_store 0.0.15 → 0.1.0
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.
- data/lib/entity_store/entity.rb +2 -0
- data/lib/entity_store/event_bus.rb +51 -22
- data/lib/entity_store/event_data_object.rb +1 -1
- data/lib/entity_store/external_store.rb +16 -6
- data/lib/entity_store/mongo_entity_store.rb +3 -3
- data/lib/entity_store/store.rb +9 -4
- data/lib/entity_store/version.rb +1 -1
- data/lib/entity_store.rb +2 -21
- data/spec/entity_store/event_bus_spec.rb +34 -6
- data/spec/entity_store/external_store_spec.rb +69 -43
- data/spec/entity_store/store_spec.rb +3 -2
- data/spec/entity_store_spec.rb +1 -8
- data/spec/spec_helper.rb +12 -0
- metadata +18 -2
data/lib/entity_store/entity.rb
CHANGED
@@ -1,36 +1,65 @@
|
|
1
1
|
module EntityStore
|
2
2
|
class EventBus
|
3
|
-
|
4
|
-
def publish(entity_type, event)
|
5
|
-
publish_externally entity_type, event
|
3
|
+
include Hatchet
|
6
4
|
|
7
|
-
|
5
|
+
def publish(entity_type, event)
|
6
|
+
publish_externally entity_type, event
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
logger.debug { "publishing #{event.inspect}" }
|
9
|
+
|
10
|
+
subscribers_to(event.receiver_name).each do |s|
|
11
|
+
begin
|
12
|
+
s.new.send(event.receiver_name, event)
|
13
|
+
logger.debug { "called #{s.name}##{event.receiver_name} with #{event.inspect}" }
|
14
|
+
rescue => e
|
15
|
+
logger.error "#{e.message} when calling #{s.name}##{event.receiver_name} with #{event.inspect}", e
|
16
16
|
end
|
17
17
|
end
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def subscribers_to(event_name)
|
21
|
+
subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def subscribers
|
25
|
+
EntityStore.event_subscribers
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
def publish_externally(entity_type, event)
|
29
|
+
external_store.add_event(entity_type, event)
|
30
|
+
end
|
31
|
+
|
32
|
+
def external_store
|
33
|
+
@_external_store ||= ExternalStore.new
|
34
|
+
end
|
30
35
|
|
31
|
-
|
32
|
-
|
36
|
+
# Public - replay events of a given type to a given subscriber
|
37
|
+
#
|
38
|
+
# since - Time reference point
|
39
|
+
# type - String type name of event
|
40
|
+
# subscriber - Class of the subscriber to replay events to
|
41
|
+
#
|
42
|
+
# Returns nothing
|
43
|
+
def replay(since, type, subscriber)
|
44
|
+
max_items = 100
|
45
|
+
event_data_objects = external_store.get_events(since, type, max_items)
|
46
|
+
|
47
|
+
while event_data_objects.count > 0 do
|
48
|
+
event_data_objects.each do |event_data_object|
|
49
|
+
begin
|
50
|
+
event = get_type_constant(event_data_object.type).new(event_data_object.attrs)
|
51
|
+
subscriber.new.send(event.receiver_name, event)
|
52
|
+
logger.info { "replayed #{event.inspect} to #{subscriber.name}##{event.receiver_name}" }
|
53
|
+
rescue => e
|
54
|
+
logger.error "#{e.message} when replaying #{event_data_object.inspect} to #{subscriber}", e
|
55
|
+
end
|
56
|
+
end
|
57
|
+
event_data_objects = external_store.get_events(event_data_objects.last.id, type, max_items)
|
33
58
|
end
|
34
59
|
end
|
60
|
+
|
61
|
+
def get_type_constant(type_name)
|
62
|
+
type_name.split('::').inject(Object) {|obj, name| obj.const_get(name) }
|
63
|
+
end
|
35
64
|
end
|
36
65
|
end
|
@@ -29,13 +29,23 @@ module EntityStore
|
|
29
29
|
)
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
# Public - get events since a Time or ID
|
33
|
+
#
|
34
|
+
# since - Time or String id to filter events from
|
35
|
+
# type - String optionally filter the event type to return (default=nil)
|
36
|
+
# max_items - Fixnum max items to return (default=100)
|
37
|
+
#
|
38
|
+
# Returns Enumerable EventDataObject
|
39
|
+
def get_events(since, type=nil, max_items=100)
|
40
|
+
since_id = since.is_a?(Time) ? BSON::ObjectId.from_time(since) : BSON::ObjectId.from_string(since)
|
41
|
+
|
42
|
+
query = { '_id' => { '$gt' => since_id } }
|
43
|
+
query['_type'] = type if type
|
36
44
|
|
37
|
-
options = {
|
38
|
-
|
45
|
+
options = {
|
46
|
+
:sort => [['_id', Mongo::ASCENDING]],
|
47
|
+
:limit => max_items
|
48
|
+
}
|
39
49
|
|
40
50
|
collection.find(query, options).collect { |e| EventDataObject.new(e)}
|
41
51
|
end
|
@@ -4,6 +4,7 @@ require 'uri'
|
|
4
4
|
module EntityStore
|
5
5
|
class MongoEntityStore
|
6
6
|
include Mongo
|
7
|
+
include Hatchet
|
7
8
|
|
8
9
|
def open_connection
|
9
10
|
@db ||= open_store
|
@@ -79,7 +80,7 @@ module EntityStore
|
|
79
80
|
begin
|
80
81
|
event.apply(entity)
|
81
82
|
rescue => e
|
82
|
-
|
83
|
+
logger.error "Failed to apply #{event.class.name} #{event.attributes} to #{id} with #{e.inspect}", e
|
83
84
|
end
|
84
85
|
entity.version = event.entity_version
|
85
86
|
end
|
@@ -107,8 +108,7 @@ module EntityStore
|
|
107
108
|
begin
|
108
109
|
get_type_constant(attrs['_type']).new(attrs)
|
109
110
|
rescue => e
|
110
|
-
logger
|
111
|
-
logger.error "Error loading type #{attrs['_type']}"
|
111
|
+
logger.error "Error loading type #{attrs['_type']}", e
|
112
112
|
nil
|
113
113
|
end
|
114
114
|
end.select { |e| !e.nil? }
|
data/lib/entity_store/store.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module EntityStore
|
2
2
|
class Store
|
3
|
+
include Hatchet
|
4
|
+
|
3
5
|
def storage_client
|
4
6
|
@_storage_client ||= MongoEntityStore.new
|
5
7
|
end
|
@@ -9,7 +11,7 @@ module EntityStore
|
|
9
11
|
add_events(entity)
|
10
12
|
entity
|
11
13
|
rescue => e
|
12
|
-
|
14
|
+
logger.error { "Store#add error: #{e.inspect} - #{entity.inspect}" }
|
13
15
|
raise e
|
14
16
|
end
|
15
17
|
|
@@ -33,12 +35,12 @@ module EntityStore
|
|
33
35
|
end
|
34
36
|
entity
|
35
37
|
rescue => e
|
36
|
-
|
38
|
+
logger.error { "Store#do_save error: #{e.inspect} - #{entity.inspect}" }
|
37
39
|
raise e
|
38
40
|
end
|
39
41
|
|
40
42
|
def snapshot_entity(entity)
|
41
|
-
|
43
|
+
logger.info { "Store#snapshot_entity : Snapshotting #{entity.id}"}
|
42
44
|
storage_client.snapshot_entity(entity)
|
43
45
|
end
|
44
46
|
|
@@ -52,7 +54,7 @@ module EntityStore
|
|
52
54
|
e.entity_version = entity.version
|
53
55
|
storage_client.add_event(e)
|
54
56
|
end
|
55
|
-
entity.pending_events.each {|e|
|
57
|
+
entity.pending_events.each {|e| event_bus.publish(entity.type, e) }
|
56
58
|
entity.clear_pending_events
|
57
59
|
end
|
58
60
|
|
@@ -77,5 +79,8 @@ module EntityStore
|
|
77
79
|
@_storage_client = nil
|
78
80
|
end
|
79
81
|
|
82
|
+
def event_bus
|
83
|
+
@_event_bus ||= EventBus.new
|
84
|
+
end
|
80
85
|
end
|
81
86
|
end
|
data/lib/entity_store/version.rb
CHANGED
data/lib/entity_store.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module EntityStore
|
2
|
-
|
2
|
+
|
3
|
+
require 'hatchet'
|
3
4
|
require 'entity_store/entity'
|
4
5
|
require 'entity_store/entity_value'
|
5
6
|
require 'entity_store/event'
|
@@ -47,26 +48,6 @@ module EntityStore
|
|
47
48
|
@_snapshot_threshold = value
|
48
49
|
end
|
49
50
|
|
50
|
-
def log_level
|
51
|
-
@_log_level ||= Logger::INFO
|
52
|
-
end
|
53
|
-
|
54
|
-
def log_level=(value)
|
55
|
-
@_log_level = value
|
56
|
-
end
|
57
|
-
|
58
|
-
def logger
|
59
|
-
unless @_logger
|
60
|
-
@_logger = Logger.new(STDOUT)
|
61
|
-
@_logger.progname = "Entity_Store"
|
62
|
-
@_logger.level = log_level
|
63
|
-
end
|
64
|
-
@_logger
|
65
|
-
end
|
66
|
-
|
67
|
-
def logger=(value)
|
68
|
-
@_logger = value
|
69
|
-
end
|
70
51
|
end
|
71
52
|
|
72
53
|
|
@@ -15,17 +15,18 @@ describe EventBus do
|
|
15
15
|
before(:each) do
|
16
16
|
@entity_type = random_string
|
17
17
|
@event = DummyEvent.new(:name => random_string)
|
18
|
+
@event_bus = EventBus.new
|
18
19
|
end
|
19
20
|
describe ".publish" do
|
20
21
|
before(:each) do
|
21
22
|
@subscriber = mock("Subscriber", :dummy_event => true)
|
22
23
|
DummySubscriber.stub(:new) { @subscriber }
|
23
24
|
@subscriber_class2 = mock("SubscriberClass", :instance_methods => ['bilge'], :name => "SubscriberClass")
|
24
|
-
|
25
|
-
|
25
|
+
@event_bus.stub(:subscribers).and_return([DummySubscriber, @subscriber_class2])
|
26
|
+
@event_bus.stub(:publish_externally)
|
26
27
|
end
|
27
28
|
|
28
|
-
subject {
|
29
|
+
subject { @event_bus.publish(@entity_type, @event) }
|
29
30
|
|
30
31
|
it "calls the receiver method on the subscriber" do
|
31
32
|
@subscriber.should_receive(:dummy_event).with(@event)
|
@@ -36,7 +37,7 @@ describe EventBus do
|
|
36
37
|
subject
|
37
38
|
end
|
38
39
|
it "publishes event to the external event push" do
|
39
|
-
|
40
|
+
@event_bus.should_receive(:publish_externally).with(@entity_type, @event)
|
40
41
|
subject
|
41
42
|
end
|
42
43
|
end
|
@@ -44,14 +45,41 @@ describe EventBus do
|
|
44
45
|
describe ".publish_externally" do
|
45
46
|
before(:each) do
|
46
47
|
@external_store = mock(ExternalStore)
|
47
|
-
|
48
|
+
@event_bus.stub(:external_store) { @external_store }
|
48
49
|
end
|
49
50
|
|
50
|
-
subject {
|
51
|
+
subject { @event_bus.publish_externally @entity_type, @event }
|
51
52
|
|
52
53
|
it "should publish to the external store" do
|
53
54
|
@external_store.should_receive(:add_event).with(@entity_type, @event)
|
54
55
|
subject
|
55
56
|
end
|
56
57
|
end
|
58
|
+
|
59
|
+
describe "#replay" do
|
60
|
+
before(:each) do
|
61
|
+
@since = random_time
|
62
|
+
@type = 'DummyEvent'
|
63
|
+
@subscriber = mock("Subscriber", :dummy_event => true)
|
64
|
+
DummySubscriber.stub(:new) { @subscriber }
|
65
|
+
|
66
|
+
@external_store = mock(ExternalStore)
|
67
|
+
@id = random_object_id
|
68
|
+
@external_store.stub(:get_events) { |since| since == @id ? [] : [
|
69
|
+
EventDataObject.new('_id' => @id, '_type' => DummyEvent.name, 'name' => random_string)
|
70
|
+
]}
|
71
|
+
@event_bus.stub(:external_store) { @external_store }
|
72
|
+
end
|
73
|
+
|
74
|
+
subject { @event_bus.replay(@since, @type, DummySubscriber) }
|
75
|
+
|
76
|
+
it "gets the events for that period" do
|
77
|
+
@external_store.should_receive(:get_events).with(@since, @type, 100)
|
78
|
+
subject
|
79
|
+
end
|
80
|
+
it "publishes them to the subscriber" do
|
81
|
+
@subscriber.should_receive(:dummy_event).with(an_instance_of(DummyEvent))
|
82
|
+
subject
|
83
|
+
end
|
84
|
+
end
|
57
85
|
end
|
@@ -36,63 +36,89 @@ describe ExternalStore do
|
|
36
36
|
|
37
37
|
describe "#get_events" do
|
38
38
|
before(:each) do
|
39
|
-
@
|
40
|
-
@
|
41
|
-
DummyEvent.new(:name => random_string, :entity_id => random_object_id),
|
42
|
-
DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
|
43
|
-
DummyEvent.new(:name => random_string, :entity_id => random_object_id),
|
44
|
-
DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
|
45
|
-
DummyEvent.new(:name => random_string, :entity_id => random_object_id)
|
46
|
-
]
|
47
|
-
|
48
|
-
@events.each { |e| @store.add_event(@entity_type, e)}
|
49
|
-
end
|
50
|
-
|
51
|
-
context "when no options" do
|
39
|
+
@reference_time = Time.now
|
40
|
+
@ids = (-2..2).collect { |i| BSON::ObjectId.from_time(@reference_time + i) }
|
52
41
|
|
53
|
-
|
42
|
+
@store.collection.insert({'_id' => @ids[0], '_type' => 'DummyEvent'})
|
43
|
+
@store.collection.insert({'_id' => @ids[1], '_type' => 'DummyEventTwo'})
|
44
|
+
@store.collection.insert({'_id' => @ids[2], '_type' => 'DummyEvent'})
|
45
|
+
@store.collection.insert({'_id' => @ids[3], '_type' => 'DummyEventTwo'})
|
46
|
+
@store.collection.insert({'_id' => @ids[4], '_type' => 'DummyEvent'})
|
47
|
+
end
|
48
|
+
|
49
|
+
subject { @store.get_events @since, @type }
|
54
50
|
|
55
|
-
|
56
|
-
|
51
|
+
context "when time passed as since" do
|
52
|
+
before(:each) do
|
53
|
+
@since = @reference_time
|
57
54
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
55
|
+
context "when no type filter" do
|
56
|
+
before(:each) do
|
57
|
+
@type = nil
|
58
|
+
@results = subject
|
59
|
+
end
|
60
|
+
it "returns two records" do
|
61
|
+
@results.count.should eq(2)
|
62
|
+
end
|
63
|
+
it "it returns the 4th item first" do
|
64
|
+
@results.first.id.should eq(@ids[3].to_s)
|
65
|
+
end
|
66
|
+
it "it returns the 5th item second" do
|
67
|
+
@results[1].id.should eq(@ids[4].to_s)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
context "when type filter 'DummyEventTwo' passed" do
|
64
71
|
before(:each) do
|
65
|
-
@
|
72
|
+
@type = "DummyEventTwo"
|
73
|
+
@results = subject
|
66
74
|
end
|
67
|
-
|
68
|
-
|
69
|
-
subject.count.should eq(@options[:limit])
|
75
|
+
it "returns 1 record" do
|
76
|
+
@results.count.should eq(1)
|
70
77
|
end
|
78
|
+
it "returns the 4th item" do
|
79
|
+
@results.first.id.should eq(@ids[3].to_s)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when id passed as since" do
|
85
|
+
before(:each) do
|
86
|
+
@since = @ids[1].to_s
|
71
87
|
end
|
72
|
-
|
73
|
-
context "when after index passed" do
|
88
|
+
context "when no type filter passed" do
|
74
89
|
before(:each) do
|
75
|
-
|
76
|
-
@
|
90
|
+
@type = nil
|
91
|
+
@results = subject
|
92
|
+
end
|
93
|
+
it "returns 3 records" do
|
94
|
+
@results.count.should eq(3)
|
95
|
+
end
|
96
|
+
it "it returns the 3rd item first" do
|
97
|
+
@results.first.id.should eq(@ids[2].to_s)
|
77
98
|
end
|
78
|
-
|
79
|
-
|
80
|
-
subject.count.should eq(2)
|
99
|
+
it "it returns the 4th item second" do
|
100
|
+
@results[1].id.should eq(@ids[3].to_s)
|
81
101
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
102
|
+
it "it returns the 5th item second" do
|
103
|
+
@results[2].id.should eq(@ids[4].to_s)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
context "when type filter 'DummyEvent' passed" do
|
85
107
|
before(:each) do
|
86
|
-
@
|
108
|
+
@type = "DummyEvent"
|
109
|
+
@results = subject
|
110
|
+
end
|
111
|
+
it "returns 2 records" do
|
112
|
+
@results.count.should eq(2)
|
87
113
|
end
|
88
|
-
|
89
|
-
|
90
|
-
subject.count.should eq(3)
|
114
|
+
it "returns the 3rd item" do
|
115
|
+
@results.first.id.should eq(@ids[2].to_s)
|
91
116
|
end
|
92
|
-
|
93
|
-
|
117
|
+
it "returns the 5th item" do
|
118
|
+
@results[1].id.should eq(@ids[4].to_s)
|
119
|
+
end
|
120
|
+
end
|
94
121
|
end
|
95
|
-
|
96
122
|
end
|
97
123
|
|
98
124
|
end
|
@@ -43,7 +43,8 @@ describe Store do
|
|
43
43
|
@storage_client = mock("StorageClient", :add_event => true)
|
44
44
|
@store = Store.new
|
45
45
|
@store.stub(:storage_client) { @storage_client }
|
46
|
-
EventBus
|
46
|
+
@event_bus = mock(EventBus, :publish => true)
|
47
|
+
@store.stub(:event_bus) { @event_bus}
|
47
48
|
end
|
48
49
|
|
49
50
|
subject { @store.add_events(@entity) }
|
@@ -68,7 +69,7 @@ describe Store do
|
|
68
69
|
end
|
69
70
|
it "publishes each event to the EventBus" do
|
70
71
|
@entity.pending_events.each do |e|
|
71
|
-
|
72
|
+
@event_bus.should_receive(:publish).with(@entity.type, e)
|
72
73
|
end
|
73
74
|
subject
|
74
75
|
end
|
data/spec/entity_store_spec.rb
CHANGED
@@ -2,14 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe EntityStore do
|
4
4
|
describe ".setup" do
|
5
|
-
|
6
|
-
EntityStore.setup do |config|
|
7
|
-
config.log_level = Logger::WARN
|
8
|
-
end
|
9
|
-
end
|
10
|
-
it "has a log_level of WARN" do
|
11
|
-
EntityStore.log_level.should eq(Logger::WARN)
|
12
|
-
end
|
5
|
+
|
13
6
|
end
|
14
7
|
|
15
8
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,6 +6,18 @@ RSpec.configure do |config|
|
|
6
6
|
config.color_enabled = true
|
7
7
|
end
|
8
8
|
|
9
|
+
Hatchet.configure do |config|
|
10
|
+
# Reset the logging configuration
|
11
|
+
config.reset!
|
12
|
+
config.level :error
|
13
|
+
# Use the format without time, etc so we don't duplicate it
|
14
|
+
config.formatter = Hatchet::SimpleFormatter.new
|
15
|
+
# Set up a STDOUT appender
|
16
|
+
config.appenders << Hatchet::LoggerAppender.new do |appender|
|
17
|
+
appender.logger = Logger.new(STDOUT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
9
21
|
include EntityStore
|
10
22
|
|
11
23
|
def random_string
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: entity_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-11-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mongo
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '1.6'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: hatchet
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.0.20
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.0.20
|
46
62
|
description: Event sourced entity store with a Mongo body
|
47
63
|
email: adam.bird@gmail.com
|
48
64
|
executables: []
|