aggro 0.0.1 → 0.0.2

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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.travis.yml +15 -0
  4. data/Gemfile +9 -0
  5. data/README.md +5 -1
  6. data/Rakefile +10 -0
  7. data/aggro.gemspec +8 -1
  8. data/lib/aggro.rb +191 -7
  9. data/lib/aggro/abstract_store.rb +12 -0
  10. data/lib/aggro/aggregate.rb +98 -0
  11. data/lib/aggro/aggregate_ref.rb +68 -6
  12. data/lib/aggro/attribute_dsl.rb +96 -0
  13. data/lib/aggro/binding_dsl.rb +45 -0
  14. data/lib/aggro/block_helper.rb +14 -0
  15. data/lib/aggro/channel.rb +37 -0
  16. data/lib/aggro/client.rb +12 -0
  17. data/lib/aggro/cluster_config.rb +57 -0
  18. data/lib/aggro/command.rb +16 -0
  19. data/lib/aggro/concurrent_actor.rb +26 -0
  20. data/lib/aggro/event_bus.rb +94 -0
  21. data/lib/aggro/event_dsl.rb +53 -0
  22. data/lib/aggro/event_proxy.rb +23 -0
  23. data/lib/aggro/event_serializer.rb +14 -0
  24. data/lib/aggro/file_store.rb +97 -0
  25. data/lib/aggro/file_store/reader.rb +21 -0
  26. data/lib/aggro/file_store/writer.rb +27 -0
  27. data/lib/aggro/handler/command.rb +60 -0
  28. data/lib/aggro/handler/create_aggregate.rb +42 -0
  29. data/lib/aggro/handler/get_events.rb +30 -0
  30. data/lib/aggro/handler/query.rb +60 -0
  31. data/lib/aggro/handler/start_saga.rb +56 -0
  32. data/lib/aggro/local_node.rb +28 -0
  33. data/lib/aggro/locator.rb +32 -0
  34. data/lib/aggro/message/ask.rb +16 -0
  35. data/lib/aggro/message/command.rb +36 -0
  36. data/lib/aggro/message/create_aggregate.rb +16 -0
  37. data/lib/aggro/message/endpoint.rb +16 -0
  38. data/lib/aggro/message/events.rb +24 -0
  39. data/lib/aggro/message/get_events.rb +16 -0
  40. data/lib/aggro/message/heartbeat.rb +16 -0
  41. data/lib/aggro/message/invalid_target.rb +20 -0
  42. data/lib/aggro/message/ok.rb +20 -0
  43. data/lib/aggro/message/publisher_endpoint_inquiry.rb +16 -0
  44. data/lib/aggro/message/query.rb +36 -0
  45. data/lib/aggro/message/result.rb +16 -0
  46. data/lib/aggro/message/start_saga.rb +28 -0
  47. data/lib/aggro/message/unhandled_operation.rb +20 -0
  48. data/lib/aggro/message/unknown_operation.rb +20 -0
  49. data/lib/aggro/message_parser.rb +10 -0
  50. data/lib/aggro/message_router.rb +26 -0
  51. data/lib/aggro/nanomsg_transport.rb +31 -0
  52. data/lib/aggro/nanomsg_transport/client.rb +35 -0
  53. data/lib/aggro/nanomsg_transport/connection.rb +98 -0
  54. data/lib/aggro/nanomsg_transport/publish.rb +17 -0
  55. data/lib/aggro/nanomsg_transport/publisher.rb +37 -0
  56. data/lib/aggro/nanomsg_transport/raw_reply.rb +18 -0
  57. data/lib/aggro/nanomsg_transport/raw_request.rb +18 -0
  58. data/lib/aggro/nanomsg_transport/reply.rb +17 -0
  59. data/lib/aggro/nanomsg_transport/request.rb +17 -0
  60. data/lib/aggro/nanomsg_transport/server.rb +84 -0
  61. data/lib/aggro/nanomsg_transport/socket_error.rb +20 -0
  62. data/lib/aggro/nanomsg_transport/subscribe.rb +27 -0
  63. data/lib/aggro/nanomsg_transport/subscriber.rb +82 -0
  64. data/lib/aggro/node.rb +29 -0
  65. data/lib/aggro/node_list.rb +39 -0
  66. data/lib/aggro/projection.rb +13 -0
  67. data/lib/aggro/query.rb +11 -0
  68. data/lib/aggro/saga.rb +94 -0
  69. data/lib/aggro/saga_runner.rb +87 -0
  70. data/lib/aggro/saga_runner/start_saga.rb +12 -0
  71. data/lib/aggro/saga_status.rb +29 -0
  72. data/lib/aggro/server.rb +88 -0
  73. data/lib/aggro/subscriber.rb +48 -0
  74. data/lib/aggro/subscription.rb +41 -0
  75. data/lib/aggro/transform/boolean.rb +16 -0
  76. data/lib/aggro/transform/email.rb +26 -0
  77. data/lib/aggro/transform/id.rb +34 -0
  78. data/lib/aggro/transform/integer.rb +22 -0
  79. data/lib/aggro/transform/money.rb +22 -0
  80. data/lib/aggro/transform/noop.rb +16 -0
  81. data/lib/aggro/transform/string.rb +16 -0
  82. data/lib/aggro/transform/time_interval.rb +24 -0
  83. data/lib/aggro/version.rb +1 -1
  84. data/spec/lib/aggro/abstract_store_spec.rb +15 -0
  85. data/spec/lib/aggro/aggregate_ref_spec.rb +63 -12
  86. data/spec/lib/aggro/aggregate_spec.rb +207 -0
  87. data/spec/lib/aggro/channel_spec.rb +87 -0
  88. data/spec/lib/aggro/client_spec.rb +26 -0
  89. data/spec/lib/aggro/cluster_config_spec.rb +33 -0
  90. data/spec/lib/aggro/command_spec.rb +52 -0
  91. data/spec/lib/aggro/concurrent_actor_spec.rb +44 -0
  92. data/spec/lib/aggro/event_bus_spec.rb +20 -0
  93. data/spec/lib/aggro/event_serializer_spec.rb +28 -0
  94. data/spec/lib/aggro/file_store/reader_spec.rb +32 -0
  95. data/spec/lib/aggro/file_store/writer_spec.rb +67 -0
  96. data/spec/lib/aggro/file_store_spec.rb +51 -0
  97. data/spec/lib/aggro/handler/command_spec.rb +78 -0
  98. data/spec/lib/aggro/handler/create_aggregate_spec.rb +64 -0
  99. data/spec/lib/aggro/handler/get_events_handler_spec.rb +45 -0
  100. data/spec/lib/aggro/handler/query_spec.rb +78 -0
  101. data/spec/lib/aggro/handler/start_saga_spec.rb +64 -0
  102. data/spec/lib/aggro/local_node_spec.rb +52 -0
  103. data/spec/lib/aggro/locator_spec.rb +61 -0
  104. data/spec/lib/aggro/message/ask_spec.rb +23 -0
  105. data/spec/lib/aggro/message/command_spec.rb +50 -0
  106. data/spec/lib/aggro/message/create_aggregate_spec.rb +28 -0
  107. data/spec/lib/aggro/message/endpoint_spec.rb +23 -0
  108. data/spec/lib/aggro/message/events_spec.rb +37 -0
  109. data/spec/lib/aggro/message/get_events_spec.rb +33 -0
  110. data/spec/lib/aggro/message/heartbeat_spec.rb +23 -0
  111. data/spec/lib/aggro/message/invalid_target_spec.rb +28 -0
  112. data/spec/lib/aggro/message/ok_spec.rb +27 -0
  113. data/spec/lib/aggro/message/publisher_endpoint_inquiry_spec.rb +23 -0
  114. data/spec/lib/aggro/message/query_spec.rb +50 -0
  115. data/spec/lib/aggro/message/start_saga_spec.rb +37 -0
  116. data/spec/lib/aggro/message/unhandled_operation_spec.rb +28 -0
  117. data/spec/lib/aggro/message/unknown_operation_spec.rb +28 -0
  118. data/spec/lib/aggro/message_parser_spec.rb +16 -0
  119. data/spec/lib/aggro/message_router_spec.rb +35 -0
  120. data/spec/lib/aggro/nanomsg_transport/socket_error_spec.rb +21 -0
  121. data/spec/lib/aggro/nanomsg_transport_spec.rb +37 -0
  122. data/spec/lib/aggro/node_list_spec.rb +38 -0
  123. data/spec/lib/aggro/node_spec.rb +44 -0
  124. data/spec/lib/aggro/projection_spec.rb +22 -0
  125. data/spec/lib/aggro/query_spec.rb +47 -0
  126. data/spec/lib/aggro/saga_runner_spec.rb +84 -0
  127. data/spec/lib/aggro/saga_spec.rb +126 -0
  128. data/spec/lib/aggro/saga_status_spec.rb +56 -0
  129. data/spec/lib/aggro/server_spec.rb +118 -0
  130. data/spec/lib/aggro/subscriber_spec.rb +59 -0
  131. data/spec/lib/aggro/subscription_spec.rb +50 -0
  132. data/spec/lib/aggro/transform/boolean_spec.rb +23 -0
  133. data/spec/lib/aggro/transform/email_spec.rb +13 -0
  134. data/spec/lib/aggro/transform/id_spec.rb +70 -0
  135. data/spec/lib/aggro/transform/integer_spec.rb +30 -0
  136. data/spec/lib/aggro/transform/money_spec.rb +34 -0
  137. data/spec/lib/aggro/transform/string_spec.rb +15 -0
  138. data/spec/lib/aggro/transform/time_interval_spec.rb +29 -0
  139. data/spec/lib/aggro_spec.rb +63 -19
  140. data/spec/spec_helper.rb +21 -2
  141. metadata +283 -3
@@ -0,0 +1,29 @@
1
+ module Aggro
2
+ # Public: Represents an aggro server node.
3
+ class Node < Struct.new(:id, :endpoint)
4
+ def client
5
+ @client ||= Aggro::Client.new(endpoint)
6
+ end
7
+
8
+ def publisher_endpoint
9
+ @publisher_endpoint ||= discover_publisher_endpoint
10
+ end
11
+
12
+ def to_s
13
+ id
14
+ end
15
+
16
+ private
17
+
18
+ def discover_publisher_endpoint
19
+ message = Message::PublisherEndpointInquiry.new(Aggro.local_node.id)
20
+ response = client.post(message)
21
+
22
+ if response.is_a? Message::Endpoint
23
+ response.endpoint
24
+ else
25
+ fail "Could not discover publisher endpoint for #{id}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module Aggro
2
+ # Public: Computes which nodes are responsible for a given aggregate ID.
3
+ class NodeList
4
+ DEFAULT_REPLICATION_FACTOR = 3
5
+
6
+ attr_reader :state
7
+
8
+ def add(node)
9
+ hash_ring << node
10
+
11
+ update_state
12
+ end
13
+
14
+ def nodes_for(id, replication_factor = default_replication_factor)
15
+ nodes
16
+ .cycle
17
+ .take(nodes.index(hash_ring.node_for(id)) + replication_factor)
18
+ .last(replication_factor)
19
+ end
20
+
21
+ def nodes
22
+ hash_ring.nodes.sort_by(&:id)
23
+ end
24
+
25
+ private
26
+
27
+ def default_replication_factor
28
+ [nodes.length, DEFAULT_REPLICATION_FACTOR].min
29
+ end
30
+
31
+ def hash_ring
32
+ @hash_ring ||= ConsistentHashing::Ring.new
33
+ end
34
+
35
+ def update_state
36
+ @state = Digest::MD5.hexdigest(nodes.map(&:to_s).join)[0..16].hex
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module Aggro
2
+ # Public: Mixin to turn a PORO into an Aggro projection.
3
+ module Projection
4
+ extend ActiveSupport::Concern
5
+
6
+ include BindingDSL
7
+ include EventDSL
8
+
9
+ def initialize(id)
10
+ Aggro.event_bus.subscribe(id, self)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Aggro
2
+ # Public: Mixin to turn a PORO into an Aggro query.
3
+ module Query
4
+ extend ActiveSupport::Concern
5
+ include AttributeDSL
6
+
7
+ def to_details
8
+ { name: model_name.name, args: serialized_attributes }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,94 @@
1
+ module Aggro
2
+ # Public: Mixin to turn a PORO into an Aggro saga.
3
+ module Saga
4
+ extend ActiveSupport::Concern
5
+
6
+ include AttributeDSL
7
+ include BindingDSL
8
+ include EventDSL
9
+
10
+ included do
11
+ generate_id :causation_id
12
+ generate_id :correlation_id
13
+ end
14
+
15
+ def bindings
16
+ @runner.bindings
17
+ end
18
+
19
+ def default_filters
20
+ { correlation_id: correlation_id }
21
+ end
22
+
23
+ def saga_id
24
+ @saga_id ||= SecureRandom.uuid
25
+ end
26
+
27
+ def start
28
+ fail 'Saga is not valid' unless valid?
29
+
30
+ promise = SagaStatus.new(saga_id)
31
+
32
+ message = Message::StartSaga.new Aggro.local_node.id, saga_id, to_details
33
+ response = primary_node.client.post message
34
+
35
+ if response.is_a? Message::OK
36
+ promise
37
+ else
38
+ fail 'Saga could not be started'
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def primary_node
45
+ @primary_node ||= Locator.new(saga_id).primary_node
46
+ end
47
+
48
+ def to_details
49
+ { name: model_name.name, args: serialized_attributes }
50
+ end
51
+
52
+ def reject(reason = nil)
53
+ fail 'Runner not set' unless @runner
54
+
55
+ @runner.reject reason
56
+ end
57
+
58
+ def resolve(value = nil)
59
+ fail 'Runner not set' unless @runner
60
+
61
+ @runner.resolve value
62
+ end
63
+
64
+ def transition(step_name, *args)
65
+ fail 'Runner not set' unless @runner
66
+
67
+ @runner.transition step_name, *args
68
+ end
69
+
70
+ class_methods do
71
+ def handler_for_step(step_name)
72
+ steps[step_name]
73
+ end
74
+
75
+ def handles_step?(step_name)
76
+ steps.key? step_name
77
+ end
78
+
79
+ def initial(step_name = nil)
80
+ step_name ? @initial = step_name : @initial
81
+ end
82
+
83
+ def step(step_name, &block)
84
+ steps[step_name] = block
85
+ end
86
+
87
+ private
88
+
89
+ def steps
90
+ Aggro.step_handlers[name]
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,87 @@
1
+ require 'aggro/saga_runner/start_saga'
2
+
3
+ module Aggro
4
+ # Private: Aggregate which runs saga processes.
5
+ class SagaRunner
6
+ include Aggregate
7
+
8
+ allows StartSaga do |command|
9
+ @details = command.details
10
+
11
+ @klass = ActiveSupport::Inflector.constantize command.name
12
+
13
+ @saga = @klass.new(@details).tap do |saga|
14
+ saga.instance_variable_set(:@saga_id, command.id)
15
+ saga.instance_variable_set(:@runner, self)
16
+ end
17
+
18
+ did.started state: @klass.initial
19
+
20
+ run_step @klass.initial
21
+ end
22
+
23
+ def bindings
24
+ @bindings ||= []
25
+ end
26
+
27
+ def cancel_bindings
28
+ bindings.each(&:cancel)
29
+ @bindings = []
30
+ end
31
+
32
+ def reject(reason)
33
+ did.rejected reason: reason
34
+
35
+ teardown
36
+ end
37
+
38
+ def resolve(value)
39
+ did.resolved value: value
40
+
41
+ teardown
42
+ end
43
+
44
+ def transition(step_name, *args)
45
+ cancel_bindings
46
+ did.transitioned state: step_name, args: args
47
+
48
+ run_step step_name, args
49
+ end
50
+
51
+ private
52
+
53
+ def did
54
+ @_context = @details
55
+ super
56
+ end
57
+
58
+ def run_step(step_name, args = [])
59
+ with_thread_ids do
60
+ handler = @klass.handler_for_step(step_name)
61
+
62
+ fail "Step '#{step_name}' does not exist" unless handler
63
+
64
+ @saga.send(:instance_exec, *args, &handler)
65
+ end
66
+ rescue => e
67
+ reject e.to_s
68
+ end
69
+
70
+ def teardown
71
+ @saga = nil
72
+ cancel_bindings
73
+ Aggro.event_bus.subscribe(@id, self)
74
+ end
75
+
76
+ def with_thread_ids
77
+ old_causation_id = Thread.current[:causation_id]
78
+ old_correlation_id = Thread.current[:correlation_id]
79
+ Thread.current[:causation_id] = @details[:causation_id]
80
+ Thread.current[:correlation_id] = @details[:correlation_id]
81
+ yield
82
+ ensure
83
+ Thread.current[:causation_id] = old_causation_id
84
+ Thread.current[:correlation_id] = old_correlation_id
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,12 @@
1
+ module Aggro
2
+ class SagaRunner
3
+ # Private: Command to start a SagaRunner.
4
+ class StartSaga
5
+ include Command
6
+
7
+ id :id
8
+ string :name
9
+ attribute :details
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module Aggro
2
+ # Public: Tracks the state of a saga as it processes.
3
+ class SagaStatus
4
+ include Projection
5
+ include Concurrent::Obligation
6
+
7
+ def initialize(id)
8
+ @state = :unscheduled
9
+ init_obligation
10
+ super
11
+ end
12
+
13
+ events do
14
+ def started
15
+ self.state = :pending
16
+ end
17
+
18
+ def rejected(reason)
19
+ set_state false, nil, reason
20
+ event.set
21
+ end
22
+
23
+ def resolved(value)
24
+ set_state true, value, nil
25
+ event.set
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,88 @@
1
+ module Aggro
2
+ # Public: Binds a transport endpoint and handles incoming messages.
3
+ class Server
4
+ RAW_HANDLER = :handle_raw
5
+
6
+ HANDLERS = {
7
+ Message::Command => :handle_command,
8
+ Message::CreateAggregate => :handle_create,
9
+ Message::GetEvents => :handle_get_events,
10
+ Message::Heartbeat => :handle_heartbeat,
11
+ Message::PublisherEndpointInquiry => :handle_publisher_endpoint_inquiry,
12
+ Message::Query => :handle_query,
13
+ Message::StartSaga => :handle_start_saga
14
+ }
15
+
16
+ def initialize(endpoint, publisher_endpoint)
17
+ @endpoint = endpoint
18
+ @publisher_endpoint = publisher_endpoint
19
+
20
+ @transport_server = Aggro.transport.server endpoint, method(RAW_HANDLER)
21
+ @transport_publisher = Aggro.transport.publisher publisher_endpoint
22
+ end
23
+
24
+ def bind
25
+ @transport_server.start
26
+ @transport_publisher.open_socket
27
+ end
28
+
29
+ def handle_message(message)
30
+ message_router.route message
31
+ end
32
+
33
+ def publish(message)
34
+ @transport_publisher.publish message
35
+ end
36
+
37
+ def stop
38
+ @transport_server.stop
39
+ @transport_publisher.close_socket
40
+ end
41
+
42
+ private
43
+
44
+ def handle_command(message)
45
+ Handler::Command.new(message, self).call
46
+ end
47
+
48
+ def handle_create(message)
49
+ Handler::CreateAggregate.new(message, self).call
50
+ end
51
+
52
+ def handle_get_events(message)
53
+ Handler::GetEvents.new(message, self).call
54
+ end
55
+
56
+ def handle_heartbeat(_message)
57
+ Message::OK.new
58
+ end
59
+
60
+ def handle_publisher_endpoint_inquiry(_message)
61
+ Message::Endpoint.new @publisher_endpoint
62
+ end
63
+
64
+ def handle_query(message)
65
+ Handler::Query.new(message, self).call
66
+ end
67
+
68
+ def handle_raw(raw)
69
+ handle_message MessageParser.parse raw
70
+ end
71
+
72
+ def handle_start_saga(message)
73
+ Handler::StartSaga.new(message, self).call
74
+ end
75
+
76
+ def message_router
77
+ @message_router ||= begin
78
+ MessageRouter.new.tap do |router|
79
+ HANDLERS.each { |type, sym| router.attach_handler type, method(sym) }
80
+ end
81
+ end
82
+ end
83
+
84
+ def publisher
85
+ @publisher ||= Publisher.new(local_node.publisher_endpoint)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,48 @@
1
+ module Aggro
2
+ # Public: Subscribes to topics at a given endpoint.
3
+ class Subscriber
4
+ RAW_HANDLER = :handle_raw
5
+
6
+ def initialize(endpoint, callable = nil, &block)
7
+ if callable
8
+ @callback = callable
9
+ elsif block_given?
10
+ @callback = block
11
+ else
12
+ fail ArgumentError
13
+ end
14
+
15
+ @transport_sub = Aggro.transport.subscriber endpoint, method(RAW_HANDLER)
16
+ @subscribed_topics = Set.new
17
+ end
18
+
19
+ def bind
20
+ @transport_sub.start
21
+ end
22
+
23
+ def handle_message(message)
24
+ @callback.call message.id, message.events if message.is_a? Message::Events
25
+ end
26
+
27
+ def stop
28
+ @transport_sub.stop
29
+ end
30
+
31
+ def subscribe_to_topic(topic)
32
+ return if @subscribed_topics.include? topic
33
+
34
+ @subscribed_topics << topic
35
+ @transport_sub.add_subscription message_prefix_for_topic(topic)
36
+ end
37
+
38
+ private
39
+
40
+ def handle_raw(raw)
41
+ handle_message MessageParser.parse raw
42
+ end
43
+
44
+ def message_prefix_for_topic(topic)
45
+ Message::Events::TYPE_CODE + topic
46
+ end
47
+ end
48
+ end