Class FutureWork
- All Implemented Interfaces:
Abortable
,Cancelable
,Terminable
,Runnable
- Direct Known Subclasses:
FailedFuture
,ParallelFuture
,SequencedFuture
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
- 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 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 Summary
Fields -
Constructor Summary
ConstructorsConstructorDescriptionFutureWork
(String name, long estimatedTimeMs) Returns work that has no associated runnable, but does include a time estimate.FutureWork
(String name, FutureRunnable runnable) Returns work that will perform the specifiedrunnable
.FutureWork
(String name, FutureRunnable runnable, long estimatedTimeMs) Returns work that will perform the specifiedFutureRunnable
, which is expected to take the specified amount of time to complete. -
Method Summary
Modifier and TypeMethodDescriptionvoid
Aborts the work with the specified reason.void
abort
(String reason, ReasonData reasonData) Aborts the work with the specified reason and optional reason data.void
Aborts the work with the specified reason and optional data and view.append
(String name, FutureEvent event, FutureRunnable callback) Appends an event callback.void
Cancels the work with the specified reason.void
cancel
(String reason, ReasonData reasonData) Cancels the work with the specified reason and optional reason data.void
Cancels the work with the specified reason and optional data and view.void
Cancels an existing timeout.void
Disables the abort of abandoned futures for this future.void
execute()
Execute this future on the internal kOS thread pool.void
Fails the work with the specified reason.void
fail
(String reason, ReasonData reasonData) Fails the work with the specified reason and optional reason data.void
Fails the work with the specified reason and optional data and view.int
Returns the override value for abandoned future handling.Returns a client data wrapper object.getData()
Returns the optional data associated with the work.Returns the current state of the work.long
Returns the estimated end time in mono time.long
Returns the estimated time of the operation in milliseconds.int
getId()
Returns the unique ID of the work.getName()
Returns the name of this future.getNote()
Return the note associated with the future.int
Returns the progress as a value between 0 and 100 based on the time estimate or the overridden progress value usingsetProgress()
.Returns the reason for the end state.Returns the reason data if there is any.long
Returns the estimated remaining time for the operation in milliseconds.Return the root future of the graph.long
Returns the start time of the work in mono time.Returns the optional client tracker.void
Generates aThread.interrupt()
call against the work thread if there is an interruptable condition pending.boolean
isAbort()
Returns true if the end state is "abort".boolean
isCancel()
Returns true if the end state is "cancel".boolean
isDone()
Returns true if the end state is "done".boolean
isFail()
Returns true if the end state is "fail".boolean
Returns the state of theinterruptable
flag.boolean
Returns true if setting interruptable to true would result in an interrupt.boolean
boolean
boolean
Returns true if the end state is "success".boolean
Returns true if the end state is "terminate".prepend
(String name, FutureEvent event, FutureRunnable callback) Prepends an event callback.Removes any and all callbacks with the specified name.final void
run()
This is the entry point to start therunnable
associated with the work.void
setAbortAbandonedTimeoutMs
(int ms) A common bug when using futures is to never complete the future.Sets optional data associated with the work.setEstimatedTimeMs
(long estimatedTimeMs) Sets the total estimated time of the operation in milliseconds.void
setInterruptable
(boolean interruptable) Setsinterruptable
state.void
Set the note on this future.void
setParent
(FutureWork future) Sets the parent of this future.void
setProgress
(int progress) Sets the progress value.setRemainingTimeMs
(long remainingTimeMs) Updates the estimated time of the operation with the amount of time remaining.setRunnable
(FutureRunnable runnable) Sets therunnable
to be performed by the work.void
setState
(FutureState state, String reason, ReasonData reasonData) Sets the state of the work.void
setState
(FutureState state, String reason, Object data, Class<?> view) Sets the state of the work.setTimeout
(long delay) Adds a timeout to the work.setTimeout
(long delay, FutureRunnable callback) Adds a timeout to the work that will call the specified callback.setTimeout
(long delay, String reason) Adds a timeout to the work.void
success()
Marks the work "successful".toString()
boolean
waitUntilFinished
(long timeoutMillis) Sleeps until the work finishes or the timeout is reached.void
whenFinished
(Runnable runnable) Run the specified code when this future is finished.
-
Field Details
-
REASON_errTimedOut
- See Also:
-
REASON_errStartCallbackFailed
- See Also:
-
-
Constructor Details
-
FutureWork
Returns work that has no associated runnable, but does include a time estimate. This can be used withsetRunnable()
to create a future which will be assigned aFutureRunnable
at a later time.- Parameters:
name
- the name of the future (for diagnostics)estimatedTimeMs
- the estimated time of the work in ms
-
FutureWork
Returns work that will perform the specifiedFutureRunnable
, 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 performestimatedTimeMs
- the estimated time (in msec) it will take to complete this work
-
FutureWork
Returns work that will perform the specifiedrunnable
.- Parameters:
name
- the name of the future (for diagnostics)runnable
- the work to perform
-
-
Method Details
-
setParent
Sets the parent of this future. This is not required, but can be used to tie future data together in a diagnostic trace. -
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
Sets therunnable
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 therunnable
associated with the work. This isfinal
, 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 callingsetRunnable()
, or using the constructor which takes aFutureRunnable
. -
cancel
Cancels the work with the specified reason.- Specified by:
cancel
in interfaceCancelable
- Parameters:
reason
- the cancel reason
-
cancel
Cancels the work with the specified reason and optional data and view.- Parameters:
reason
- the cancel reasondata
- optional associated dataview
- optional view to apply to the data
-
cancel
Cancels the work with the specified reason and optional reason data.- Parameters:
reason
- the cancel reasonreasonData
- optional associated data
-
abort
Aborts the work with the specified reason. -
abort
Aborts the work with the specified reason and optional data and view.- Parameters:
reason
- the abort reasondata
- optional associated dataview
- optional view to apply to the data
-
abort
Aborts the work with the specified reason and optional reason data.- Parameters:
reason
- the abort reasonreasonData
- optional associated data
-
fail
Fails the work with the specified reason.- Parameters:
reason
- the fail reason
-
fail
Fails the work with the specified reason and optional data and view.- Parameters:
reason
- the fail reasondata
- optional associated dataview
- optional view to apply to the data
-
fail
Fails the work with the specified reason and optional reason data.- Parameters:
reason
- the fail reasonreasonData
- optional associated data
-
success
public void success()Marks the work "successful". -
setState
Sets the state of the work. Same as callingsuccess()
,fail()
,abort()
, orcancel()
. 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 toreason
- the reason for the statedata
- optional reason dataview
- optional view to apply to the data
-
setState
Sets the state of the work. Same as callingsuccess()
,fail()
,abort()
, orcancel()
. 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 toreason
- the reason for the statereasonData
- optional data associated with the reason
-
getEndState
Returns the current state of the work. -
getReason
Returns the reason for the end state. -
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 ifpreState
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
Setsinterruptable
state. By default, work is not interruptable, which ensures that any asynchronous work runs to completion. If work is long-running, it can periodically checkisInterrupted()
to see if the end state has already been set externally. If the work is going to block, theninterruptable
can be set to true, which causes the run thread to receive aThread.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 theinterruptable
flag. -
interruptIfPending
Generates aThread.interrupt()
call against the work thread if there is an interruptable condition pending. This is equivalent to settinginterruptable
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 checkisInterrupted()
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
Appends an event callback.- Parameters:
name
- the name of this callback (for diagnostics)event
- the type of event the callback is forcallback
- the callback
-
prepend
Prepends an event callback.- Parameters:
name
- the name of this callback (for diagnostics)event
- the type of event the callback is forcallback
- the callback
-
remove
Removes any and all callbacks with the specified name. -
setData
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 withsetClientData()
. -
getData
Returns the optional data associated with the work. -
getTracker
Returns the optional client tracker.- Returns:
- the client tracker token
-
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
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
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
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 mscallback
- 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. SeesetAbortAbandonedTimeoutMs()
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 theFutureService
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 callssetAbortAbandonedTimeoutMs(-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
Sets the total estimated time of the operation in milliseconds.- Parameters:
estimatedTimeMs
- the new total estimated time
-
setRemainingTimeMs
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 usingsetProgress()
.- 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
Return the note associated with the future. -
setNote
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 usesetRootNote()
which will set the note on the root future of the future graph which is typically the only data being exposed to external systems. SettsetRootNote()
for more details about how notes can be used.- Parameters:
note
- the new note
-
getRootFuture
Return the root future of the graph. When futures are chained into parent/child graphs usingParallelFuture
andSequencedFuture
, 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
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
-
isRunStarted
public boolean isRunStarted() -
isRunComplete
public boolean isRunComplete()
-