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

Improve concurrency model using a single internal dispatch queue (close #820) #842

Merged
merged 11 commits into from
Dec 1, 2023

Conversation

matus-tomlein
Copy link
Contributor

This PR contains a refactoring of the internal classes in the tracker in order to improve concurrency (following recommendations in issue #820) and make it better handle concurrent accesses. This should help with several open bug reports: #774, #817, #816.

Generally, there are the following types of changes:

  • using serial dispatch queues in order to serialize access to properties within each object.
  • changing mutable to immutable properties to remove the need to protect the access.
  • some refactoring along the way.

Tracker

track function

I added an option to run it synchronously (using a function parameter). I realized this was necessary because of how Session is implemented. If the function was run async, the foreground/background event could be tracked with already changed session instead of the session at the time the event was tracked. Btw, this is also the case on the Android tracker which has an async track method, but in that case we added this workaround in the tracker.

We didn't catch this in the tests because the async behaviour was mocked by the MockDispatchQueueWrapper. I removed this wrapper, it's probably better not to mock the async behaviour because then we are not testing what is really happening.

TrackerData

To make it easier to reason about the tracker state and pass it around, I added a new wrapper class TrackerData. This holds the private properties of the tracker. It doesn't protect the access to the properties, the idea is that this will only be called from the Tracker private functions or from TrackerPayloadBuilder (see below).

The Tracker class still publishes the same properties to external classes (mainly TrackerControllerImpl) as it did before. However, it runs all the accesses on a serial dispatch queue in a sync manner.

TrackerPayloadBuilder

This is an internal class that basically extracts the payload building functions from the Tracker class. The reason for this was to make it easier to test and reason about these functions separately from the Tracker. Everything in this class is executed on the serial queue so can access the tracker data directly.

Subject

Here we just wrap access to the subject properties in a serial dispatch queue.

Session

I have added two serial dispatch queues here:

  • dispatchQueue used to protect access to all session data and session updates
  • backgroundUpdateQueue to queue background and foreground updates

Emitter

I added a serial queue to protect access to the internal properties, but have also tried to make some properties immutable so that we don't have to add unnecessary protections. In particular, I made the namespace, networkConnection, and eventStore immutable. This required a change in how they are instantiated – I moved all of them to be defined in the Emitter constructor.

DefaultNetworkConnection

Added a serial queue here as well to protect access to the mutable properties.

Other changes

EventSink test helper

This is just a helper in the tests to make it easier to check tracked events using plugins – instead of always defining a custom plugin as we did before, we can use this helper and be consistent in our tests.

@hakonk
Copy link

hakonk commented Nov 1, 2023

While I have not had time to look thoroughly at all the changes, I have a couple of thoughts:

  • Several places internal properties and methods are dispatched ad hoc via the sync function. This is a good improvement, but I believe it could have been simplified by making a distinction between the execution context for internal types and public ones, where the public interface explicitly dispatches the internal code on the internal queue, whether it be async or sync. Perhaps it would be beneficial to have an async public interface as many analytics events are fire and forget? dispatchPrecondition(.on(internalQueue)) can be used in internal types to ensure that code is indeed dispatched on the correct queue. If all internal code is dispatched on the same serial queue, there is no need for explicit synchronization. If there is a need for more than one quality of service, a queue hierarchy can be employed. I think it would be wise to gradually introduce more concurrency where needed instead of preemptively making the code highly concurrent as this introduces more complexity.

  • Making properties immutable is a good idea. That said, when mutating the properties of an instance of a reference type that is declared as let one may still get data races. If all code in internal classes is dispatched on the internal queue, this would not be a problem.

@matus-tomlein matus-tomlein force-pushed the issue/820-improve_concurrency_modeel branch 4 times, most recently from 9045bf7 to 5ab2545 Compare November 8, 2023 16:09
@matus-tomlein
Copy link
Contributor Author

Thank you for the suggestions @hakonk, that's super valuable!

I agree with starting with one internal queue and moving the dispatch to the public interface does make a lot of sense. I have tried to accomplish this in the following way:

  • Wrapped all the public controllers using wrapper classes that dispatch the work on the internal queue before executing it. You can see the ..IQWrapper classes in this folder.
  • Removed all synchronization from internal classes, everything is now executed on the single internal queue. There is only one exception – network requests are made on a separate background queue (in the Emitter class).
  • Added checks for dispatchPrecondition in a few relevant places to make sure we are on the internal queue.

While I agree with your point about having a "fire and forget" async interface for the tracker, I think this can only be applied to some of the tracker APIs. Most importantly, the track() function definitely needs to be async, probably also the flush in Emitter. However, I think functions that update the internal state of the tracker should by synchronous to provide a consistent experience for the user. For instance, I made all the setters for properties synchronous, so that when a user calls a setter and getter in sequence, the getter always returns the updated value.

If you have any more feedback, please let us know, really appreciate it!

@matus-tomlein matus-tomlein force-pushed the issue/820-improve_concurrency_modeel branch 5 times, most recently from 940b0d4 to 5d2f58d Compare November 14, 2023 09:34
@matus-tomlein matus-tomlein changed the title Improve concurrency model for internal Emitter, Tracker, Subject and Session classes (close #820) Improve concurrency model using a single internal dispatch queue (close #820) Nov 16, 2023
Copy link
Contributor

@mscwilson mscwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
Huge effort. This should make the tracker much safer against concurrency problems. The EventSink test improvements are nice too.

Sources/Core/Emitter/Emitter.swift Outdated Show resolved Hide resolved
Sources/Core/Tracker/Tracker.swift Outdated Show resolved Hide resolved
@matus-tomlein matus-tomlein force-pushed the issue/820-improve_concurrency_modeel branch from 1e97006 to 1c8b9cc Compare December 1, 2023 14:43
@matus-tomlein matus-tomlein merged commit 28a7164 into release/6.0.0 Dec 1, 2023
20 checks passed
@matus-tomlein matus-tomlein deleted the issue/820-improve_concurrency_modeel branch December 1, 2023 15:17
@AvdLee
Copy link

AvdLee commented Dec 15, 2023

@matus-tomlein is there a timeline for this to get released? We see numerous of crashes that are likely fixed with this PR. 🙏

@mscwilson
Copy link
Contributor

Hi @AvdLee, sorry to hear about the crashes. We have a couple of other features to finish first. We're aiming to release v6 by the end of January.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants