Skip to content

Add Prover Based ShutdownManager #489

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

Open
wants to merge 88 commits into
base: master
Choose a base branch
from

Conversation

baierd
Copy link
Contributor

@baierd baierd commented May 30, 2025

Most solvers allow either re-usable shutdown or we use shared environments that are created per prover, making them isolated enough to be shut down without stopping the context.

This PR adds a controlled way of shutting down only a single prover with the manager returned by our API IFF it is supported by the solver. These prover shutdown managers are all children of the context given shutdown manager.

This should only be merged after #501, is this branch is merged here.

@baierd baierd self-assigned this May 30, 2025
Copy link
Member

@kfriedberger kfriedberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I find the idea good. It matches our architectural guidelines and solves a clear problem. The PR needs some minor improvements.

* ProverEnvironment}.
* @throws UnsupportedOperationException if the solver does not support prover specific shutdown.
*/
ShutdownManager getShutdownManagerForProver() throws UnsupportedOperationException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to API change, better provide a default implementation that throws the UnsupportedException.

Additionally, his method is a plain getter. Why does a plain getter throw such an exception? The getter itself does not depend on solver-features that could be unsupported, but only on JavaSMT-internal code. Perhaps just remove the exception from signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I removed the exception from the signature and added a default implementation.

return getShutdownManagerForProverImpl();
}

protected ShutdownManager getShutdownManagerForProverImpl() throws UnsupportedOperationException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this method is not generic, we do not need it. just use inline it in the normal getShutdoenManagerForProver.
With a default implementation in the API, we can even remove this implmentation completely and just mark the overridden method as abstract.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I imagined that i need more solver specific code, but i didn't. I removed it. Thanks for the feedback!

return proverShutdownManager
.getNotifier()
.shouldShutdown(); // shutdownNotifer is defined in the
// superclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is formatted ugly, and it might not be understandable, please rephrase.

Is getNotifier() a "fast" method call or does it construct any objects? The callback is expected to be executed quite often from native solver code. We could store the notifier directly within the class and just reuse it.

Addtiionally, we already have a method for getting the manager. Why do we then use direct access here? Please use the getter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the comment, as the new default should be obvious.

getNotifier() does not construct any objects, it just returns the notifier that is created when the ShutdownManager is created. You can argue that it is faster to have the reference directly, but this is built once when the prover is created. I changed it so that the notifier is generally accessible directly in the superclass.

Thanks for the feedback!

@@ -119,7 +123,8 @@ private boolean readSATResult(Result resultValue) throws SolverException, Interr
return false;
} else if (resultValue == Result.UNSAT) {
return true;
} else if (resultValue == Result.UNKNOWN && shutdownNotifier.shouldShutdown()) {
} else if (resultValue == Result.UNKNOWN
&& proverShutdownManager.getNotifier().shouldShutdown()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it guaranteed that the manager always returns the same notifier-instance and the notifier always returns the same flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! The notifier is created final and only once, see here: private final ShutdownNotifier notifier = new ShutdownNotifier(this); The flag can not be changed once a shutdown is triggered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referencing some existing source code is no "guarantee".
The guarantee would be a JavaDoc specification for the ShutdownManager-API, which is missing:
https://sosy-lab.github.io/java-common-lib/api/org/sosy_lab/common/ShutdownManager.html#getNotifier()

A user can provide his own implementation of the shutdown-manager and change its behaviour.
I really would prefer to keep a local reference to this notifier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no intention of a guarantee that getNotifier() always returns the same instance, but there is definitively the intention that this does not matter and that all notifiers handed out by a manager always return the shutdown state of the manager, and that shouldShutdown() will keep returning true forever after it has returned true once.

Since the split in separate manager and notifier classes this was not fully written down in the JavaDoc, but I improved this now: sosy-lab/java-common-lib@108faa6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PhilippWendler i just noticed that there is/can be a delay in between the shutdown being requested and the children being informed. See the 2 tests here. Is this intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what exactly you are referring to. But if you have multi-threaded code, then you can of course never be sure when each of the threads is scheduled in relation to the others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but i would have expected that the chain of signals in the shutdown notifiers/managers is built in such a way that they are all informed in a synchronized way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with "synchronized way"? "synchronized" is not some magic global property, one can only synchronize against specific things.

If you want help, you need to be more specific. What is the sequence of operations that you are observing and that is unexpected/unwanted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was merely curious ;D
We discussed this offline sufficiently. Thank you!

@Override
protected ShutdownManager getShutdownManagerForProverImpl() throws UnsupportedOperationException {
return proverShutdownManager;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could move this method higher up in the hierarchy and just provide it in the abstract super-class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it completely, as we didn't really need it.

@@ -49,7 +49,7 @@ protected BoolectorAbstractProver(
this.manager = manager;
this.creator = creator;
this.btor = btor;
terminationCallback = shutdownNotifier::shouldShutdown;
terminationCallback = proverShutdownManager.getNotifier()::shouldShutdown;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for comparison to above: Here, we keep the same notifier and reuse it for all callback-calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build a new manager in every prover creation (see here) and then use this new managers notifier to request shutdowns. So we always use the one notifier bound to the one manager of the prover (that is a child of the contexts ShutdownNotifier)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now outdated due to the changes.

*/
private TerminationCallback getTerminationTest() {
return () -> {
proverShutdownManager.getNotifier().shutdownIfNecessary();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extract the reference to the notifier and just reuse.

Might look like this:

final ShutdownNotifier notifier = getShutdownManagerForProver().getNotifier()
return () -> {
  notifier.shutdownIfNecessary();
  return false;
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Thanks for the hint.

t.start();
assertThrows(InterruptedException.class, pe::isUnsat);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the tests checks whether the solver is usable after a shutdown.
None of the tests checks that, out of multiple parallel instances, if one is terminated, then all other provers remain running and deliver results.

Could you add such tests?

Copy link
Contributor Author

@baierd baierd Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just writing some tests and noticed that we do not really define how a solver (context and prover) should behave after a context-wide shutdown is requested.
Should prover creation throw a exception if the context is already shut down?
What operations are still usable and guaranteed to be correct/safe after a shutdown in your current prover?

@kfriedberger
Copy link
Member

This PR might provide a solution for a quite old issue: #100 :-)

@@ -119,7 +123,8 @@ private boolean readSATResult(Result resultValue) throws SolverException, Interr
return false;
} else if (resultValue == Result.UNSAT) {
return true;
} else if (resultValue == Result.UNKNOWN && shutdownNotifier.shouldShutdown()) {
} else if (resultValue == Result.UNKNOWN
&& proverShutdownManager.getNotifier().shouldShutdown()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referencing some existing source code is no "guarantee".
The guarantee would be a JavaDoc specification for the ShutdownManager-API, which is missing:
https://sosy-lab.github.io/java-common-lib/api/org/sosy_lab/common/ShutdownManager.html#getNotifier()

A user can provide his own implementation of the shutdown-manager and change its behaviour.
I really would prefer to keep a local reference to this notifier.

result = smtEngine.checkSat();
}
shutdownNotifier.shutdownIfNecessary();
proverShutdownManager.getNotifier().shutdownIfNecessary();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to avoid calling getNotifier() more than once here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just forgot to push everything. Should be removed now. Sorry for the delay.

- test context interrupt with subsequent prover usage
- test prover interrupt with subsequent feature usage (ignored because unfinished)
@baierd
Copy link
Contributor Author

baierd commented Jun 2, 2025

I cleaned up the default behavior of the prover API (isUnsat(), getModel(), getUnsatCore() etc.) so that they share the common prerequisites and no inconsistencies within the solver implementations exist.

I found 2 things i want to discuss:

  1. The JavaDoc of unsatCoreOverAssumptions() states that: Returns an UNSAT core (if it exists, otherwise {@code Optional.empty()}), over the chosen assumptions. Does NOT require the {@link ProverOptions#GENERATE_UNSAT_CORE} option to work. But we have and check for the option GENERATE_UNSAT_CORE_OVER_ASSUMPTIONS. So i guess the JavaDoc should be updated?
  2. the 2 public methods getSeqInterpolants0() and getTreeInterpolants0() are not named well in my opinion. I suggest better names like getSingletonSeqInterpolants)? (i am happy about better name suggestions ;D)

Comment on lines +94 to +96
* @implNote If the SMT solver is able to handle satisfiability tests with assumptions please
* consider implementing the {@link InterpolatingProverEnvironment} interface, and return an
* Object of this type here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment. The return type of the method is InterpolatingProverEnvironment, one always needs to return such an instance. What does this have to do with "solving with assumptions"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a comment for developers (e.g. students) so that they get info what to implement. We have this at several locations and for example helped me back in my bachelors thesis.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, having @implNotes is good. But what does this one want to say? Isn't InterpolatingProverEnvironment always required, and what does implementing it have to do with "satisfiability tests with assumptions"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not always required. The default impl throws a UnsupportedOperationException.
It is meant to lead the developer to the conclusion that this needs to be implemented if you want to interpolate. Usually students (in my experience) notice once they run the tests that things are missing ;D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but then the comment should still refer to interpolation and not satisfiability tests with assumptions.

BaierD added 17 commits July 21, 2025 17:25
…llegalArgumentException for msat_model_create_iterator()
…iterator into branch feature_prover_based_shutdown
…rruptedException for assumption removal and just throw it
…rruptedException and just throw it, including getUnsatCore()
@baierd
Copy link
Contributor Author

baierd commented Jul 31, 2025

@kfriedberger @PhilippWendler i now merged #501 into this to get the Solver/InterruptedExceptions for the model and prover API. I also added them to getUnsatCore(). So now nearly every method in the prover throws both. This allows us to remove all that special handling for the exceptions (except iterator() ;D).

@PhilippWendler
Copy link
Member

I just realized that ca14eff was even more important than I thought before.

If the intention of checkShutdownState() was "the method must not be called with shutdown already being triggered before", then this is not even possible. The following sequence can always happen:

  1. Caller checks that no shutdown is triggered and decides to call a method like getUnsatCore.
  2. Other thread triggers shutdown, e.g., due to timeout.
  3. checkShutdownState() is called and throws.

This means that no matter what an application does, if it uses the shutdown interface as it is intended, it would randomly crash with IllegalStateException, which would be a bug.

So the only way to handle shutdown requests properly can be to throw InterruptedException, as it is done now I guess?

But note that it is unnecessary to check for shutdown immediately when entering a method. Because of the asynchronicity no method can and needs to guarantee that it always notices a shutdown. The only place where one needs to check for shutdowns is in loops that execute for potentially long time.

@baierd
Copy link
Contributor Author

baierd commented Jul 31, 2025

I just realized that ca14eff was even more important than I thought before.

If the intention of checkShutdownState() was "the method must not be called with shutdown already being triggered before", then this is not even possible. The following sequence can always happen:

1. Caller checks that no shutdown is triggered and decides to call a method like `getUnsatCore`.

2. Other thread triggers shutdown, e.g., due to timeout.

3. `checkShutdownState()` is called and throws.

This means that no matter what an application does, if it uses the shutdown interface as it is intended, it would randomly crash with IllegalStateException, which would be a bug.

So the only way to handle shutdown requests properly can be to throw InterruptedException, as it is done now I guess?

But note that it is unnecessary to check for shutdown immediately when entering a method. Because of the asynchronicity no method can and needs to guarantee that it always notices a shutdown. The only place where one needs to check for shutdowns is in loops that execute for potentially long time.

Oh wow! Good catch! This means that all the API changes are actually fully justified.

@baierd baierd added this to the Release 6.0.0 milestone Jul 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants