Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protobuf cannot be used with async handlers #228

Closed
5 tasks done
paneq opened this issue Mar 16, 2018 · 13 comments
Closed
5 tasks done

Protobuf cannot be used with async handlers #228

paneq opened this issue Mar 16, 2018 · 13 comments
Assignees
Labels
Milestone

Comments

@paneq
Copy link
Member

paneq commented Mar 16, 2018

because: protocolbuffers/protobuf#4391 we always use YAML and google-protobuf does not work with it right now.

  • Figure out
  • Mutation testing
  • Can we use Protobuf's ANY data-type to serialize metadata?
  • Update protobuf tutorial in a documentation
  • Update mapper in a documentation (no longer add_metadata)
@paneq paneq added this to the v0.20 milestone Mar 16, 2018
@paneq
Copy link
Member Author

paneq commented Mar 16, 2018

hmmm, but we already have repository.event_to_serialized_record which we can serialize it and repository.serialized_record_to_event which can deserialize it. The only problem is that we would now require something to be included/extended in Async handler classes.

Hmmm, actually right now we require YAML.load, in first line of background job class. From documentation:

class SendOrderEmail < ActiveJob::Base
  def perform(event)
    event = YAML.load(event)
    email = event.data.fetch(:customer_email)
    OrderMailer.notify_customer(email).deliver_now!
  end
end

Perhaps we should require:

class SendOrderEmail < ActiveJob::Base
  def perform(event)
    event = event_store.deserialize(event)
    # ...

That would probably make much more sense as we would be able to change it in any way in the future.

Or maybe:

class SendOrderEmail < ActiveJob::Base
  def perform(event_id, data, metadata, event_type)
    event = event_store.deserialize(data, metadata, event_type)
    # ...

nah... that does not look really good.

@paneq
Copy link
Member Author

paneq commented Mar 16, 2018

yeah, event should be {event_id:, data:, metadata:, event_type:} from SerializedRecord where every value is a String and ActiveJob will know how to handle it thanks to that.

@paneq
Copy link
Member Author

paneq commented Mar 17, 2018

Workaround: protocolbuffers/protobuf#4391 (comment)

@paneq
Copy link
Member Author

paneq commented Mar 17, 2018

It seems that I need to:

  • move mapper dependency higher and make it mandatory (because no matter what repo you use we need to know how to serialize/deserialize your events for async handlers.
  • pass mapper as dependency to ActiveJobDispatcher#call ? How? ie: call(subscriber, event, mapper) ? Requires changes to RubyEventStore::PubSub::Dispatcher as well
  • Calling every Event Repository method would require passing a mapper as it no longer would be a repo dependency.

So that's quite a lot of breaking changes.

@paneq
Copy link
Member Author

paneq commented Mar 17, 2018

Alternatively we expose event_to_serialized_record and serialized_record_to_event as public methods of a Repository and it would be its internal detail whether it delegates to mapper or not.

Maybe it would event make sense to have client.serialize(event) and client.deserialize(string) on top level?

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

Note's from today's meetup:

  • RES serialization - data vs metadata
    • Serializing whole event for sending over network
    • Serializing whole event for sending to background queue
    • Is metadata part of event or not?
    • Is metadata serialized and provided separately to data or not
    • do you need to use RubyEventStore::Event or not
      • Perhaps I should not have exchanged RubyEventStore::Event with Protobuf-event but only RubyEventStore::Event#data content?
        • but then what do you send over the wire? event_id + event_type, + data ?
    • Does protobuf needs to know EVENT_TYPE to deserialize DATA?
      • I think so. Suggest they need to be sent separately
    • When in doubt check GES?
    • worth checking also in Marten https://github.com/JasperFx/marten

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

hmmm

SerializedRecord#initialize(event_id:, data:, metadata:, event_type:)

vs

RubyEventStore::Event#initialize(event_id: SecureRandom.uuid, metadata: nil, data: nil)

I just realized they are almost identical...

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

Let's say we went with

client = Client.new(repository: RailsEventStoreActiveRecord::EventRepository.new(
  mapper: RubyEventStore::Mappers::Protobuf.new,
))
client.subscribe(->(ev){@ev = ev}, [ResTesting::OrderCreated])
event = RubyEventStore::Event.new(
  event_id: "f90b8848-e478-47fe-9b4a-9f2a1d53622b",
  data: ResTesting::OrderCreated.new(
    customer_id: 123,
    order_id: "K3THNX9",
))
client.publish_event(event, stream_name: 'test')

then we would also need to reimplement subscribe. In other words, our subscription is tight to class names and not event_types...

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

In other words, our subscription is tight to class names and not event_types

Here:

      def notify_subscribers(event)
        all_subscribers_for(event.class).each do |subscriber|
          @dispatcher.call(subscriber, event)
        end
      end

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

https://www.dropbox.com/s/tryu3dyk4jvus32/Screenshot%202018-03-30%2016.21.19.png?dl=0

Interesting to see what bunny (ruby client for rabbitmq does):

require 'bunny'

connection = Bunny.new
connection.start

ch = connection.create_channel
q = ch.queue('', :exclusive => true)
x  = ch.default_exchange

# set up the consumer
q.subscribe(:exclusive => true, :manual_ack => false) do |delivery_info, properties, payload|
  puts properties.content_type # => "application/octet-stream"
  puts properties.priority     # => 8

  puts properties.headers["time"] # => a Time instance

  puts properties.headers["coordinates"]["latitude"] # => 59.35
  puts properties.headers["participants"]            # => 11
  puts properties.headers["venue"]                   # => "Stockholm"
  puts properties.headers["true_field"]              # => true
  puts properties.headers["false_field"]             # => false
  puts properties.headers["nil_field"]               # => nil
  puts properties.headers["ary_field"].inspect       # => ["one", 2.0, 3, [{ "abc" => 123}]]

  puts properties.timestamp      # => a Time instance
  puts properties.type           # => "kinda.checkin"
  puts properties.reply_to       # => "a.sender"
  puts properties.correlation_id # => "r-1"
  puts properties.message_id     # => "m-1"
  puts properties.app_id         # => "bunny.example"

  puts delivery_info.consumer_tag # => a string
  puts delivery_info.redelivered? # => false
  puts delivery_info.delivery_tag # => 1
  puts delivery_info.routing_key  # => server generated queue name prefixed with "amq.gen-"
  puts delivery_info.exchange     # => ""
end

# publishing
x.publish("hello",
          :routing_key => "#{q.name}",
          :app_id      => "bunny.example",
          :priority    => 8,
          :type        => "kinda.checkin",
          # headers table keys can be anything
          :headers     => {
            :coordinates => {
              :latitude  => 59.35,
              :longitude => 18.066667
            },
            :time         => Time.now,
            :participants => 11,
            :venue        => "Stockholm",
            :true_field   => true,
            :false_field  => false,
            :nil_field    => nil,
            :ary_field    => ["one", 2.0, 3, [{"abc" => 123}]]
          },
          :timestamp      => Time.now.to_i,
          :reply_to       => "a.sender",
          :correlation_id => "r-1",
          :message_id     => "m-1")

sleep 1.0
connection.close

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

The interesting part is that payload (which is like our data) is provided separately from metadata-properties such as message id, correlation id etc (similar to what we put in metadata)

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

@paneq
Copy link
Member Author

paneq commented Mar 30, 2018

paneq added a commit that referenced this issue Mar 30, 2018
and protobuf generated class is only passed as Data !

this should make serialization for background jobs and networking easier.

Issue: #228
paneq added a commit that referenced this issue Mar 30, 2018
res_testing.OrderCreated instead of ResTesting::OrderCreated,
this is less ruby-related and more protobuf oriented. If that descriptor
name is used throught many apps, it should be easier to handle it
compared to ruby classes.

Issue: #228
paneq added a commit that referenced this issue Mar 31, 2018
by handling it on Proto wrapper level.

Seriously approaching this would require having a :mapper exposed
in :dispatcher which would require having it exposed in :broker but it
is already used in :repository.

So either we move the dependency higher and pass it down through many
layers, or make it a responsibility of :repository which can delegate to
:mapper if it wants to but then we would need to pass :repository around
to :dispatcher so it could call #event_to_serialized_record when enquing
a job and #serialized_record_to_event when dequeing.

I am not yet convinced about it.

Two additional thoughts:

* maybe repository should return RubyEventStore::SerializedRecord
  and then the migher layer (Client) would use the :mapper to transform
  it to the right event object.

* similarly when saving we could do the same, the other way, Client
  would already use mapper to serialize the event and repository would
  receive RubyEventStore::SerializedRecord.

* The benefit would be that we could get RAW data from repository, not
  transform it to domain events and stream over network.

  Current solution require getting domain events (which are
  deserialized) and then serializing them again to send over wire.

Issue: #228 #255
paneq added a commit that referenced this issue Apr 1, 2018
paneq added a commit that referenced this issue Apr 1, 2018
by using Proto wrapper and implementing YAML serialization which
delegates for proto serialization in case of event's :data field.

Issue: #228 #255 #257
@paneq paneq self-assigned this Apr 1, 2018
paneq added a commit that referenced this issue Apr 1, 2018
paneq added a commit that referenced this issue Apr 1, 2018
becuase of eql? behavior/bug from google-protobuf:
protocolbuffers/protobuf#4455

and bring back the original implementation to RubyEventStore::Event
which uses eql? instead of ==.

Issue: #228 #255 #257
paneq added a commit that referenced this issue Apr 2, 2018
paneq added a commit that referenced this issue Apr 2, 2018
@paneq paneq mentioned this issue Apr 3, 2018
paneq added a commit that referenced this issue Apr 4, 2018
as discussed in #255
to simplify potential serialization of it.

Issue: #255 #228
paneq added a commit that referenced this issue Apr 4, 2018
as discussed in #255
to simplify potential serialization of it.

Issue: #255 #228
paneq added a commit that referenced this issue Apr 7, 2018
paneq added a commit that referenced this issue Apr 8, 2018
@paneq paneq closed this as completed Apr 8, 2018
mpraglowski added a commit that referenced this issue Nov 27, 2018
YAML serialization is not used anymore when dispatching events to
async subscribers, instead we pass SerializedRecord serialized by your
mapper of choice which would be a protobuf in this case.

See more:
#228
b36a5ab
#363
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant