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

Relax the ServiceManager singleton nature #671

Merged
merged 14 commits into from
Nov 18, 2024

Conversation

breedx-splk
Copy link
Contributor

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 the OpenTelmetryRumBuilder difficult if not impossible. So this makes a first attempt at the following:

  1. Eliminate the static singleton nature of the 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 call ServiceManager.get() in an ad-hoc manner.
  2. Allows a custom ServiceManager instance to be injected into the OpenTelemetryRumBuilder in case the (power) user needs to override some defaults or provide their own implementation of a service.
  3. Several instrumentations need to get access to services during installation time, so the instrumentation API needed to change to accommodate this. As a result, we now have an InstallationContext data class in the instrumentation package that bundles up the app, the otel instance, and service manager instance.
  4. Unfortunately, some of the disk buffering configuration ended up hitting the static ServiceManager.get() singleton to get the PeriodicWorkService 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.
  5. The list of services that are implied/default is no longer coupled inside the 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:

  1. 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 a ServiceManager instance. Maybe we just make the interface no longer internal?
  2. As @surbhiia has pointed out elsewhere, the list of created and started services still doesn't map well or appropriately to the requirements of the actual instrumentation. In other words, there could still be services created and started that are not used by any instrumentation or exporters or anything.

In any case, I think the testability is greatly improved and the lifecycle is less complex.

@breedx-splk breedx-splk requested a review from a team as a code owner October 30, 2024 04:07
data class InstallationContext(
val application: Application,
val openTelemetry: OpenTelemetry,
val serviceManager: ServiceManager,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@surbhiia surbhiia Oct 31, 2024

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. :)

Copy link
Contributor

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.

Copy link
Contributor

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)
Copy link
Contributor

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).

Copy link
Contributor Author

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. :)

Copy link
Contributor

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?

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.

Copy link
Contributor

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 :-)

Copy link
Contributor

@bidetofevil bidetofevil left a 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) {
Copy link
Contributor

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)
Copy link
Contributor

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,
Copy link
Contributor

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.

@breedx-splk
Copy link
Contributor Author

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.

@breedx-splk breedx-splk merged commit 85e48b8 into open-telemetry:main Nov 18, 2024
3 checks passed
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