Interface EvaluationContext

All Known Subinterfaces:
ActionContext

public sealed interface EvaluationContext permits ActionContext (not exhaustive)
What a condition is evaluated against.

Implemented by the engine, which passes it to a language. It's sealed, so a language can't implement it; a language's unit tests create one with io.github.brantunger.unruly.test.LanguageTestContexts, from the unruly-engine-test artifact. Because only the engine implements it, a later release can add methods to it without breaking languages.

Identity: a context equals only itself, however its facts change and whatever another run's facts are, and its hashCode() reads neither the facts nor, for an ActionContext, the output object. A run passes one evaluation context to every condition, and a new action context to each action, so the two are different objects. A Session serves one run at a time and later runs reuse it, and no call to it marks where a run starts or ends, so state a run leaves in a session is still there for the next run; state for one run belongs in runScoped(java.lang.Object, java.util.function.Supplier<? extends T>), or, if it holds a resource that must be released, such as a runtime's context or interpreter opened for the run, in runScopedClosing(java.lang.Object, java.util.function.Supplier<? extends T>), which closes it when the run ends. A map keyed on contexts must not keep them alive, as a WeakHashMap doesn't.

  • Method Summary

    Modifier and Type
    Method
    Description
    @Nullable Instant
    Returns when the run must stop, from the timeout its engine or its run call was given: when the run started, on the system clock, plus its timeout.
    Map<String,@Nullable Object>
    Returns the values of the run's facts by name.
    boolean
    Returns whether the run must stop, because its thread has been interrupted or it has passed its deadline.
    Has action run once when the run passes its deadline, unless the registration returned is closed first.
    default <T> T
    runScoped(Object key, Supplier<? extends T> init)
    Returns the value kept under key for this run, making it with init the first time any condition or action of the run asks for it.
    default <T extends AutoCloseable>
    T
    runScopedClosing(Object key, Supplier<? extends T> init)
    Returns the value kept under key for this run, as runScoped(java.lang.Object, java.util.function.Supplier<? extends T>) does, and closes it when the run ends.
    default Instant
    Returns when the run started, by the engine's RulesEngineBuilder.clock(...): the one instant the run read from it, which judged every rule's validFrom and validTo, and which the run's listeners get as RunContext.startedAt().
    default Set<String>
    Returns the tags the run was given with RunOptions.withTags(...): the run uses only the rules that carry at least one of them, and skips the others.
    default Duration
    Returns how long the run has left before it must stop, measured as the engine measures it.
  • Method Details

    • facts

      Map<String,@Nullable Object> facts()
      Returns the values of the run's facts by name.
      Returns:
      A read-only map, whose values can be null; writing to it throws UnsupportedOperationException
    • isCancelled

      boolean isCancelled()
      Returns whether the run must stop, because its thread has been interrupted or it has passed its deadline.

      The engine checks this itself before each condition and each action, and again when each one returns, so a language that evaluates an expression and returns needn't. A language whose expressions can stop part-way polls it, or maps it to its own cancellation, so a long-running expression stops too; one whose runtime can be stopped only from outside registers an action with onCancel(Runnable) for the deadline. Returning any value is enough: the engine stops the run as soon as the expression returns, whatever it returned. Throwing an exception once the run is cancelled stops the run the same way; an Error, or an exception with one anywhere in its cause chain, such as one wrapping what a fact's Java code threw, is still that rule's failure. A language that can't stop inside an expression runs it to its end, and the run stops when it returns.

      Returns:
      true if the run must stop
    • deadline

      @Nullable Instant deadline()
      Returns when the run must stop, from the timeout its engine or its run call was given: when the run started, on the system clock, plus its timeout. So it compares with Instant.now(), and shows the deadline a person or a log expects.

      It is for showing. What stops the run is measured with a monotonic clock from when the run started, so a step of the system clock while the run goes on moves Instant.now(), but not when the run stops. To time a language's own work, use timeLeft(), which is exact; isCancelled() says whether the run must stop.

      Returns:
      The deadline, or null if the run has none
    • timeLeft

      default Duration timeLeft()
      Returns how long the run has left before it must stop, measured as the engine measures it. A language that gives a call of its own a timeout, such as a script it runs on another thread, gives it this much time; working it out from deadline() and Instant.now() instead is off by as much as the system clock has been stepped since the run started.

      A run without a deadline has Duration.ofNanos(Long.MAX_VALUE), about 292 years, and a run whose deadline is further away than that has a little less: so the time left always converts with Duration.toNanos() or Duration.toMillis() without overflowing. The time left says nothing about an interrupt: isCancelled() answers for both.

      Returns:
      The time left: positive while the deadline is ahead, Duration.ZERO once it has passed, and at most Duration.ofNanos(Long.MAX_VALUE)
    • startedAt

      default Instant startedAt()
      Returns when the run started, by the engine's RulesEngineBuilder.clock(...): the one instant the run read from it, which judged every rule's validFrom and validTo, and which the run's listeners get as RunContext.startedAt(). A condition that compares against the current time should compare against this, so it agrees with the rules the run selected and with a test's fixed clock. It isn't comparable to deadline(), which is measured with the system clock. A nested run reads the clock again, so its contexts have an instant of their own.
      Returns:
      The instant the run started at
    • tags

      default Set<String> tags()
      Returns the tags the run was given with RunOptions.withTags(...): the run uses only the rules that carry at least one of them, and skips the others. They are the run's RunContext.tags(), the same set, so a language can bind them to a name its expressions read without copying them. A nested run has tags of its own.
      Returns:
      The run's tags: in String order, unmodifiable, and empty if the run uses every rule
    • onCancel

      default CancelRegistration onCancel(Runnable action)
      Has action run once when the run passes its deadline, unless the registration returned is closed first. It is for a language whose runtime can be stopped part-way only from outside the expression, by setting a flag or cancelling the runtime, so it needs no timer of its own: the action does that, and the expression stops.

      The action runs while the expression runs, on a pooled daemon platform thread, or on a virtual thread of its own when the pool can't start one, so it must be thread-safe; if no thread can be started for it, as when the JVM is out of memory, that is logged and the action never runs. If the engine's cancel timer, the thread that waits for the deadline, can't be started, this method throws instead, and nothing is registered. The action's thread inherits none of the run's inheritable thread-locals, and has no context class loader. A pooled thread runs later actions, of any run or engine, and after each resets only its context class loader and interrupt status, so the action must remove any ThreadLocal value or MDC entry it sets, or the next action on that thread sees it. The action should be short, such as setting a flag that the runtime reads: one still running, or not yet started because no thread was free, a second after the timer handed it over is logged at WARN, though a slow action delays no other, of this run or any other. What it throws, or why no thread could be started for it, is logged and ignored: at ERROR if it is, or carries, a fatal Error, as defined next, and at WARN otherwise. If the deadline has already passed, the action runs on this thread before this method returns, and a fatal Error it throws, a VirtualMachineError such as OutOfMemoryError but not a StackOverflowError nor an InternalError that wraps one (what Throwables.isFatal(java.lang.Throwable) tells, leaving out what Throwables.isOverflow(java.lang.Throwable) reads as an overflow), or an exception that carries one (what Throwables.fatalError(java.lang.Throwable) finds), is thrown from this method instead. A run without a deadline registers nothing: the registration returned does nothing when closed. Passing the deadline stops the run as isCancelled() describes, and never interrupts its thread, so neither does the action unless it interrupts it itself. An interrupt never runs the action: isCancelled() still answers for both.

      The deadline is the one deadline() shows, measured as timeLeft() measures it, so a nested run's is no later than the run it was started from. Every context of a run, the evaluation context and each action context, registers against the same deadline. The run closes the registrations still open when it ends, as it closes the values kept with runScopedClosing(java.lang.Object, java.util.function.Supplier<? extends T>), and none can be registered after that. Closing doesn't wait for an action that has started, so an action whose deadline passed just as its expression or its run ended may still run just after, even once the run has returned. A language that reuses its runtime therefore gives each expression a stop flag of its own, made fresh for it and registered and closed with it, or checks isCancelled() before it acts on the flag, so a late action can't stop a later expression.

      Parameters:
      action - What to run when the run passes its deadline
      Returns:
      The registration, whose CancelRegistration.close() stops the action from running if it hasn't started
      Throws:
      NullPointerException - if action is null
      IllegalStateException - if the run has ended, or its registrations are being closed
      VirtualMachineError - the fatal error the action threw, or carried, when the deadline had already passed and the action ran on this thread; or the error, such as an OutOfMemoryError, with which starting the engine's cancel timer thread failed, in which case nothing is registered
    • runScoped

      default <T> T runScoped(Object key, Supplier<? extends T> init)
      Returns the value kept under key for this run, making it with init the first time any condition or action of the run asks for it. So a language that converts the facts before it evaluates an expression, such as with FactProperties.toData(java.lang.Object, int), can convert them once per run rather than once per expression.

      Every condition and action of one run() shares the values: the evaluation context and each action context of the run return the same value for a key. A nested run has values of its own, and the next run starts with none. A key the language owns, such as its compiler, keeps its values apart from another language's. The same key must always hold the same type, or the caller gets a ClassCastException.

      The value is made when it is first asked for, and isn't made again during the run, so it doesn't see a change that Java code makes to a fact later in the run. The engine keeps the values until the run returns, and then lets them go without closing them: a resource a language opens for one run belongs in runScopedClosing(java.lang.Object, java.util.function.Supplier<? extends T>), which closes it when the run ends, and one it keeps from run to run in its Session. Another thread can ask for a value too, such as a script the language runs on another thread: a request for a key whose init is running on another thread throws IllegalStateException rather than waiting for it. If init throws, nothing is kept, and the next call for the key calls its init again. An init can ask for other keys, but not for its own: that throws IllegalStateException, rather than recursing or making the value twice.

      Type Parameters:
      T - The value's type
      Parameters:
      key - The key, compared with Object.equals(Object)
      init - Makes the value the first time the run asks for key
      Returns:
      The value kept under key, never null
      Throws:
      NullPointerException - if key or init is null, or init returns null
      IllegalStateException - if the init of key is running, so it asked for its own key or is running on another thread, or the run keeps a value under key with runScopedClosing(java.lang.Object, java.util.function.Supplier<? extends T>)
    • runScopedClosing

      default <T extends AutoCloseable> T runScopedClosing(Object key, Supplier<? extends T> init)
      Returns the value kept under key for this run, as runScoped(java.lang.Object, java.util.function.Supplier<? extends T>) does, and closes it when the run ends. It is for a resource a language opens for one run and must release, such as a runtime's context or interpreter, or a connection the run's expressions share.

      The engine closes the values once the run has ended, however it ends: it returns, fails, passes its deadline or is interrupted. It closes them on the thread that ran it, after the run's last listener call and before run() returns. The run still holds its copy of the rules then, so no other run uses its sessions until its values are closed, and no session of the copy is closed before them. They are closed in the reverse of the order they were made, so a value whose init asked for another is closed before that one, and each is closed whatever the others throw. What a close() throws is handled as what a Session.close() throws is: it is logged at WARN, and the run's outcome stands, unless it is a fatal Error, such as an OutOfMemoryError. Once every value is closed, run() throws that error in place of the result, or of a failure of the run that isn't fatal, which the error carries as a suppressed exception; a run that failed with a fatal error of its own throws its own, carrying the one from close(). A nested run closes its own values when it ends.

      A key is either closing or not: asking with this method for a key that the run keeps a value under with runScoped(java.lang.Object, java.util.function.Supplier<? extends T>), or the other way round, throws IllegalStateException. So does asking for a value with this method once the run's values are being closed, or have been, such as from a value's close() or through a context kept past its run, as that value would never be closed. This method can be called from another thread while the run ends: the value is then either closed with the run's others, or refused with IllegalStateException, and never left open, unless the run's end fails twice, as it can when the stack or the heap runs out. The inits of closing values run one at a time: a call on another thread while one runs waits for it, then gets the value made for its key, or, if that init threw, makes its own. The wait has no limit and ignores interrupts, keeping the interrupt status; the run's end waits the same way, and a waiting call is refused once the run begins to end. So an init that waits for another thread which itself calls this method hangs both. A language's unit tests close a test context's values with io.github.brantunger.unruly.test.LanguageTestContexts.endRun. If an init ends the run that way itself and returns a value, nothing is kept under key: the value is closed, and refused with the IllegalStateException a later call gets, which carries what that close() threw as a suppressed exception, unless that is or carries a fatal Error. The error is thrown in its place, carrying the IllegalStateException. Otherwise the values behave as runScoped(java.lang.Object, java.util.function.Supplier<? extends T>) describes.

      Type Parameters:
      T - The value's type
      Parameters:
      key - The key, compared with Object.equals(Object)
      init - Makes the value the first time the run asks for key
      Returns:
      The value kept under key, never null
      Throws:
      NullPointerException - if key or init is null, or init returns null
      IllegalStateException - if the init of key is running, so it asked for its own key, the run keeps a value under key with runScoped(java.lang.Object, java.util.function.Supplier<? extends T>), or the run's values are being or have been closed, including when init itself ended the run