ActiveSupport: Refactor Notifications::Fanout to support interleaved start/finish#43241
ActiveSupport: Refactor Notifications::Fanout to support interleaved start/finish#43241theojulienne wants to merge 1 commit intorails:mainfrom
Conversation
|
Thanks for opening this. It's definitely something we should solve. I am concerned there will be other places not prepared for events to be out of order. For example AS::Subscriber seems to keep a thread local stack to group events. I'd love to remove this but we'll have to see how/where it is used https://github.com/rails/rails/blob/main/activesupport/lib/active_support/subscriber.rb#L135-L162 One thing I'd love to do, seeing as we're adjusting the public API a little, is use this as an opportunity to speed up and improve the notification handling. It's always bugged me how many duplicate Time objects we allocate and the levels of indirection (This is definitely outside the scope of what is needed here, but I'd like to try to spike on that this week). Another thing I'm wondering is whether we could use the |
Having looked a bit more I'd rather we not support this (specifically arbitrary data from listeners). Allowing state other than plain I'm wondering if a version of finish ( |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Summary
Currently the way the
ActiveSupport::Notifications::InstrumenterAPI is written, the#startmethod is allowed to return state that is then returned in#finish_with_stateto ensure instrumented timings and events are distributed to the same listeners, but it also presents an API that appears to support calling#startand#finish_with_statewithout any explicit ordering between multiple calls of these operations. However, the way thatActiveSupport::Notifications::Fanoutis currently implemented, only the list of listeners is maintained using this state, while any state relating to theEvented,Timed,MonotonicTimed, andEventObjectsubscribers are stored using a stack in thread-local storage.When instrumenting code that doesn't start and end within a single block, the public
#startand#finish_with_statemethods would seemingly support being interleaved, but in reality because of the usage of a thread-local stack they do not. This became a problem at GitHub during testing and instrumentation of IOPromises, but it would be the same for anything that needs to be instrumented that doesn't guarantee that start/finish are executed in the same (stack) order.An example of code that would currently misbehave:
This PR attempts to simplify this logic to just be implemented once in
Fanoutrather than every subscriber class, and to ensure that state is used to store all intermediate state if it is provided, but also retains backwards compatibility of using a thread-local stack when it is not provided.Other Information
The implementation in this PR allows continued usage of the
#finishvariant that relies on a thread-local stack. It does this by allowing the subscriber classes to return state, and storing that in a single stack, which is used if#finishis used rather than#finish_with_state. This means that code that uses#finish_with_state(including by extension#instrument) are guaranteed to get the correct state regardless of interleaving of events. Older code that might use#startand#finishwill use the thread-local stack and will not support interleaving of events (retaining the current behaviour).The listeners provided to
Fanoutcan also now implement#finish_with_stateand this will be preferenced over#finish. This allows backwards compatibility with custom listeners (as per the unchanged tests there), but allows custom class-based listeners to adopt the state as well, in case they want to have their state returned to them.