-
Notifications
You must be signed in to change notification settings - Fork 42
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
Relax the ServiceManager singleton nature #671
Relax the ServiceManager singleton nature #671
Conversation
… setter with same sane defaults.
…face and into impl
data class InstallationContext( | ||
val application: Application, | ||
val openTelemetry: OpenTelemetry, | ||
val serviceManager: ServiceManager, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the application and serviceManager nullable as they're not needed in all instrumentations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly, but I think we mostly want to use the same context when installing all instrumentations at startup -- in which case some of the instrumentations will need some of these, so they won't be null.
I think it's simpler if instrumentations just ignore (don't use) the parts they don't need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense! We can perhaps provide a getter for a no-op ServiceManager instance to support the use cases when a user is utilizing one/some of the many instrumentations like the okhttp3 instrumentation that does not require a ServiceManager?
Or we can skip that too assuming such users can bypass the AndroidInstrumentation interface and directly call the underlying code under install method but providing a no-op could be beneficial for users who want to leverage the
AndroidInstrumentation interface too for wrapping in their own discover instrumentations feature. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of dealing with nulls, why not make them interfaces that we can provide Fakes to during a test?
If it's all Kotlin, then nulls are fine if it's handled downstream in only a few key places, but because there's still so much Java, I'd go with Fake or no-op implementations so we never ever get NPEs, but also will be able to bypass this during tests. Another advantage of that is there's no need to mock any of these during tests - just instantiate a the fake and you're good to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on yesterday's SIG meeting, we should try to find a way to allow instrumentations to reuse services/utilities without having to expose them as public apis. There's more details about it all on this issue.
application: Application, | ||
openTelemetry: OpenTelemetry, | ||
) | ||
fun install(ctx: InstallationContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea. I still need to take a deeper look at the whole PR so I can provide a better review overall, though this part reminded me of this other PR which makes it a bit confusing to me because there it seemed like we wanted to avoid passing core-specific types to instrumentations? I'm ok with doing so btw, I just want to make sure what's the overall consensus of the kind of use cases we want to support for instrumentations (whether we need them to be fully independent or not).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And when you say "core-specific types" in this case you mean both the InstallationContext
itself but also the ServiceManager
contained within?
In any case, the api needs to have something to pass to install()
, and instrumentations might want to leverage the consistent way we've started setting up to get at services. I'm open to other ways that don't involve global/static/singletons. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And when you say "core-specific types" in this case you mean both the
InstallationContext
itself but also theServiceManager
contained within?
Yes, that's what I was thinking.
In any case, the api needs to have something to pass to install(), and instrumentations might want to leverage the consistent way we've started setting up to get at services. I'm open to other ways that don't involve global/static/singletons. :)
Makes sense, we still need to pass something to instrumentations, and I like this idea of passing a single object of ours because it means that we could add more tools in the future if needed without having to change the signature of install()
.
I think the solution to this will become clearer once we fully understand the problem, if it's a matter of having "zero dependency" on anything that's outside of the upstream java repo, then things might get quite complicated, however, if it's just a matter of package size concerns and having a lot of code from a dependency that might not be fully used, such as the Android core, then it's probably a matter of extracting the instrumentation-related files out of core, as @surbhiia mentioned here, where we can also add a new type like InstallationContext
. If I understood correctly, I think the problem is the latter, so maybe we can get away with just moving this type into the separate "instrumentation API" module. Now, what to do about the service manager is another issue though, I hope we can discuss the details properly in a SIG meeting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree that there are probably several layers of functionality we can split this out into that will allow the interfaces, singletons, and config objects be more decoupled, it may still be useful to maintain this uber object for init that references all the layers for convenience - the alternative is to have N different objects and have them passed in separately. At that point, it's more of an aesthetic choice, and I don't particular care too much :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. The change moves the project ahead, so I'm good with it overall (see specific comments for non-blocking suggestions)
CacheStorage storage = serviceManager.getCacheStorage(); | ||
@NonNull | ||
private ServiceManager getServiceManager() { | ||
if (serviceManager == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory this should be called only from the main thread, but probably doesn't hurt to synchronize the init/set?
application: Application, | ||
openTelemetry: OpenTelemetry, | ||
) | ||
fun install(ctx: InstallationContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree that there are probably several layers of functionality we can split this out into that will allow the interfaces, singletons, and config objects be more decoupled, it may still be useful to maintain this uber object for init that references all the layers for convenience - the alternative is to have N different objects and have them passed in separately. At that point, it's more of an aesthetic choice, and I don't particular care too much :-)
data class InstallationContext( | ||
val application: Application, | ||
val openTelemetry: OpenTelemetry, | ||
val serviceManager: ServiceManager, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of dealing with nulls, why not make them interfaces that we can provide Fakes to during a test?
If it's all Kotlin, then nulls are fine if it's handled downstream in only a few key places, but because there's still so much Java, I'd go with Fake or no-op implementations so we never ever get NPEs, but also will be able to bypass this during tests. Another advantage of that is there's no need to mock any of these during tests - just instantiate a the fake and you're good to go.
There's still a lot of open discussion in this PR, but it's been approved for 2 weeks without any blocking reviews, so I'm going to merge to try and keep momentum. Like always, happy to continue revising incrementally as we see fit. |
As noted elsewhere and discussed in the Android SIG today, the static singleton nature of the
ServiceManager
both causes modularity challenges and makes testing in vendor distros that wrap theOpenTelmetryRumBuilder
difficult if not impossible. So this makes a first attempt at the following:ServiceManager
. This primarily means that any class that depends on this must have it injected (via construction). There is still only one instance used at runtime, but no longer can classes reach out statically to randomly callServiceManager.get()
in an ad-hoc manner.ServiceManager
instance to be injected into theOpenTelemetryRumBuilder
in case the (power) user needs to override some defaults or provide their own implementation of a service.InstallationContext
data class in the instrumentation package that bundles up the app, the otel instance, and service manager instance.ServiceManager.get()
singleton to get thePeriodicWorkService
when creating the default implementations of the (from disk) export scheduler. I decided that having functional components as "configuration" was probably stupid, so I just pulled them out all the way up to the builder.ServiceManager
interface, but instead fixed in the sole implementation:ServiceManagerImpl
.Two things that this PR does not yet do that will need to be addressed in follow-up efforts:
ServiceManager
interface is still marked as internal and lives in an internal package. This is problematic because the OTRB now has a public interface method that can take aServiceManager
instance. Maybe we just make the interface no longer internal?In any case, I think the testability is greatly improved and the lifecycle is less complex.