Skip to content

Instantly share code, notes, and snippets.

@krisleech
Last active April 20, 2021 12:05
Show Gist options
  • Save krisleech/73b46d95e7ec257908a9b240069b3c11 to your computer and use it in GitHub Desktop.
Save krisleech/73b46d95e7ec257908a9b240069b3c11 to your computer and use it in GitHub Desktop.
Rails Event Store same event handler async and sync
class MyEventHandler
  include BackgroundEventHandler
  
  def call(event)  
  end
end

class MyOtherEventHandler
  include ForegroundEventHandler
  
  def call(event)
  end
end

in the background event handler call is alised to perform. This means the background event handler can be used sync by just calling call but this means causation and correlation ids are not set as they are wrapped in perform.

Sidekiq require to impliment perform, but calls perform_async based on https://railseventstore.org/docs/v2/subscribe/ we register multiple dispatchers, the first one to match is used. the match is based on the event handler responding to "perform_async" so we could make event handlers either sync or async by adding/removing this method, or patching respond_to? Maybe we don't include Sidekiq until the object is initalized, so by default it is sync.

e.g.

# this would work for an individual handler, but we register classes.
module EventHandler
  def async
    self.include(Background)
  end
end  

class MyEventHandler
  include EventHandler
  
  def call(event)
  end
end

MyEventHandler.async.call(event)

However we always call new on classes, we don't register objects.

Registering objects would mean the existing ones must all be stateless. This is proberbly true, TBC.

We could also use SimpleDelegator to do this:

AsyncHandler.new(MyHandler.new)

class AsyncHandler < SimpleDelegator
  def initialize(handler)
    handler.include(Background)
    super(handler)
  end
end

can we do this at class level:

AsyncHandler.new(MyHandler)

class AsyncHandler < SimpleDelegator
  def new(*args)
    super.tap { |handler| handler.include(Background) }
  end
end

could add a convience method:

module EventHander
  def self.async
    AsyncHandler.new(self)
  end
end

MyHandler.async
# class level (default) flag for sync/async and instance level one which has presidence

module EventHandler
  def self.async # class macro
    @async = true
  end
  
  def self.sync(*args)    
    instance = allocate
    instance.async = true
    instance.initialize(*args)
    instance
  end
  
  attr_accessor :async
  
  def initialize(*args)
    include Background if !sync || self.class.async
    super
  end
end  


class MyBackEventHandler < EventHandler
  async
  
  def call(event)
    # ...
  end
end

class MyForeEventHandler < EventHandler 
  def call(event)
    # ...
  end
end



MyBackEventHandler.new.call(event) # runs in background

MyBackForeHandler.new.call(event) # runs in foreground

how to run background in foreground for backfill:

MyBackEventHandler.new(async: true).call(event) # runs in foreground

MyBackEventHandler.sync.call(event) # runs in foreground

Problem (minor-ish), async and sync methods seem like oppisites but do very different things, one is a class macro, the other constructs an oject.

# class level (default) flag for sync/async and instance level one which has presidence

module EventHandler
  def self.async(*args)    
    instance = allocate
    instance.async = true
    instance.initialize(*args)
    instance
  end
  
  def self.sync(*args)
    instance = allocate
    instance.async = false
    instance.initialize(*args)
    instance
  end
  
  def initialize(*args)
    include Background if async
    super
  end
  
  attr_accessor :async # nil (falsey) by default
end  


class MyBackEventHandler < EventHandler 
  background
  
  def call(event)
    # ...
  end
end

class MyForeEventHandler < EventHandler 
  def call(event)
    # ...
  end
end



MyBackEventHandler.new.call(event) # runs in background

MyBackEventHandler.sync.call(event) # runs in background

MyBackForeHandler.async.call(event) # runs in foreground

how to run background in foreground for backfill:

MyBackEventHandler.sync.call(event) # runs in foreground

problem is we always call #new and this defaults to sync.

To do this we would need to include an sync/async flag when registering the event handler so we know if to initialize using sync or async.

# class level flag for sync/async and instance level one which has presidence
# Background (Sidekiq, ::RailsEventStore::AsyncHandler) added at runtime


module EventHandler
  def self.async # class macro
    @async = true
  end
  
  def self.sync # class marco
    @sync = false
  end
  
  def self.sync?
    !!@async
  end
  
  def sync
    @async = false
  end
  
  def async
    @async = true
  end
  
  def async?
    async || self.class.sync?
  end
  
  def initialize(*args)
    include Background if async?
    super
  end
end  


class MyBackEventHandler < EventHandler
  async
  
  def call(event)
    # ...
  end
end

class MyForeEventHandler < EventHandler 
  async # optional since default
  
  def call(event)
    # ...
  end
end



MyBackEventHandler.new.call(event) # runs in background

MyBackForeHandler.new.call(event) # runs in foreground

how to run background in foreground for backfill:

MyBackEventHandler.new.sync.call(event) # runs in foreground  (will not work, Background already included or not)

^ too late to call sync after new, since background has already been included.

# class level flag for sync/async and instance level one which has presidence
# Background (Sidekiq, ::RailsEventStore::AsyncHandler) added at runtime


module EventHandler
  def self.async # class macro
    @async = true
  end
  
  def self.sync # class marco
    @sync = false
  end
  
  def self.sync?
    !!@async
  end
  
  def sync
    @async = false
  end
  
  def async
    @async = true
  end
  
  def async?
    async || self.class.sync?
  end
  
  def self.new_sync(*args)
    instance = allocate
    insatnce.sync = true
    instance.initialize(*args)
    instance
  end
    
  
  def initialize(*args)
    include Background if async?
    super
  end
end  


class MyBackEventHandler < EventHandler
  async
  
  def call(event)
    # ...
  end
end

class MyForeEventHandler < EventHandler 
  sync # optional since default
  
  def call(event)
    # ...
  end
end



MyBackEventHandler.new.call(event) # runs in background

MyBackForeHandler.new.call(event) # runs in foreground

how to run background in foreground for backfill:

MyBackEventHandler.new_sync.call(event) # runs in foreground)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment