java.lang.Object
com.tccc.kos.commons.util.concurrent.future.FutureWork
All Implemented Interfaces:
Abortable, Cancelable, Terminable, Runnable
Direct Known Subclasses:
FailedFuture, ParallelFuture, SequencedFuture

public class FutureWork extends Object implements Runnable, Terminable
This is the kOS equivalent of Java's FutureTask.

While a Java Future is about performing an asynchronous computation and then returning a value, it's much more common in kOS that we're coordinating physical operations, such as running a pump. These tend to be long-running and require user feedback.

It's also common that these asynchronous tasks are grouped together by higher-order logic so that they run sequentially, in parallel, or even in nested graphs. This work rarely produces a result, and the desire is to determine if the work was successful or not.

Since these are common physical actions, there is typically a need for the user to be able to cancel them. Cancelling a physical operation usually involves a great deal of hardware and state cleanup vs. interrupting a thread. There is also more complexity to the state, such as the concept of aborting an operation, which is semantically distinct from cancelling. A cancellation is user-initiated, while an abort is software-initiated.

This feeds into the concept of "success", which is more complex than a standard Java Future can handle. For example, a task may reach completion, in which case it's considered completed and successful. If cancelled, it is also completed, but not successful. If aborted, it is also completed but not successful. However, when cancelled we expect the failure because the user requested it, whereas an abort is unexpected. Depending on the operation, these distinctions are significant.

Since asynchronous work is commonly triggered by a user, we normally have to provide operation progress to the user, along with the estimated time. This can become complex when graphs of FutureWork are constructed, and we need to estimate the total time of the graph. Furthermore, it's usually necessary to broadcast information about the work being performed to other consumers, such as the UI, so that long-running tasks can show more than a simple progress bar update. As these use cases combined do not fit well within standard Java futures, we introduce a kOS-specific type of future that handles these needs.

Since kOS work is not designed around the propagation of data, it's more convenient to receive callbacks when events occur on work. FutureWork supports the following four end states:

  • SUCCESS : Work ran to completion and was successful
  • FAIL : Work ran to completion but failed
  • CANCEL : User requested the work be terminated
  • ABORT : Software requested the work be terminated
Once one of these states is reached, no other state transition can occur. There are callbacks available for these states, but it is common to perform cleanup for more than one state, so there are also higher level callbacks to make this easier:
  • DONE : Triggers on either SUCCESS or FAIL
  • TERMINATE : Triggers on either CANCEL or ABORT
  • COMPLETE : Triggers at the end of the work, regardless of the end state
Callbacks for the end states are called first, followed by higher level callbacks, finishing with the complete callbacks.

Callbacks allow for a very convenient and compact way of writing code, as there is no need to create anonymous classes to override methods, and multiple callbacks can be chained from a single work, which avoids wrapper objects or object chaining. However, be aware of the order of callbacks, as it's possible to perform cleanup in the "complete" phase but the cancel callback was used to trigger code that assumes the cleanup is done. There may be times when "complete" callbacks are used and the state of the work is checked to determine what to run as means of ensuring that this code runs last.

It is also possible to use work.waitUntilComplete() to sleep on work until it is finished.

As it's very common for work to be triggered from external processes, such as UI code, there are features designed to make it easy to integrate work with external clients. If work is returned from an endpoint, then it is automatically broadcasts data about the work over a corresponding websocket. This broadcast includes core work data, such as progress and estimated time, which can be used to drive progress bars and other similar UI elements. FutureWork contains two additional fields to ease client integration:

  • tracker : A client-supplied string that is sent back over websocket events so that the client can correlate work data to the process that initiated it, since it won't know the work ID until it is returned from the endpoint.
  • clientData : Since work data is periodically broadcast, Java code can optionally attach additional data for the client in the clientData field to be included in the broadcast. Certain processes, such as sanitizing pumps, takes a long time to run but can include progress about each individual pump in this field, thus allowing the UI to update more than just a simple progress bar.
Since:
1.0
Version:
2023-01-20
  • Field Details

  • Constructor Details

    • FutureWork

      public FutureWork(String name, long estimatedTimeMs)
      Returns work that has no associated runnable, but does include a time estimate. This can be used with setRunnable() to create a future which will be assigned a FutureRunnable at a later time.
      Parameters:
      name - the name of the future (for diagnostics)
      estimatedTimeMs - the estimated time of the work in ms
    • FutureWork

      public FutureWork(String name, FutureRunnable runnable, long estimatedTimeMs)
      Returns work that will perform the specified FutureRunnable , which is expected to take the specified amount of time to complete.
      Parameters:
      name - the name of the future (for diagnostics)
      runnable - the work to perform
      estimatedTimeMs - the estimated time (in msec) it will take to complete this work
    • FutureWork

      public FutureWork(String name, FutureRunnable runnable)
      Returns work that will perform the specified runnable .
      Parameters:
      name - the name of the future (for diagnostics)
      runnable - the work to perform
  • Method Details

    • setParent

      public void setParent(FutureWork future)
      Sets the parent of this future. This is not required, but can be used to tie future data together in a diagnostic trace.
    • getName

      public String getName()
      Returns the name of this future.
    • getId

      public int getId()
      Returns the unique ID of the work. This starts at "1" and is unique for the lifetype of the JVM. It restarts numbering on the next reboot. External clients use this to correlate runtime data. Internally, this is useful for tracking work within logs.
      Returns:
      the unique ID of the work
    • setRunnable

      public FutureWork setRunnable(FutureRunnable runnable)
      Sets the runnable to be performed by the work. Unlike Java futures, it is possible for this work to complete without changing the end state. For example, this work may simply kick off a hardware process and some other mechanism is used to determine when that hardware is done, and that will change the state.

      This runnable can set the state, but callbacks won't be triggered until this function returns. This prevents race conditions between this code and any state event callbacks.

      If this runnable throws an exception, then the state is set to "aborted".

      Parameters:
      runnable - the code to run as part of the work
      Throws:
      IllegalStateException - if work has already been run and completed
    • execute

      public void execute()
      Execute this future on the internal kOS thread pool.
    • run

      public final void run()
      This is the entry point to start the runnable associated with the work. This is final , as it performs setup steps before the user-supplied work is actually started, and exists to make it easy to put work into thread pools. User work is defined by calling setRunnable() , or using the constructor which takes a FutureRunnable .
      Specified by:
      run in interface Runnable
    • cancel

      public void cancel(String reason)
      Cancels the work with the specified reason.
      Specified by:
      cancel in interface Cancelable
      Parameters:
      reason - the cancel reason
    • cancel

      public void cancel(String reason, Object data, Class<?> view)
      Cancels the work with the specified reason and optional data and view.
      Parameters:
      reason - the cancel reason
      data - optional associated data
      view - optional view to apply to the data
    • cancel

      public void cancel(String reason, ReasonData reasonData)
      Cancels the work with the specified reason and optional reason data.
      Parameters:
      reason - the cancel reason
      reasonData - optional associated data
    • abort

      public void abort(String reason)
      Aborts the work with the specified reason.
      Specified by:
      abort in interface Abortable
      Parameters:
      reason - the abort reason
    • abort

      public void abort(String reason, Object data, Class<?> view)
      Aborts the work with the specified reason and optional data and view.
      Parameters:
      reason - the abort reason
      data - optional associated data
      view - optional view to apply to the data
    • abort

      public void abort(String reason, ReasonData reasonData)
      Aborts the work with the specified reason and optional reason data.
      Parameters:
      reason - the abort reason
      reasonData - optional associated data
    • fail

      public void fail(String reason)
      Fails the work with the specified reason.
      Parameters:
      reason - the fail reason
    • fail

      public void fail(String reason, Object data, Class<?> view)
      Fails the work with the specified reason and optional data and view.
      Parameters:
      reason - the fail reason
      data - optional associated data
      view - optional view to apply to the data
    • fail

      public void fail(String reason, ReasonData reasonData)
      Fails the work with the specified reason and optional reason data.
      Parameters:
      reason - the fail reason
      reasonData - optional associated data
    • success

      public void success()
      Marks the work "successful".
    • setState

      public void setState(FutureState state, String reason, Object data, Class<?> view)
      Sets the state of the work. Same as calling success() , fail() , abort() , or cancel() . The reason is commonly used as a symbolic error code that can be acted upon or logged in order to determine what happened if not successful (reason is ignored for SUCCESS).
      Parameters:
      state - the state to set the work to
      reason - the reason for the state
      data - optional reason data
      view - optional view to apply to the data
    • setState

      public void setState(FutureState state, String reason, ReasonData reasonData)
      Sets the state of the work. Same as calling success() , fail() , abort() , or cancel() . The reason is commonly used as a symbolic error code that can be acted upon or logged in order to determine what happened if not successful (reason is ignored for SUCCESS).
      Parameters:
      state - the state to set the work to
      reason - the reason for the state
      reasonData - optional data associated with the reason
    • getEndState

      public FutureState getEndState()
      Returns the current state of the work.
    • getReason

      public String getReason()
      Returns the reason for the end state.
    • getReasonData

      public ReasonData getReasonData()
      Returns the reason data if there is any.
    • isSuccess

      public boolean isSuccess()
      Returns true if the end state is "success".
    • isFail

      public boolean isFail()
      Returns true if the end state is "fail".
    • isCancel

      public boolean isCancel()
      Returns true if the end state is "cancel".
    • isAbort

      public boolean isAbort()
      Returns true if the end state is "abort".
    • isDone

      public boolean isDone()
      Returns true if the end state is "done".
    • isTerminate

      public boolean isTerminate()
      Returns true if the end state is "terminate".
    • isInterrupted

      public boolean isInterrupted()
      Returns true if setting interruptable to true would result in an interrupt. This will be true if preState is set, meaning that the end state of this work has already been determined. This is commonly used during long run methods to find a safe exit point when the work can be ended in a clean way.
    • setInterruptable

      public void setInterruptable(boolean interruptable) throws InterruptedException
      Sets interruptable state. By default, work is not interruptable, which ensures that any asynchronous work runs to completion. If work is long-running, it can periodically check isInterrupted() to see if the end state has already been set externally. If the work is going to block, then interruptable can be set to true, which causes the run thread to receive a Thread.interrupt() to wake it when the end state is set (or immediately when marked interruptable if end state has already been set). Interruptable can be turned on and off to ensure that async code always exits in a clean way.
      Parameters:
      interruptable - true to receive interrupts
      Throws:
      InterruptedException
    • isInterruptable

      public boolean isInterruptable()
      Returns the state of the interruptable flag.
    • interruptIfPending

      public void interruptIfPending() throws InterruptedException
      Generates a Thread.interrupt() call against the work thread if there is an interruptable condition pending. This is equivalent to setting interruptable to true and false again. This is typically called by work code after an uninterruptable block to see if an interrupt was generated during that block. It is also possible to check isInterrupted() and perform an action if true, but it is common for long-running work to leverage exceptions to clean up work so this method cleanly works with that exception handling infrastructure. This can be called from the running future thread or a different thread.
      Throws:
      InterruptedException
    • append

      public FutureWork append(String name, FutureEvent event, FutureRunnable callback)
      Appends an event callback.
      Parameters:
      name - the name of this callback (for diagnostics)
      event - the type of event the callback is for
      callback - the callback
    • prepend

      public FutureWork prepend(String name, FutureEvent event, FutureRunnable callback)
      Prepends an event callback.
      Parameters:
      name - the name of this callback (for diagnostics)
      event - the type of event the callback is for
      callback - the callback
    • remove

      public FutureWork remove(String name)
      Removes any and all callbacks with the specified name.
    • setData

      public FutureWork setData(Object data)
      Sets optional data associated with the work. Provides a way to easily attach state to the work that can then be accessed in event handlers. This data is for internal use only and will not be shared with external clients as is the case with setClientData() .
    • getData

      public Object getData()
      Returns the optional data associated with the work.
    • getTracker

      public String getTracker()
      Returns the optional client tracker.
      Returns:
      the client tracker token
    • getClientData

      public JsonViewWrapper getClientData()
      Returns a client data wrapper object. Any data to be shared with the client when the work is broadcast over websocket can be placed in the data field of the wrapper. This can contain data beyond progress and estimated to allow the UI to update other aspects of the screen while the work is running. This data is not intended for use within Java to share state internally since it will be broadcast (see setData()). The wrapper has a view field which is used to constain the view of the data when being serialized. This provides fine-grained control over the data sent to the client.
    • setTimeout

      public FutureWork setTimeout(long delay)
      Adds a timeout to the work. If the timeout is reached, the work is aborted with a reason of REASON_ERR_TIMED_OUT. The timeout won't start until the work is started.
      Parameters:
      delay - the delay until the timeout occurs (in milliseconds)
    • setTimeout

      public FutureWork setTimeout(long delay, String reason)
      Adds a timeout to the work. If the timeout is reached, the work is aborted with the specified reason. The timeout won't start until the work is started.
      Parameters:
      delay - the delay until the timeout occurs (in milliseconds)
      reason - the reason code to use if the timer fires
    • setTimeout

      public FutureWork setTimeout(long delay, FutureRunnable callback)
      Adds a timeout to the work that will call the specified callback. If the work is to be terminated, it should be aborted. The timeout won't start until the work is started.
      Parameters:
      delay - the delay until the timeout occurs in ms
      callback - the function to call if the timer fires
    • cancelTimeout

      public void cancelTimeout()
      Cancels an existing timeout.
    • getAbortAbandonedTimeoutMs

      public int getAbortAbandonedTimeoutMs()
      Returns the override value for abandoned future handling. See setAbortAbandonedTimeoutMs() for details.
    • setAbortAbandonedTimeoutMs

      public void setAbortAbandonedTimeoutMs(int ms)
      A common bug when using futures is to never complete the future. This can leave futures running indefinitely with no easy way to detect this condition. When returning futures to external systems, this can cause external systems to hang.

      FutureService addresses this by automatically aborting futures that have exceeded the estimated time by a certain amount (service config option). In some cases, particularly when the estimated time is hard to compute correctly, this can result in futures being aborted unexpectedly. This setting allows the abort timeout to be adjusted for this particular future. A value of zero will use the FutureService default which is typically about a minute. A positive value will override the default value. A negative value will disable this functionality entirely and the future will live forever if not properly completed.

      Parameters:
      ms - the abort timeout to use
    • disableAbortAbandoned

      public void disableAbortAbandoned()
      Disables the abort of abandoned futures for this future. This simply calls setAbortAbandonedTimeoutMs(-1) to disable the abort feature. This is commonly used for futures that don't have an estimated time but are potentially long running to ensure that they aren't considered abandoned futures that were never completed.
    • getStartTimeMono

      public long getStartTimeMono()
      Returns the start time of the work in mono time.
      Returns:
      the start time of the operation or zero if the work hasn't started
    • getEstimatedTimeMs

      public long getEstimatedTimeMs()
      Returns the estimated time of the operation in milliseconds.
      Returns:
      the estimated time of the operation
    • setEstimatedTimeMs

      public FutureWork setEstimatedTimeMs(long estimatedTimeMs)
      Sets the total estimated time of the operation in milliseconds.
      Parameters:
      estimatedTimeMs - the new total estimated time
    • setRemainingTimeMs

      public FutureWork setRemainingTimeMs(long remainingTimeMs)
      Updates the estimated time of the operation with the amount of time remaining. This computes how long the operation has been running, adds the new remaining time, and sets the total estimated time to the result.
      Parameters:
      remainingTimeMs - amount of time remaining
    • getRemainingTimeMs

      public long getRemainingTimeMs()
      Returns the estimated remaining time for the operation in milliseconds.
      Returns:
      the estimated remaining time
    • getEstimatedEndTimeMono

      public long getEstimatedEndTimeMono()
      Returns the estimated end time in mono time.
      Returns:
      estimated end time in mono time
    • getProgress

      public int getProgress()
      Returns the progress as a value between 0 and 100 based on the time estimate or the overridden progress value using setProgress() .
      Returns:
      progress value
    • setProgress

      public void setProgress(int progress)
      Sets the progress value. This allows the progress to be driven by a process other than estimated time. Once set, the progress is no longer internally driven and the caller is expected to drive the progress to completion.
      Parameters:
      progress - the new progress value (internally clipped to 0..100)
    • getNote

      public String getNote()
      Return the note associated with the future.
    • setNote

      public void setNote(String note)
      Set the note on this future. A note is typically used to convey sub-state information of the future as it progresses without the need to use client data. For this use case, typically use setRootNote() which will set the note on the root future of the future graph which is typically the only data being exposed to external systems. Sett setRootNote() for more details about how notes can be used.
      Parameters:
      note - the new note
    • getRootFuture

      public FutureWork getRootFuture()
      Return the root future of the graph. When futures are chained into parent/child graphs using ParallelFuture and SequencedFuture , this returns the top future of the graph. This can be used to set client data on the root which is then part of the future information sent by the broker.
    • waitUntilFinished

      public boolean waitUntilFinished(long timeoutMillis)
      Sleeps until the work finishes or the timeout is reached.
      Parameters:
      timeoutMillis - how long to wait for the work to finish (0 = forever)
      Returns:
      true if the work is finished, false if timed out
    • whenFinished

      public void whenFinished(Runnable runnable)
      Run the specified code when this future is finished. If the future is already finished, it will be called in the context of the calling thread. If not finished, it will be called when the future is finished.
    • toString

      public String toString()
      Overrides:
      toString in class Object
    • isRunStarted

      public boolean isRunStarted()
    • isRunComplete

      public boolean isRunComplete()