sandthorn 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +28 -22
- data/README.md +154 -15
- data/lib/sandthorn.rb +21 -4
- data/lib/sandthorn/aggregate_root_base.rb +75 -36
- data/lib/sandthorn/aggregate_root_marshal.rb +15 -5
- data/lib/sandthorn/aggregate_root_snapshot.rb +15 -6
- data/lib/sandthorn/event_inspector.rb +43 -11
- data/lib/sandthorn/version.rb +1 -1
- data/spec/sandthorn_spec.rb +40 -0
- data/spec/tracing_spec.rb +98 -112
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 406c440420a9d7f88fe93583721b54b2f5d21248
|
4
|
+
data.tar.gz: 9f9158b4be2dd23d95e952c9a9649a17a2b6b4c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc57b92951bd52e571e2428c33fa81b297bfe6a7a488569fd54cf116f822b5dee4549377adb003344004e4f7868a0aa76086b938cf210b6a45dd378373303a7c
|
7
|
+
data.tar.gz: 2843b99f89f0b2261b0b554f2812dc198b6d21084cf0890a3d2a0b1540bdb1145c49bcf3a5ffeb78a5f07ce1998201a6a21b3e2a24b5da9a43bb0a440fcf8ab8
|
data/Gemfile.lock
CHANGED
@@ -16,45 +16,51 @@ GEM
|
|
16
16
|
term-ansicolor
|
17
17
|
thor
|
18
18
|
diff-lcs (1.2.5)
|
19
|
-
docile (1.1.
|
20
|
-
gem-release (0.7.
|
19
|
+
docile (1.1.5)
|
20
|
+
gem-release (0.7.3)
|
21
21
|
method_source (0.8.2)
|
22
|
-
mime-types (2.
|
23
|
-
multi_json (1.
|
22
|
+
mime-types (2.3)
|
23
|
+
multi_json (1.10.1)
|
24
|
+
netrc (0.7.7)
|
24
25
|
pg (0.17.1)
|
25
|
-
pry (0.
|
26
|
-
coderay (~> 1.0)
|
27
|
-
method_source (~> 0.8)
|
26
|
+
pry (0.10.0)
|
27
|
+
coderay (~> 1.1.0)
|
28
|
+
method_source (~> 0.8.1)
|
28
29
|
slop (~> 3.4)
|
29
30
|
pry-doc (0.6.0)
|
30
31
|
pry (~> 0.9)
|
31
32
|
yard (~> 0.8)
|
32
|
-
rake (10.
|
33
|
-
rest-client (1.
|
34
|
-
mime-types (>= 1.16)
|
35
|
-
|
36
|
-
|
37
|
-
rspec-
|
38
|
-
rspec-
|
39
|
-
|
40
|
-
rspec-
|
41
|
-
|
42
|
-
rspec-
|
33
|
+
rake (10.3.2)
|
34
|
+
rest-client (1.7.2)
|
35
|
+
mime-types (>= 1.16, < 3.0)
|
36
|
+
netrc (~> 0.7)
|
37
|
+
rspec (3.0.0)
|
38
|
+
rspec-core (~> 3.0.0)
|
39
|
+
rspec-expectations (~> 3.0.0)
|
40
|
+
rspec-mocks (~> 3.0.0)
|
41
|
+
rspec-core (3.0.3)
|
42
|
+
rspec-support (~> 3.0.0)
|
43
|
+
rspec-expectations (3.0.3)
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
+
rspec-support (~> 3.0.0)
|
46
|
+
rspec-mocks (3.0.3)
|
47
|
+
rspec-support (~> 3.0.0)
|
48
|
+
rspec-support (3.0.3)
|
43
49
|
sandthorn_driver_sequel (1.1.0)
|
44
50
|
pg
|
45
51
|
sequel
|
46
|
-
sequel (4.
|
47
|
-
simplecov (0.
|
52
|
+
sequel (4.13.0)
|
53
|
+
simplecov (0.9.0)
|
48
54
|
docile (~> 1.1.0)
|
49
55
|
multi_json
|
50
56
|
simplecov-html (~> 0.8.0)
|
51
57
|
simplecov-html (0.8.0)
|
52
|
-
slop (3.
|
58
|
+
slop (3.6.0)
|
53
59
|
sqlite3 (1.3.9)
|
54
60
|
term-ansicolor (1.3.0)
|
55
61
|
tins (~> 1.0)
|
56
62
|
thor (0.19.1)
|
57
|
-
tins (1.
|
63
|
+
tins (1.3.0)
|
58
64
|
yard (0.8.7.4)
|
59
65
|
|
60
66
|
PLATFORMS
|
data/README.md
CHANGED
@@ -4,16 +4,32 @@
|
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/sandthorn.png)](http://badge.fury.io/rb/sandthorn)
|
5
5
|
|
6
6
|
# Sandthorn Event Sourcing
|
7
|
-
A ruby
|
7
|
+
A ruby library for saving an object's state as a series of events.
|
8
8
|
|
9
|
-
## What is Event Sourcing
|
9
|
+
## What is Event Sourcing?
|
10
10
|
|
11
11
|
"Capture all changes to an application state as a sequence of events."
|
12
12
|
[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html)
|
13
13
|
|
14
|
-
##
|
14
|
+
## When do I need event sourcing?
|
15
15
|
|
16
|
-
|
16
|
+
If the history of how an object came to be is important a well known technique is to generate a separate history log. The log is generated in parallel with the object and all actions to the object needs to be stored to the log by a separate method call. With event sourcing the history log is now integrated with the object and generated based on the actions that are made on the object, the log is now the fact that the object is built upon.
|
17
|
+
|
18
|
+
|
19
|
+
## Why Sandthorn?
|
20
|
+
|
21
|
+
If you have been following [Uncle Bob](http://blog.8thlight.com/uncle-bob/2014/05/11/FrameworkBound.html) you know what he thinks of the "Rails way" and how we get bound to the Rails framework. We have created Sandthorn to decouple our models from Active Record and restore them to what they should be, i.e., Plain Old Ruby Objects (PORO) with a twist of Sandthorn magic.
|
22
|
+
|
23
|
+
Check out examples of Sandthorn:
|
24
|
+
|
25
|
+
* [Examples](https://github.com/Sandthorn/sandthorn_examples) including a product shop and TicTacToe game.
|
26
|
+
* Live [demo](http://demo.sandthorn.org) comparing Active Record and Sandthorn.
|
27
|
+
|
28
|
+
## How do I use Sandthorn?
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
Think of it as an object database where you store not only what the new value of an attribute is, but also when and why it changed.
|
17
33
|
_Example:_
|
18
34
|
|
19
35
|
```ruby
|
@@ -65,12 +81,9 @@ ship.save
|
|
65
81
|
|
66
82
|
new_ship = Ship.find ship.id
|
67
83
|
puts new_ship.name
|
68
|
-
|
69
|
-
# For more info look at the specs.
|
70
|
-
|
71
84
|
```
|
72
85
|
|
73
|
-
|
86
|
+
# Installation
|
74
87
|
|
75
88
|
Add this line to your application's Gemfile:
|
76
89
|
|
@@ -84,18 +97,144 @@ Or install it yourself as:
|
|
84
97
|
|
85
98
|
$ gem install sandthorn
|
86
99
|
|
87
|
-
|
100
|
+
# Configuring Sandthorn
|
101
|
+
|
102
|
+
Sandthorn relies on a driver is specific to the data storage that you are using. This means Sandthorn can be used with any data storage given that a driver exists.
|
103
|
+
|
104
|
+
To setup a driver you need to add it to your project's Gemfile and configure it in your application code.
|
105
|
+
|
106
|
+
gem 'sandthorn_driver_sequel'
|
107
|
+
|
108
|
+
The driver is configured when your application launches. Here's an example of how to do it using the Sequel driver and a sqlite3 database.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
url = "sqlite://spec/db/sequel_driver.sqlite3"
|
112
|
+
driver = SandthornDriverSequel.driver_from_url(url: url)
|
113
|
+
catch_all_config = [ { driver: driver } ]
|
114
|
+
Sandthorn.configuration = catch_all_config
|
115
|
+
```
|
116
|
+
|
117
|
+
First we specify the path to the sqlite3 database in the `url` variable. Secondly, the specific driver is instantiated with the `url`. Hence, the driver could be instantiated using a different configuration, for example, an address to a Postgres database. Finally, `Sandthorn.configure` accepts a keyword list with options. The only option which is required is `driver`.
|
118
|
+
|
119
|
+
The first time you use the Sequel driver it is necessary to install the database schema.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
url = "sqlite://spec/db/sequel_driver.sqlite3"
|
123
|
+
SandthornDriverSequel::Migration.new url: url
|
124
|
+
SandthornDriverSequel.migrate_db url: url
|
125
|
+
```
|
126
|
+
|
127
|
+
Optionally, when using Sandthorn in your tests you can configure it in a `spec_helper.rb` which is then required by your test suites [example](https://github.com/Sandthorn/sandthorn_examples/blob/master/sandthorn_tictactoe/spec/spec_helper.rb#L20-L30). Note that the Sequel driver accepts a special parameter to empty the database between each test.
|
128
|
+
|
129
|
+
The Sequel driver is the only production-ready driver to date.
|
130
|
+
|
131
|
+
# Usage
|
132
|
+
|
133
|
+
Any object that should have event sourcing capability must include the methods provided by `Sandthorn::AggregateRoot`. These make it possible to `commit` events and `save` changes to an aggregate. Use the `include` directive as follows:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
require 'sandthorn'
|
137
|
+
|
138
|
+
class Board
|
139
|
+
include Sandthorn::AggregateRoot
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
All objects that include `Sandthorn::AggregateRoot` is provided with an `aggregate_id` which is a [UUID](http://en.wikipedia.org/wiki/Universally_unique_identifier).
|
144
|
+
|
145
|
+
### `Sandthorn::AggregateRoot.commit`
|
146
|
+
|
147
|
+
It is required that an event is commited to the aggregate to be stored as an event. `commit` extracts the object's delta and locally caches the state changes that has been applied to the aggregate. Commonly, commit is called when an event is applied. In [CQRS](http://martinfowler.com/bliki/CQRS.html), events are named using past tense.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
def mark player, pos_x, pos_y
|
151
|
+
# change some state
|
152
|
+
marked
|
153
|
+
end
|
154
|
+
|
155
|
+
def marked
|
156
|
+
commit
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
`commit` determines the state changes by monitoring the object's readable fields.
|
161
|
+
|
162
|
+
### `Sandthorn::AggregateRoot.save`
|
163
|
+
|
164
|
+
Once one or more commits have been applied to an aggregate it should be saved. This means all commited events will be persisted by the specific Sandthorn driver. `save` is called by the owning object.
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
board = Board.new
|
168
|
+
board.mark :o, 0, 1
|
169
|
+
board.save
|
170
|
+
```
|
171
|
+
|
172
|
+
### `Sandthorn::AggregateRoot.all`
|
173
|
+
|
174
|
+
It is possible to retrieve an array with all instances of a specific aggregate.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
Board.all
|
178
|
+
```
|
179
|
+
|
180
|
+
Since it return's an `Array` you can, for example, filter on an aggregate's fields
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
Board.all.select { |board| board.active == true }
|
184
|
+
```
|
185
|
+
|
186
|
+
### `Sandthorn::AggregateRoot.find`
|
187
|
+
|
188
|
+
Using `find` it is possible to retrieve a specific aggregate using it's id.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
uuid = '550e8400-e29b-41d4-a716-446655440000'
|
192
|
+
board = Board.find(uuid)
|
193
|
+
board.aggregate_id == uuid
|
194
|
+
```
|
195
|
+
|
196
|
+
If no aggregate with the specifid id is found, a `Sandthorn::Errors::AggregateNotFound` exception is raised.
|
197
|
+
|
198
|
+
|
199
|
+
### `Sandthorn::AggregateRoot.aggregate_trace`
|
200
|
+
|
201
|
+
Using `aggregate_trace` one can store meta data with events. The data is not aggregate specific, for example, one can store who executed a specific command on the aggregate.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
board.aggregate_trace {player: "Fred"} do |aggregate|
|
205
|
+
aggregate.mark :o, 0, 1
|
206
|
+
aggregate.save
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
`aggregate_trace` can also be specified on a class.
|
211
|
+
|
212
|
+
````ruby
|
213
|
+
Board.aggregate_trace {ip: :127.0.0.1} do
|
214
|
+
board = Board.new
|
215
|
+
board.mark :o , 0, 1
|
216
|
+
board.save
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
In this case, the resulting events from the commands `new` and `mark` will have the trace `{ip: :127.0.0.1}` attached to them.
|
221
|
+
|
222
|
+
|
223
|
+
# Development
|
224
|
+
|
225
|
+
Run tests: `rake`
|
226
|
+
|
227
|
+
Run benchmark tests: `rake benchmark`
|
88
228
|
|
89
|
-
|
229
|
+
Load a console: `rake console`
|
90
230
|
|
91
|
-
|
231
|
+
# Contributing
|
92
232
|
|
93
|
-
|
94
|
-
`rake console`
|
233
|
+
We're happy to accept pull requests that makes the code cleaner or more idiomatic, the documentation more understandable, or improves the testsuite. Even considering opening an issue for what's troubling you or writing a blog post about how you used Sandthorn is worth a lot too!
|
95
234
|
|
96
|
-
|
235
|
+
In general, the contribution process for code works like this.
|
97
236
|
|
98
|
-
1. Fork
|
237
|
+
1. Fork this repo
|
99
238
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
239
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
101
240
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/lib/sandthorn.rb
CHANGED
@@ -58,14 +58,31 @@ module Sandthorn
|
|
58
58
|
def get_events aggregate_types: [], take: 0, after_sequence_number: 0
|
59
59
|
drivers = drivers_for_aggregate_types type_names: aggregate_types
|
60
60
|
raise Sandthorn::Errors::Error.new "Cannot get events from multiple contexts simultaneously, only one single context can be handled at a time." unless drivers.length == 1
|
61
|
-
driver = drivers.first
|
61
|
+
driver = drivers.first
|
62
62
|
events = driver.get_events aggregate_types: aggregate_types, take: take, after_sequence_number: after_sequence_number
|
63
63
|
events.each do |event|
|
64
64
|
event[:event_args] = deserialize event[:event_data]
|
65
65
|
event.delete(:event_data)
|
66
|
-
end
|
66
|
+
end
|
67
67
|
events
|
68
68
|
end
|
69
|
+
|
70
|
+
def obsolete_snapshots type_names: [], min_event_distance: 0
|
71
|
+
drivers = drivers_for_aggregate_types type_names: type_names
|
72
|
+
obsolete = drivers.flat_map { |driver| driver.obsolete_snapshots(class_names: type_names, max_event_distance: min_event_distance) }
|
73
|
+
yielder = []
|
74
|
+
obsolete.each do |single_obsolete|
|
75
|
+
type = Kernel.const_get single_obsolete[:aggregate_type]
|
76
|
+
aggregate = type.aggregate_find single_obsolete[:aggregate_id]
|
77
|
+
if block_given?
|
78
|
+
yield aggregate
|
79
|
+
else
|
80
|
+
yielder << aggregate
|
81
|
+
end
|
82
|
+
end
|
83
|
+
yielder unless block_given?
|
84
|
+
end
|
85
|
+
|
69
86
|
private
|
70
87
|
def driver_for class_name, &block
|
71
88
|
driver = identify_driver_from_class class_name
|
@@ -73,8 +90,8 @@ module Sandthorn
|
|
73
90
|
driver
|
74
91
|
end
|
75
92
|
def identify_driver_from_class class_name
|
76
|
-
matches = configuration.select do |conf|
|
77
|
-
r = Regexp.new "^#{conf[:aggregate_pattern]}"
|
93
|
+
matches = configuration.select do |conf|
|
94
|
+
r = Regexp.new "^#{conf[:aggregate_pattern]}"
|
78
95
|
pattern = class_name.to_s
|
79
96
|
conf[:aggregate_pattern].nil? || r.match(pattern)
|
80
97
|
end
|
@@ -7,6 +7,7 @@ module Sandthorn
|
|
7
7
|
attr_reader :aggregate_current_event_version
|
8
8
|
attr_reader :aggregate_originating_version
|
9
9
|
attr_reader :aggregate_stored_serialized_object
|
10
|
+
attr_reader :aggregate_trace_information
|
10
11
|
|
11
12
|
alias :id :aggregate_id
|
12
13
|
|
@@ -22,45 +23,65 @@ module Sandthorn
|
|
22
23
|
event[:event_data] = Sandthorn.serialize event[:event_args]
|
23
24
|
event[:event_args] = nil #Not send extra data over the wire
|
24
25
|
end
|
26
|
+
|
25
27
|
unless aggregate_events.empty?
|
26
|
-
Sandthorn.save_events(
|
28
|
+
Sandthorn.save_events(
|
29
|
+
aggregate_events,
|
30
|
+
aggregate_originating_version,
|
31
|
+
aggregate_id,
|
32
|
+
self.class.name
|
33
|
+
)
|
34
|
+
|
27
35
|
@aggregate_events = []
|
28
36
|
@aggregate_originating_version = @aggregate_current_event_version
|
29
37
|
end
|
38
|
+
|
30
39
|
self
|
31
40
|
end
|
32
41
|
|
42
|
+
def aggregate_trace args
|
43
|
+
@aggregate_trace_information = args
|
44
|
+
yield self if block_given?
|
45
|
+
@aggregate_trace_information = nil
|
46
|
+
end
|
47
|
+
|
33
48
|
def commit *args
|
34
49
|
aggregate_attribute_deltas = get_delta
|
35
|
-
|
50
|
+
|
36
51
|
unless aggregate_attribute_deltas.empty?
|
37
52
|
method_name = caller_locations(1,1)[0].label.gsub("block in ", "")
|
38
53
|
increase_current_aggregate_version!
|
39
|
-
|
40
|
-
data
|
41
|
-
|
54
|
+
|
55
|
+
data = {
|
56
|
+
method_name: method_name,
|
57
|
+
method_args: args,
|
58
|
+
attribute_deltas: aggregate_attribute_deltas
|
59
|
+
}
|
60
|
+
trace_information = @aggregate_trace_information
|
61
|
+
unless trace_information.nil? || trace_information.empty?
|
62
|
+
data.merge!({ trace: trace_information })
|
63
|
+
end
|
64
|
+
|
65
|
+
@aggregate_events << ({
|
66
|
+
aggregate_version: @aggregate_current_event_version,
|
67
|
+
event_name: method_name,
|
68
|
+
event_args: data
|
69
|
+
})
|
42
70
|
end
|
71
|
+
|
43
72
|
self
|
44
73
|
end
|
45
74
|
|
46
75
|
alias :record_event :commit
|
47
|
-
|
48
|
-
# def aggregate_trace args
|
49
|
-
# @aggregate_trace_information = args
|
50
|
-
# yield self
|
51
|
-
# @aggregate_trace_information = nil
|
52
|
-
# end
|
53
76
|
|
54
77
|
module ClassMethods
|
55
78
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# @aggregate_trace_information = nil
|
63
|
-
# end
|
79
|
+
@@aggregate_trace_information = nil
|
80
|
+
def aggregate_trace args
|
81
|
+
@@aggregate_trace_information = args
|
82
|
+
yield self
|
83
|
+
@@aggregate_trace_information = nil
|
84
|
+
end
|
64
85
|
|
65
86
|
def all
|
66
87
|
aggregate_id_list = Sandthorn.get_aggregate_list_by_typename(self.name)
|
@@ -75,34 +96,41 @@ module Sandthorn
|
|
75
96
|
def aggregate_find aggregate_id
|
76
97
|
class_name = self.respond_to?(:name) ? self.name : self.class # to be able to extend a string for example.
|
77
98
|
events = Sandthorn.get_aggregate(aggregate_id, class_name)
|
78
|
-
raise Sandthorn::Errors::AggregateNotFound unless events and !events.empty?
|
79
99
|
|
80
|
-
|
100
|
+
unless events and !events.empty?
|
101
|
+
raise Sandthorn::Errors::AggregateNotFound
|
102
|
+
end
|
103
|
+
|
104
|
+
transformed_events = events.map do |e|
|
105
|
+
e.merge(event_args: Sandthorn.deserialize(e[:event_data]))
|
106
|
+
end
|
107
|
+
|
81
108
|
aggregate_build transformed_events
|
82
109
|
end
|
83
110
|
|
84
111
|
def new *args
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
#end
|
112
|
+
super.tap do |aggregate|
|
113
|
+
aggregate.aggregate_trace @@aggregate_trace_information do |aggr|
|
114
|
+
aggr.aggregate_base_initialize
|
115
|
+
aggr.aggregate_initialize
|
116
|
+
aggr.send :set_aggregate_id, Sandthorn.generate_aggregate_id
|
117
|
+
aggr.send :commit, *args
|
118
|
+
return aggr
|
119
|
+
end
|
120
|
+
end
|
95
121
|
end
|
96
122
|
|
97
123
|
def aggregate_build events
|
98
124
|
current_aggregate_version = 0
|
125
|
+
|
99
126
|
if first_event_snapshot?(events)
|
100
|
-
aggregate = start_build_from_snapshot events
|
127
|
+
aggregate = start_build_from_snapshot events
|
101
128
|
current_aggregate_version = aggregate.aggregate_originating_version
|
102
129
|
events.shift
|
103
130
|
else
|
104
131
|
aggregate = start_build_from_new events
|
105
132
|
end
|
133
|
+
|
106
134
|
attributes = build_instance_vars_from_events events
|
107
135
|
current_aggregate_version = events.last[:aggregate_version] unless events.empty?
|
108
136
|
aggregate.send :clear_aggregate_events
|
@@ -112,9 +140,11 @@ module Sandthorn
|
|
112
140
|
aggregate.send :set_instance_variables!, attributes
|
113
141
|
aggregate
|
114
142
|
end
|
143
|
+
|
115
144
|
private
|
145
|
+
|
116
146
|
def build_instance_vars_from_events events
|
117
|
-
events.
|
147
|
+
events.each_with_object({}) do |event, instance_vars|
|
118
148
|
event_args = event[:event_args]
|
119
149
|
event_name = event[:event_name]
|
120
150
|
attribute_deltas = event_args[:attribute_deltas]
|
@@ -124,22 +154,26 @@ module Sandthorn
|
|
124
154
|
end
|
125
155
|
instance_vars.merge! deltas
|
126
156
|
end
|
127
|
-
instance_vars
|
128
157
|
end
|
129
158
|
end
|
159
|
+
|
130
160
|
def first_event_snapshot? events
|
131
161
|
events.first[:event_name].to_sym == :aggregate_set_from_snapshot
|
132
162
|
end
|
163
|
+
|
133
164
|
def start_build_from_snapshot events
|
134
165
|
snapshot = events.first[:event_args][0]
|
135
166
|
end
|
167
|
+
|
136
168
|
def start_build_from_new events
|
137
169
|
new_args = events.first[:event_args][:method_args]
|
170
|
+
|
138
171
|
if new_args.nil?
|
139
172
|
aggregate = new
|
140
173
|
else
|
141
|
-
aggregate = new
|
174
|
+
aggregate = new(*new_args)
|
142
175
|
end
|
176
|
+
|
143
177
|
aggregate.send :aggregate_clear_current_event_version!
|
144
178
|
aggregate
|
145
179
|
end
|
@@ -154,7 +188,12 @@ module Sandthorn
|
|
154
188
|
end
|
155
189
|
|
156
190
|
def extract_relevant_aggregate_instance_variables
|
157
|
-
instance_variables.select
|
191
|
+
instance_variables.select do |variable|
|
192
|
+
equals_aggregate_id = variable.to_s == "@aggregate_id"
|
193
|
+
does_not_contain_aggregate = !variable.to_s.start_with?("@aggregate_")
|
194
|
+
|
195
|
+
equals_aggregate_id || does_not_contain_aggregate
|
196
|
+
end
|
158
197
|
end
|
159
198
|
|
160
199
|
def set_orginating_aggregate_version! aggregate_version
|
@@ -13,13 +13,17 @@ module Sandthorn
|
|
13
13
|
def set_instance_variables! attribute
|
14
14
|
super attribute
|
15
15
|
init_vars = extract_relevant_aggregate_instance_variables
|
16
|
-
|
16
|
+
|
17
|
+
init_vars.each do |attribute_name|
|
18
|
+
@aggregate_stored_instance_variables[attribute_name] =
|
19
|
+
::Marshal.dump(instance_variable_get(attribute_name))
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
def get_delta
|
20
24
|
deltas = extract_relevant_aggregate_instance_variables
|
21
|
-
deltas.each { |d| delta_attribute(d)}
|
22
|
-
|
25
|
+
deltas.each { |d| delta_attribute(d) }
|
26
|
+
|
23
27
|
result = @aggregate_attribute_deltas
|
24
28
|
clear_aggregate_deltas
|
25
29
|
result
|
@@ -30,6 +34,7 @@ module Sandthorn
|
|
30
34
|
def delta_attribute attribute_name
|
31
35
|
old_dump = @aggregate_stored_instance_variables[attribute_name]
|
32
36
|
new_dump = ::Marshal.dump(instance_variable_get(attribute_name))
|
37
|
+
|
33
38
|
unless old_dump == new_dump
|
34
39
|
store_attribute_deltas attribute_name, new_dump, old_dump
|
35
40
|
store_aggregate_instance_variable attribute_name, new_dump
|
@@ -39,7 +44,12 @@ module Sandthorn
|
|
39
44
|
def store_attribute_deltas attribute_name, new_dump, old_dump
|
40
45
|
new_value_to_store = ::Marshal.load(new_dump)
|
41
46
|
old_value_to_store = old_dump ? ::Marshal.load(old_dump) : nil
|
42
|
-
|
47
|
+
|
48
|
+
@aggregate_attribute_deltas << {
|
49
|
+
attribute_name: attribute_name.to_s.delete("@"),
|
50
|
+
old_value: old_value_to_store,
|
51
|
+
new_value: new_value_to_store
|
52
|
+
}
|
43
53
|
end
|
44
54
|
|
45
55
|
def store_aggregate_instance_variable attribute_name, new_dump
|
@@ -51,4 +61,4 @@ module Sandthorn
|
|
51
61
|
end
|
52
62
|
end
|
53
63
|
end
|
54
|
-
end
|
64
|
+
end
|
@@ -9,17 +9,26 @@ module Sandthorn
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def aggregate_snapshot!
|
12
|
-
|
12
|
+
if @aggregate_events.count > 0
|
13
|
+
raise Errors::SnapshotError,
|
14
|
+
"Can't take snapshot on object with unsaved events"
|
15
|
+
end
|
16
|
+
|
13
17
|
@aggregate_snapshot = {
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
18
|
+
event_name: "aggregate_set_from_snapshot",
|
19
|
+
event_args: [self],
|
20
|
+
aggregate_version: @aggregate_current_event_version
|
17
21
|
}
|
18
22
|
end
|
19
23
|
|
20
24
|
def save_snapshot
|
21
|
-
|
22
|
-
|
25
|
+
unless aggregate_snapshot
|
26
|
+
raise Errors::SnapshotError, "No snapshot has been created!"
|
27
|
+
end
|
28
|
+
|
29
|
+
@aggregate_snapshot[:event_data] = Sandthorn
|
30
|
+
.serialize aggregate_snapshot[:event_args]
|
31
|
+
|
23
32
|
@aggregate_snapshot[:event_args] = nil
|
24
33
|
Sandthorn.save_snapshot aggregate_snapshot, aggregate_id, self.class.name
|
25
34
|
@aggregate_snapshot = nil
|
@@ -2,51 +2,83 @@ module Sandthorn
|
|
2
2
|
module EventInspector
|
3
3
|
def has_unsaved_event? event_name, options = {}
|
4
4
|
unsaved = events_with_trace_info
|
5
|
+
|
5
6
|
if self.aggregate_events.empty?
|
6
7
|
unsaved = []
|
7
8
|
else
|
8
|
-
unsaved.reject!
|
9
|
+
unsaved.reject! do |e|
|
10
|
+
e[:aggregate_version] < self
|
11
|
+
.aggregate_events.first[:aggregate_version]
|
12
|
+
end
|
9
13
|
end
|
14
|
+
|
10
15
|
matching_events = unsaved.select { |e| e[:event_name] == event_name }
|
11
16
|
event_exists = matching_events.length > 0
|
12
17
|
trace = has_trace? matching_events, options.fetch(:trace, {})
|
13
18
|
|
14
|
-
|
19
|
+
!!(event_exists && trace)
|
15
20
|
end
|
21
|
+
|
16
22
|
def has_saved_event? event_name, options = {}
|
17
23
|
saved = events_with_trace_info
|
18
|
-
|
24
|
+
|
25
|
+
unless self.aggregate_events.empty?
|
26
|
+
saved.reject! do |e|
|
27
|
+
e[:aggregate_version] >= self
|
28
|
+
.aggregate_events.first[:aggregate_version]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
19
32
|
matching_events = saved.select { |e| e[:event_name] == event_name }
|
20
33
|
event_exists = matching_events.length > 0
|
21
34
|
trace = has_trace? matching_events, options.fetch(:trace, {})
|
22
35
|
|
23
|
-
|
36
|
+
!!(event_exists && trace)
|
24
37
|
end
|
38
|
+
|
25
39
|
def has_event? event_name, options = {}
|
26
|
-
matching_events = events_with_trace_info
|
40
|
+
matching_events = events_with_trace_info
|
41
|
+
.select { |e| e[:event_name] == event_name }
|
42
|
+
|
27
43
|
event_exists = matching_events.length > 0
|
28
44
|
trace = has_trace? matching_events, options.fetch(:trace, {})
|
29
|
-
|
45
|
+
!!(event_exists && trace)
|
30
46
|
end
|
47
|
+
|
31
48
|
def events_with_trace_info
|
32
49
|
saved = Sandthorn.get_aggregate_events self.aggregate_id, self.class
|
33
50
|
unsaved = self.aggregate_events
|
34
|
-
all = saved
|
51
|
+
all = saved
|
52
|
+
.concat(unsaved)
|
53
|
+
.sort { |a, b| a[:aggregate_version] <=> b[:aggregate_version] }
|
54
|
+
|
35
55
|
extracted = all.collect do |e|
|
36
56
|
if e[:event_args].nil? && !e[:event_data].nil?
|
37
57
|
data = Sandthorn.deserialize e[:event_data]
|
38
58
|
else
|
39
59
|
data = e[:event_args]
|
40
60
|
end
|
41
|
-
|
42
|
-
|
61
|
+
|
62
|
+
unless data.nil? || !data.is_a?(Hash)
|
63
|
+
trace = data[:trace]
|
64
|
+
end
|
65
|
+
|
66
|
+
{
|
67
|
+
aggregate_version: e[:aggregate_version],
|
68
|
+
event_name: e[:event_name].to_sym,
|
69
|
+
trace: trace
|
70
|
+
}
|
43
71
|
end
|
44
|
-
|
72
|
+
|
73
|
+
extracted
|
45
74
|
end
|
75
|
+
|
46
76
|
private
|
77
|
+
|
47
78
|
def get_unsaved_events event_name
|
48
79
|
self.aggregate_events.select { |e| e[:event_name] == event_name.to_s }
|
49
80
|
end
|
81
|
+
|
50
82
|
def get_saved_events event_name
|
51
83
|
saved_events = Sandthorn.get_aggregate_events self.aggregate_id, self.class
|
52
84
|
saved_events.select { |e| e[:event_name] == event_name.to_s }
|
@@ -60,4 +92,4 @@ module Sandthorn
|
|
60
92
|
true
|
61
93
|
end
|
62
94
|
end
|
63
|
-
end
|
95
|
+
end
|
data/lib/sandthorn/version.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class AnAggregate
|
4
|
+
include Sandthorn::AggregateRoot
|
5
|
+
attr_accessor :test_block_count
|
6
|
+
def initialize
|
7
|
+
@test_block_count = false
|
8
|
+
end
|
9
|
+
def touch; touched; end
|
10
|
+
def touched; commit; end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Sandthorn do
|
14
|
+
before(:each) {
|
15
|
+
@aggregate = AnAggregate.new
|
16
|
+
@aggregate.touch
|
17
|
+
@aggregate.save
|
18
|
+
}
|
19
|
+
|
20
|
+
context "when doing snapshots" do
|
21
|
+
it "retrieves a list of obsolete snapshots" do
|
22
|
+
obsolete_aggregates = Sandthorn.obsolete_snapshots type_names: [AnAggregate], min_event_distance: 0
|
23
|
+
expect(obsolete_aggregates).to_not be_empty
|
24
|
+
end
|
25
|
+
|
26
|
+
it "accepts a block that is applied to each aggregate" do
|
27
|
+
obsolete_aggregates = []
|
28
|
+
Sandthorn.obsolete_snapshots type_names: [AnAggregate], min_event_distance: 0 do |aggr|
|
29
|
+
aggr.test_block_count = true
|
30
|
+
obsolete_aggregates << aggr
|
31
|
+
end
|
32
|
+
expect(obsolete_aggregates.all? { |aggregate| aggregate.test_block_count == true }).to be_truthy
|
33
|
+
end
|
34
|
+
|
35
|
+
it "only retrieves aggregates older than min_event_distance" do
|
36
|
+
obsolete_aggregates = Sandthorn.obsolete_snapshots type_names: [AnAggregate], min_event_distance: 10
|
37
|
+
expect(obsolete_aggregates).to be_empty
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/tracing_spec.rb
CHANGED
@@ -1,121 +1,107 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sandthorn/event_inspector'
|
3
|
+
require 'sandthorn/aggregate_root_snapshot'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
class UsualSuspect
|
6
|
+
include Sandthorn::AggregateRoot
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def initialize full_name
|
9
|
+
@full_name = full_name
|
10
|
+
@charges = []
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
def charge_suspect_of_crime! crime_name
|
14
|
+
suspect_was_charged crime_name
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
private
|
18
|
+
def suspect_was_charged crime_name
|
19
|
+
@charges << crime_name
|
20
|
+
record_event crime_name
|
21
|
+
end
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
class Simple
|
25
|
+
include Sandthorn::AggregateRoot
|
26
|
+
end
|
27
|
+
module Go
|
28
|
+
def go
|
29
|
+
@foo = "bar"
|
30
|
+
record_event
|
31
|
+
end
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# simple.extend Sandthorn::AggregateRootSnapshot
|
34
|
+
describe "using a traced change" do
|
35
|
+
context "when extending an instance with aggregate_root" do
|
36
|
+
it "should record tracing if specified" do
|
37
|
+
simple = Simple.new
|
38
|
+
simple.extend Sandthorn::EventInspector
|
39
|
+
simple.extend Sandthorn::AggregateRootSnapshot
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
41
|
+
simple.extend Go
|
42
|
+
simple.aggregate_trace "123" do |traced|
|
43
|
+
traced.go
|
44
|
+
end
|
45
|
+
simple.events_with_trace_info.last[:trace].should eql("123")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
context "when not tracing" do
|
49
|
+
it "should not have any trace event info at all on new" do
|
50
|
+
suspect = UsualSuspect.new "Ronny"
|
51
|
+
event = suspect.aggregate_events.first
|
52
|
+
event[:trace].should be_nil
|
53
|
+
end
|
54
|
+
it "should not have any trace event info at all on regular event" do
|
55
|
+
suspect = UsualSuspect.new "Ronny"
|
56
|
+
event = suspect.aggregate_events.first
|
57
|
+
event[:trace].should be_nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
context "when changing aggregate in a traced context" do
|
61
|
+
let(:suspect) {UsualSuspect.new("Conny").extend Sandthorn::EventInspector}
|
62
|
+
it "should record modififier in the event" do
|
63
|
+
suspect.aggregate_trace "Ture Sventon" do |s|
|
64
|
+
s.charge_suspect_of_crime! "Theft"
|
65
|
+
end
|
66
|
+
event = suspect.events_with_trace_info.last
|
67
|
+
event[:trace].should eql "Ture Sventon"
|
68
|
+
end
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
# fork do
|
107
|
-
# UsualSuspect.aggregate_trace "Lars Krantz" do
|
108
|
-
# suspect = UsualSuspect.new("Sonny").extend Sandthorn::EventInspector
|
109
|
-
# event = suspect.events_with_trace_info.first
|
110
|
-
# event[:trace].should eql "Lars Krantz"
|
111
|
-
# sleep 1
|
112
|
-
# end
|
113
|
-
# end
|
114
|
-
# sleep 0.5
|
115
|
-
# s2 = UsualSuspect.new("Ronny").extend Sandthorn::EventInspector
|
116
|
-
# event = s2.events_with_trace_info.first
|
117
|
-
# event[:trace].should be_nil
|
118
|
-
# end
|
119
|
-
# end
|
70
|
+
it "should record optional other tracing information" do
|
71
|
+
trace_info = {ip: "127.0.0.1", client: "Mozilla"}
|
72
|
+
suspect.aggregate_trace trace_info do |s|
|
73
|
+
s.charge_suspect_of_crime! "Murder"
|
74
|
+
end
|
75
|
+
event = suspect.events_with_trace_info.last
|
76
|
+
event[:trace].should eql trace_info
|
77
|
+
end
|
78
|
+
end
|
79
|
+
context "when initializing a new aggregate in a traced context" do
|
80
|
+
it "should record modifier in the new event" do
|
81
|
+
UsualSuspect.aggregate_trace "Ture Sventon" do
|
82
|
+
suspect = UsualSuspect.new("Sonny").extend Sandthorn::EventInspector
|
83
|
+
event = suspect.events_with_trace_info.first
|
84
|
+
event[:trace].should eql "Ture Sventon"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
it "should record tracing for all events in the trace block" do
|
88
|
+
trace_info = {gender: :unknown, occupation: :master}
|
89
|
+
UsualSuspect.aggregate_trace trace_info do
|
90
|
+
suspect = UsualSuspect.new("Sonny").extend Sandthorn::EventInspector
|
91
|
+
suspect.charge_suspect_of_crime! "Hit and run"
|
92
|
+
event = suspect.events_with_trace_info.last
|
93
|
+
event[:trace].should eql trace_info
|
94
|
+
end
|
95
|
+
end
|
96
|
+
it "should record tracing for all events in the trace block" do
|
97
|
+
trace_info = {user_aggregate_id: "foo-bar-x", gender: :unknown, occupation: :master}
|
98
|
+
UsualSuspect.aggregate_trace trace_info do
|
99
|
+
suspect = UsualSuspect.new("Conny").extend Sandthorn::EventInspector
|
100
|
+
suspect.charge_suspect_of_crime! "Desception"
|
101
|
+
event = suspect.events_with_trace_info.last
|
102
|
+
event[:trace].should eql trace_info
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
120
106
|
|
121
|
-
|
107
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandthorn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lars Krantz
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-10-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -204,6 +204,7 @@ files:
|
|
204
204
|
- spec/different_driver_spec.rb
|
205
205
|
- spec/event_inspector_spec.rb
|
206
206
|
- spec/get_events_spec.rb
|
207
|
+
- spec/sandthorn_spec.rb
|
207
208
|
- spec/spec_helper.rb
|
208
209
|
- spec/tracing_spec.rb
|
209
210
|
homepage: ''
|
@@ -240,6 +241,7 @@ test_files:
|
|
240
241
|
- spec/different_driver_spec.rb
|
241
242
|
- spec/event_inspector_spec.rb
|
242
243
|
- spec/get_events_spec.rb
|
244
|
+
- spec/sandthorn_spec.rb
|
243
245
|
- spec/spec_helper.rb
|
244
246
|
- spec/tracing_spec.rb
|
245
247
|
has_rdoc:
|