Interface TaskExecutorExtension

All Superinterfaces:
HostExtensionPoint

public interface TaskExecutorExtension extends HostExtensionPoint
Implemented by a plugin bundle to provide the behaviour of a custom task type.

An implementation is an ordinary Spring bean in the bundle's plugin context (see the X-Plugin-Context manifest header). Nothing here is OSGi-specific: no BundleActivator, no BundleContext, no Declarative Services annotations. The host discovers implementations through PluginExtensionRegistry and never registers them into the root ApplicationContext.

A complete task type is two things:

  1. a declaration in the bundle's type-definitions.xml, of a type extending xlrelease.PluginTaskConfiguration, which defines the properties a release author fills in; and
  2. one bean implementing this interface, whose getTaskType() returns that declared type's name.

Execution model

execute runs on a host worker thread, not on the caller's thread, and it may block. It is invoked from the same bounded pool that runs every other task job in the product, so a blocking call holds one of those slots for its duration - exactly as a Jython script does today. That is why a plain synchronous signature is acceptable on top of an asynchronous host.

Block only for short, bounded waits. A single request/response is fine; seconds, not minutes. Anything open-ended - waiting for a deployment, an approval, a remote job - must return TaskExecutionResult.suspendFor(java.time.Duration) or TaskExecutionResult.awaitSignal() instead of sleeping. This is not a style preference: a parked thread is heap state. It does not survive a restart, redeploy, crash or failover, whereas a suspended task is persisted and does. Blocking for a long wait would silently lose in-flight work on restart, and would pass testing, because short waits work fine.

Honour Thread.interrupt() and return promptly. Interruption is how the host aborts a running task. An implementation that swallows interrupts cannot be aborted.

execute may be entered more than once for the same logical step. Job delivery is at-least-once, so implementations must tolerate re-entry - make external calls idempotent, or record progress in output properties and check it on entry (see TaskExecutionContext.isResume()).

Threads you start are yours. You may create threads, including virtual threads, inside execute; the host neither provides nor bounds them, and does not stop them at teardown. Anything you start must be joined or completed before execute returns. A thread that outlives the call is untracked by the host and keeps your bundle's classloader alive, which prevents your plugin from being cleanly uninstalled. Note also that on JDK 21, blocking inside a synchronized block on a virtual thread pins its carrier thread and costs throughput; prefer java.util.concurrent.locks around I/O.

A thread you start does not inherit the release's authenticated identity. execute itself runs already authenticated as the release's script user (the same identity, resolved the same way, that a Jython task's script runs as - see the release's "Run automated tasks as user" property), so a bridged host API call made directly inside execute is authorized correctly with no extra work. That identity lives in a plain, non- inheritable ThreadLocal, though, so a thread you spawn does not carry it: a bridged API call made from that thread runs with whatever ambient identity the new thread happens to have - typically none - and fails authentication rather than silently running as the wrong user. Make bridged API calls directly on the execute thread, or hand results back to it and make the call there.

Failure

Return TaskExecutionResult.failed(String) for an expected failure with a message a release author can act on. A thrown exception is also treated as a failure, but the author sees a stack trace rather than an explanation, so prefer the explicit result.

See Also:
  • Method Summary

    Modifier and Type
    Method
    Description
    Runs the task.
    The name of the configuration type this extension implements, in prefix.Name form - for example "acme.HelloTask" - exactly as declared in the bundle's type-definitions.xml and as rendered by com.xebialabs.deployit.plugin.api.reflect.Type#toString().
    default void
    Called after a task of this type is aborted while a previous execute had left it waiting (TaskExecutionResult.suspendFor(java.time.Duration) / TaskExecutionResult.awaitSignal()), so the extension can release external resources — cancel a delegated remote job, close a reservation — that would otherwise be orphaned, since execute is never re-entered after an abort.
  • Method Details

    • getTaskType

      String getTaskType()
      The name of the configuration type this extension implements, in prefix.Name form - for example "acme.HelloTask" - exactly as declared in the bundle's type-definitions.xml and as rendered by com.xebialabs.deployit.plugin.api.reflect.Type#toString().

      This is the host's dispatch key: it is matched against the configuration type of the task being executed. It must be unique across all installed plugins; a duplicate is a plugin installation error, not a silent override.

      Note that this is the configuration type the author declared, not task.getType() - which is xlrelease.PluginTask for every plugin task, since one compiled task class backs them all.

      Returns:
      the declared configuration type name; never null or blank
    • execute

      Runs the task.

      Called on a host worker thread. See the class Javadoc for the blocking, interruption, idempotency and thread-ownership rules that apply here - they are contractual, not advisory.

      Parameters:
      context - access to the task's properties, identity and execution log; never null
      Returns:
      what the host should do next; never null
      Throws:
      Exception - treated as a task failure; prefer TaskExecutionResult.failed(String) for expected failures
    • onAbort

      default void onAbort(TaskAbortContext context) throws Exception
      Called after a task of this type is aborted while a previous execute had left it waiting (TaskExecutionResult.suspendFor(java.time.Duration) / TaskExecutionResult.awaitSignal()), so the extension can release external resources — cancel a delegated remote job, close a reservation — that would otherwise be orphaned, since execute is never re-entered after an abort.

      Best-effort, after the fact. The task is already failed when this runs; nothing returned or thrown here changes that. Runs asynchronously on a host executor with the same thread context as execute (bundle TCCL, release script user), so bridged host API calls work. The same rules apply: keep it to short bounded calls, honour interruption, join any threads you start. A thrown exception is logged and swallowed.

      Idempotency is contractual here too. Delivery is at-least-once (the host dedupes best-effort, not transactionally), and an abort may race a still-running execute — in which case the output properties read through the context may predate that execution's unpersisted writes. Release external resources idempotently, and treat an absent correlation id as "nothing to release", never as an error.

      The default does nothing — implement only if the task holds external state worth releasing.

      Parameters:
      context - read-only view of the aborted task's properties and identity
      Throws:
      Exception