-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Dependency Injection Revisited #7637
Conversation
# Conflicts: # beacon-chain/blockchain/service.go # beacon-chain/rpc/service.go # beacon-chain/sync/initial-sync/service.go # beacon-chain/sync/subscriber.go # shared/service_registry.go # validator/web/server.go
Hey @rkapka , thanks for opening up this PR for discussion. I will give my initial thoughts on this:
Not sure I agree with this distinction, all our
I am not sure what difference this has functionally versus what we have now. We do also defer the cancelling inside a service's
This could be applied for any change to our services or child services. This change doesn't really prevent anyone from passing down non service based contexts to any child service routines(context.Backgorund). In fact this makes it harder to determine the source of all these contexts, previously with This is a pretty big change, and I have a hard time seeing the benefits of this change. The ordering of service shutdowns allows us to clean up all services properly. This PR took care of the major issues with it: This change also is making a distinction where there doesn't need to be one necessarily. |
Thank you for your point of view @nisdas
The main purpose of this PR is to simplify working with services. Although I agree that adding The reason for introducing this change is to shift the responsibility of resource cleanup from the service itself to the creator of the service. I believe that the code creating a service should be responsible for managing its lifetime. You don't have a
This argument is a bit unfair. There is no bulletproof solution - you can always pass
There are several functions in our code in the form of One more thing that I would like to add to the overall discussion is that I tried to look at the whole thing from a perspective of someone joining the project. Having the ability to just call UPDATE |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions. |
1 similar comment
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions. |
This pull request has been closed due to inactivity. Please reopen this pull request if you would like to continue working on it. |
Dependency Injection Revisited
Introduction
Composition Root
If you read some DI literature, you will stumble upon the concept of a Composition Root. A quick Google search reveals the following definition:
Our dependency graph consists of a node, which depends on several services, which in turn may depend on other services. If we follow the Composition Root pattern, then registering dependencies through
ServiceRegistry
should not take place in the node, but rather inmain.go
or another DI-specific file, and the node should be a part of the object graph. This is a topic for another time, though. For now registration does still take place in the node itself and I don't think it's that big of a deal.Who should manage an object's lifetime?
Quote from Dependency Injection Principles, Practices, and Patterns:
I think this definition can be extended from object graphs to single objects. Taking C# as an example, it has a predefined
using
keyword which can be used in this way:This is equivalent to calling a
writer.Dispose()
method after running all code inside theusing
statement. TheDispose()
method is defined on an interface and its main purpose is to release any resources managed by the object, such as DB connections. The main thing to notice here is that it's the code creating the object which invokesDispose()
, not the object itself. I think Java has a similar mechanism.Pull Request
Stop()
vscancel()
If we take a look at our services and the concept of releasing resources, such as goroutines, it is easy to see that such resources can be defined in multiple places:
New()
,Start()
, and other functions (which might even be invoked from within parent services). To me it seems pretty obvious that the equivalent of C#'sDispose()
iscontext.CancelFunc
, and thus cancelling should be used to provide resource cleanup. And because this cleanup should be called by the code which creates the object, which is node + registry in our case, then it's their responsibility. I think ofStop()
as performing closing actions on the service before its job is done, which has nothing to do with releasing resources (which should happen after the service's job is complete).Introducing
ServiceContext
This PR introduces a
ServiceContext
struct which contains a context and a cancel function.ServiceContext
is created in the node and registered alongside the service in the registry. TheCtx
field is passed directly toNew()
in the node and indirectly toStart()
andStop()
through the registry. This makes sure that allService
interface's functions use the same context, which can be cancelled from a single place. Cancelling takes place inside registry'sStopAll()
and is done after callingStop()
. It is not technically possible to cancel the context insideStop()
without passing the cancel function as a parameter (a new context acquired throughWithCancel()
will not cancel the parent context, which is the one passed intoNew()
etc.), which is essentially the same as cancelling from the outside afterStop()
is executed, only less readable.Services depending on other services
Sometimes a
Parent
service struct has aChild
service as a field. One important question arises: what context should be passed into aChild
's functionf()
when executingParent.Child.f()
? The answer is of course the parent context, simply flowing to the child through function calls - idiomatic Go at its best. Although this answer might be trivial, it is a result of moving contexts away from struct fields into function parameters in the PR. Having a context field allows to either pass the parent context as a parameter or use the child context directly inside the function (Child.ctx
). That's two ways of doing the same thing. And if the code is changed so that the child's context field no longer reuses the parent's context, then it is easy to introduce bugs because functions which used a context parameter will continue to work on the parent context.Closing thoughts
Even though this is a pretty radical change, I think we should give this design a try. If you look at the current state of services, you will see several patterns being used for cancellation as well as for creating contexts and reusing them between services and even inside one service. I believe that the proposed solution is more maintainable, easier to reason about and will make future development and DI enhancements simpler.