-
Notifications
You must be signed in to change notification settings - Fork 138
Description
Consider the following app structure (code is simplified):
// database layer component, depends on conn pool and river to insert jobs transactionally
store := postgres.NewStore(db, riverClient)
// ...
// Worker implementation that uses store
worker := some.NewWorker(store)
// ...
// River client which depends on workers
riverClient := river.NewClient(river.Config{Workers: workers})As you see, there is a dependency injection loop here: store depends on river client to insert jobs, but river client depends on workers, and worker depends on store.
I believe this situation is not unique and should be quite common for many apps.
One solution we can use currently is set river client into store later, like this:
riverClient := river.NewClient(river.Config{Workers: workers})
store.SetRiverClient(riverClient)But this is a step away from dependency injection principles, as constructor should be sufficient.
Another solution that comes to mind is to use two clients:
// This client is used to insert jobs
riverClient := river.NewClient(river.Config{Workers: nil})
store := postgres.NewStore(db, riverClient)
// This client manages workers
riverWorkersClient := river.NewClient(river.Config{Workers: workers})
riverWorkersClient.Start()The drawback is we losing validation.
Maybe I'm not seeing a better solution to this, could you suggest?
I guess this problem is a consequence of the fact that river.Client is too thick. Maybe it would be reasonable to separate job producer from workers, so the first component is responsible for creating jobs, while the second one is all about managing work. This way we can properly solve dependency issues, in fact, similar projects (like asynq already designed the API this way.