-
Notifications
You must be signed in to change notification settings - Fork 100
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
Feat: support for avoiding service loaders #664
Comments
Hello @marinier. First of all, thank you for taking the time to submit this feature request; I appreciate that you clearly laid out what you want and what you attempted to get it done. There is a lot to unpack here, I'll take it in parts. Service loader for Constraint Streams/JoinersThe reason why CS is loaded via a service loader is purely historical. There once used to be different implementations of CS, and this was the best way to make them independent. There is no such need anymore and so we may eventually remove the service loaders from this part of the codebase entirely; it's not high on the priority list, but it is also not too hard, so it may happen sooner rather than later. Service loaders in generalEven if/when we remove the service loaders from CS, they will still remain elsewhere in the codebase. For example, Enterprise Edition of the solver relies on service loaders heavily for its plug-and-play nature. We are unlikely to ever change this aspect. Which brings me to a larger question - what is wrong with service loaders? We occasionally hear that users want to avoid service loaders. Not too often, but we do hear it. Unfortunately, we never managed to find a specific reason for wanting to avoid service loaders, other than user preference. To the best of my knowledge, service loaders are:
Perhaps you will be able to add the missing piece to the puzzle for me. What are we missing? What is your reason to avoid service loaders? Finality of classesThis is a deliberate decision. It states our intent not to have these classes extended. We are very careful about our backwards compatibility, we go to great lengths not to break people as we evolve the solver. Exposing our internals for extension adds a layer of API surface we are not prepared to open. You might argue that this prevents users such as yourself from adapting the solver to your needs. And you would be right. But it also has another effect - it brought you to talk to us, explain your use case and we may now together figure out an actual solution as opposed to "a hacky workaround" as you put it. |
Thank you for your quick response! I had a breakthrough today that I think means there's a much simpler solution. The reason to avoid service loaders in my specific case is that it wasn't working. My project is a plugin for another android application that I have no control over, and it turns out that it loaded plugins using a thread that had a context class loader that was unable to find the timefold services file. However, the normal Android classloader is able to load that file just fine. So rather than avoid the classloader entirely, I just need to be able to specify which classloader to use. And this almost works in timefold -- I can call ServiceLoader<ScoreDirectorFactoryService> scoreDirectorFactoryServiceLoader =
ServiceLoader.load(ScoreDirectorFactoryService.class, classLoader); And now it works correctly! I'm hoping that passing in a custom class loader to the service loader is much less controversial than bypassing the service loader altogether. As a side note, it doesn't look like there's a So to summarize, the changes I'd like are:
|
I have added separate issues for the above requests. Thinking more about this issue, however, I think I'm realizing what the actual issue with service loaders is. The real issue is that, for the common case where you know exactly what you want and don't actually need to discover it at runtime, it adds a layer of complexity that makes debugging much more difficult. I have lost countless hours, not just on this project but on other projects that use service loaders for other purposes, trying to figure out why service loaders aren't working. It usually ends up being some kind of classpath or module path issue (e.g., somehow the desired class doesn't get packaged in a jar, or it has dependencies that don't get packaged, or a module doesn't I'm not suggesting that service loaders be dropped (there are definitely cases where you actually want discovery), I'm simply suggesting that for those cases where discovery isn't needed that it be much easier to bypass the mechanism. I can think of a couple ways to do this:
|
Is your feature request related to a problem? Please describe.
I'm running Timefold in an environment where I can't use a
ServiceLoader
. (This is on Android, and while service loaders work on Android in general, in my particular case it doesn't work for Timefold and I don't have access to the code that's causing the problem. Besides, I don't think wanting to avoid service loaders is necessarily an Android-specific issue.)The place where there's really a problem is in
ScoreDirectorFactoryFactory::decideMultipleScoreDirectorFactories
which uses aServiceLoader
to discover the different score director factory implementations and then picks one. In my case I just want to use Bavet, so no discovery is actually needed.Hacky workaround
My plan was to make my own
ScoreDirectorFactoryFactory
that overrode this method to just return aBavetScoreDirectorFactory
, like this:This went mostly smoothly except that the
config
member in the base class is private, so I had to make my own member for it.The next step was to subclass
DefaultSolverFactory
and override thebuildScoreDirectorFactory
method to useMyScoreDirectorFactoryFactory
. Unfortunately, this proved impossible. TheDefaultSolverFactory
class isfinal
so it can't be subclassed, and even if it wasn't thebuildScoreDirectorFactory
is private, so it can't be overridden (although I could have worked around that by changing the constructor in a subclass).I don't like it, but I made a class
MySolverFactory
that is a complete copy ofDefaultSolverFactory
except for the change to thebuildScoreDirectorFactory
.I then had to do similar things to use
Joiners
without usingServiceLoader
-- because it is also final and static, I had to makeMyJoiners
which manually loadsDefaultJoinerService
(again a complete copy ofJoiners
, which isn't great).And that's not the only problem -- I was using
SolutionManager
to print out debugging information about the solution, like this:But the
create
method is hard-coded to make aDefaultSolutionManager
, which is in turn hard-coded to expect aDefaultSolverFactory
, so this doesn't work withMySolverFactory
.DefaultSolutionManager
is also final, so I tried to follow the same pattern as above and create aMySolutionManager
class by copyingDefaultSolutionManager
and changing it to work withMySolverFactory
. Unfortunately, the code references theFitter
class, which is not public. So I've set aside the ability to print debugging information for now.In the end it works, which is fine for the short term, but not a good long-term solution.
Describe the solution you'd like
I'm not sure what the goals of the existing code are (why are the classes final in the first place?), so it's hard for me to know what the right solution is. Some possibilities include:
Solution 1: Make the various classes more friendly to extension. That is, don't make them final, and make the various members and methods protected instead of private. In places like
DefaultSolutionManager
's constructor don't hard-code a cast toDefaultSolverFactory
(or maybe this is ok ifDefaultSolverFactory
can be subclassed).Joiners
can't be subclassed because it's all static methods, but perhaps it could be given a static method to set whichJoinerService
to use in order to bypass theServiceLoader
.Solution 2: Add versions of some methods that allow the specific classes to be used to be specified. E.g., maybe
DefaultSolverFactory
could get a new constructor that takes an instance ofInnerScoreDirectorFactory
to use so it doesn't have to be discovered.JoinerService
could use the same approach as solution 1 in this case.Solution 3: In the config allow the specific implementation classes to be specified, and if they are just load them directly instead of using a service loader. This might go along with solution 2.
Solution 4: If loading from a service loader fails, fallback to a default implementation instead of failing (possibly making this fallback behavior configurable). This is not as flexible as solution 3, but if 95% of users are using the same services anyway (e.g.,
BavetConstraintStreamScoreDirectorFactoryService
+DefaultJoinerService
), then this might be a reasonable thing to do and it's probably much easier than the above solutions.Describe alternatives you've considered
The workaround I described above works, but it's not going to be maintainable long-term. That is, I don't want to have to update my versions of the classes every time the timefold versions change. It also just feels wrong, like I'm doing something that you tried to prevent for some reason. I just really want there to be a right way to do this.
Additional context
N/A
The text was updated successfully, but these errors were encountered: