Skip to content

Instantly share code, notes, and snippets.

@krisleech
Last active April 20, 2021 12:05
Show Gist options
  • Select an option

  • Save krisleech/73b46d95e7ec257908a9b240069b3c11 to your computer and use it in GitHub Desktop.

Select an option

Save krisleech/73b46d95e7ec257908a9b240069b3c11 to your computer and use it in GitHub Desktop.

Revisions

  1. krisleech revised this gist Apr 20, 2021. 1 changed file with 8 additions and 21 deletions.
    29 changes: 8 additions & 21 deletions RES-async-sketches.md
    Original file line number Diff line number Diff line change
    @@ -1,33 +1,20 @@
    ```
    module BackgroundEventHandler
    prepend WithMetadata
    module WithMetadata
    def call(event)
    with_metadata(....)
    super
    end
    end
    end
    alias :perform :call
    end
    ```


    ```
    class MyEventHandler
    include BackgroundEventHandler
    def call(event)
    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.



  2. krisleech created this gist Apr 20, 2021.
    368 changes: 368 additions & 0 deletions RES-async-sketches.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,368 @@
    ```
    module BackgroundEventHandler
    prepend WithMetadata
    module WithMetadata
    def call(event)
    with_metadata(....)
    super
    end
    end
    end
    alias :perform :call
    end
    ```


    ```
    class MyEventHandler
    include BackgroundEventHandler
    def call(event)
    end
    end
    ```






    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:

    ```ruby
    AsyncHandler.new(MyHandler.new)

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


    can we do this at class level:


    ```ruby
    AsyncHandler.new(MyHandler)

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

    could add a convience method:

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

    MyHandler.async
    ```



    ```ruby
    # 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.




    ```ruby
    # 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.





    ```ruby
    # 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.




    ```ruby
    # 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)
    ```