Two Java engineers debugging a cancelled CompletableFuture whose supplier continues running on an executor.

CompletableFuture Was Cancelled. Why Is the Supplier Still Running?

Tue, Oct 20, 2026

cancel(true) returns true. The future reports cancellation. A caller can reasonably conclude that its normal result will no longer arrive, yet the supplier it launched may still be waiting, consuming an execution slot, or approaching an effect. Treating that green return value as confirmation that work stopped creates a gap between the application contract and the evidence used to release it.

This playbook examines that gap in one Java process. An explicitly owned single-thread executor runs a job whose entry is acknowledged before cancellation. A closed latch prevents normal progress to a local counter increment. The experiment then observes the result handle, interruption response, final application boundary and effect count separately. Opening the gate makes continued execution visible without relying on a scheduling delay or contacting another system.

The comparison preserves the job while changing its execution carrier: CompletableFuture, an explicitly constructed FutureTask, and a separate case with a deliberately positioned stop check. A late-cancellation control preserves a result that already exists. Together, these cases distinguish a valid result-abandonment policy from an unimplemented worker-termination policy.

The release question is concrete: which component owns the requested stop, what witnesses its completion, and who accounts for effects? Accept only the cancellation contract those observations support. A diagnostic pass can expose an unacceptable candidate just as successfully as it validates a suitable one.

Declare whether cancellation means result abandonment or worker exit

Write the product requirement before choosing an API. One legitimate contract is result abandonment: once cancellation succeeds, this handle must stop offering the ordinary result. Computation may continue under an identified owner, with an explicit resource budget and an approved effect policy. This can be sufficient when the caller merely loses interest in an independently managed calculation.

A stronger contract requires the running job to stop and release specified resources. That requirement needs a control path to the execution and an independent acknowledgement at the relevant boundary. A caller returning early cannot stand in for either. Define which resources must be released and what the owner does when the acknowledgement is missing.

The Java SE 21 CompletableFuture cancellation policy treats cancellation as exceptional completion because the class does not directly control the computation producing its result. That documented distinction makes successful result cancellation compatible with continued supplier execution.

Effect reconciliation is a separate obligation under both contracts. If work already changed domain state, stopping subsequent computation does not establish what happened to that state. Assign that question to the component capable of inspecting the effect.

For review, write four independent propositions: normal result unavailable; interrupt request delivered to the intended carrier; job exited its relevant boundary; effects accounted for. Mark each with its own evidence. A successful cancel return fills only the result-related part of this record.

Separate the result handle from the execution carrier

Start with an ownership map. In the CompletableFuture branch, job::call supplies a value through an asynchronous task submitted to the explicit executor. The application retains a completion handle and a separate Job containing the observable state. In the FutureTask branch, the application constructs the runnable carrier itself, retains that same object as its future, and gives it to pool.execute(task).

The explicit-executor supplyAsync contract identifies where the supplier executes. Supplying that executor does not give the returned future a new thread-interruption contract.

Object

Responsibility in this fixture

Evidence it supplies

CompletableFuture

Holds the supplier's completion outcome

Cancellation state and result retrieval

FutureTask

Wraps and executes the callable as the retained carrier

Carrier cancellation request and result state

ExecutorService

Schedules work and owns the worker lifecycle

Termination after owned shutdown

Job

Implements the gate, stop check and effect

Entry, interruption response and final boundary

AtomicInteger effects

Records the synthetic local increment

An independently readable count

Retain the carrier when submitting it. Constructing another FutureTask later around the same callable would create a different task object, with no control over the original invocation. Here, pool.execute(task) receives the object retained in future. That object identity makes the link between the cancellation request and the submitted execution inspectable.

The TSV column named owner abbreviates the selected future/carrier flavor. The executor still schedules the work in both branches. Reading that label as proof that CompletableFuture owns its executing thread would recreate the ambiguity the experiment is intended to remove.

Related work on mandatory Java callback validation asks whether required code executes. Here, entry is already acknowledged. The remaining obligation is to identify who can influence execution after that point and who retains responsibility when the result handle changes state.

What mayInterruptIfRunning does not do here

For the standard CompletableFuture implementation, cancel(boolean) gives mayInterruptIfRunning no operational effect: interruption is not its processing-control mechanism. Passing true therefore supplies no stronger stopping request than false in C2 and C3.

A parameter name shared across future types cannot establish a common execution-ownership model. Review the concrete object receiving the call. In particular, this fixture creates no adapter connecting cancellation of its CompletableFuture to a retained runnable carrier. If an application needs that connection, it must design and qualify it explicitly, including how completion races and cleanup acknowledgements are represented.

Freeze a local, explicitly owned execution environment

The reference semantics are Java SE 21. The supplied October 20, 2026 preflight identifies OpenJDK and javac 21.0.12.1, with runtime 21.0.12.1+1-1-deb13u1-Debian. That is the recorded build, not a claim about the latest Java release. The versioned API pages specify the selected reference branch; they do not certify an arbitrary vendor build or deployment.

Keep three evidence categories separate. The API documentation states contracts. The expected case outcomes are authored from the fixture's intended ordering and effect policy. The supplied preflight reports that seven cases ran locally and matched their assertions. It is supplied execution evidence, not a newly generated transcript accompanying this article. A deployment run must retain its own runtime identity and output.

Save the complete driver below as CancelProbe.java in a directory you own. It requires a JDK compiler and standard-library classes only. It accepts no operational inputs, starts no external request and uses fresh in-memory state for every case. Its only effect under review is an integer increment. Compilation produces class files, and the command creates the evidence file in that directory.

javac CancelProbe.java && java CancelProbe > cancellation.tsv

The supplied source SHA-256 is:

fec7a5916c64a828088728a912f66127a30975268d29dc077189eaaaebcead8e

Retain the exact source bytes, including line endings and the final newline, when comparing that identity. Record java -version and javac -version separately; the driver prints java.runtime.version, which identifies its execution runtime but says nothing by itself about the compiler used.

Every case creates an executor that belongs exclusively to that case. There are no unrelated tasks whose interrupts or cleanup could contaminate the observations. No sleeps decide whether the supplier has started. The five-second limits apply to individual controller waits; they are harness safety bounds, not a promised cancellation duration or an overall runtime limit.

The neighboring discussion of Python sibling-task lifecycle addresses another runtime and a multi-task policy. Keep this laboratory focused on one Java job and its retained control objects. Substituting a different orchestration model would change the evidence required.

Start each comparison from fresh state. Reusing a cancelled future, an already-open latch or a counter from a previous case would test a different initial condition. The run method constructs both the Job and executor anew, so each observed zero or one belongs to that case alone.

Before running, inspect the working directory and preserve any existing evidence filename you need. After review, remove only this known laboratory's files. There is no reason to introduce customer identifiers, production configuration or a remotely visible operation.

Establish the started-and-blocked baseline

C1 establishes that the job can actually perform its intended local operation when allowed through the gate. Without this positive control, a zero counter in a cancellation case could reflect a broken submission path or an effect that never executes under any condition.

The complete driver follows. carrier selects the retained future type. A null interrupt means no cancellation call; otherwise its Boolean value becomes the cancellation argument. cooperativeStop activates the separate token check, and cancelLate moves the decision after successful result retrieval. These are fixed case definitions, not inputs obtained from the candidate's output.

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public final class CancelProbe {
    private static final long LIMIT_SECONDS = 5;
    private static void require(boolean ok, String message) {
        if (!ok) throw new AssertionError(message);
    }
    private static void await(CountDownLatch latch) throws InterruptedException {
        require(latch.await(LIMIT_SECONDS, TimeUnit.SECONDS), "latch deadline exceeded");
    }
    private static final class Job implements Callable<Integer> {
        final CountDownLatch started = new CountDownLatch(1);
        final CountDownLatch release = new CountDownLatch(1);
        final CountDownLatch finished = new CountDownLatch(1);
        final AtomicBoolean interrupted = new AtomicBoolean(false);
        final AtomicBoolean stop = new AtomicBoolean(false);
        final AtomicInteger effects = new AtomicInteger(0);
        @Override public Integer call() {
            started.countDown();
            try {
                release.await();
                if (stop.get()) return -2;
                effects.incrementAndGet(); // Local synthetic effect, not a network request.
                return 7;
            } catch (InterruptedException expected) {
                interrupted.set(true);
                Thread.currentThread().interrupt();
                return -1;
            } finally {
                finished.countDown();
            }
        }
    }
    private static void run(String id, boolean carrier, Boolean interrupt,
                            boolean cooperativeStop, boolean cancelLate) throws Exception {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        Job job = new Job();
        try {
            Future<Integer> future;
            if (carrier) {
                FutureTask<Integer> task = new FutureTask<>(job);
                future = task;
                pool.execute(task);
            } else {
                future = CompletableFuture.supplyAsync(job::call, pool);
            }
            await(job.started);
            require(job.release.getCount() == 1, id + ": release unexpectedly open");
            require(!future.isDone(), id + ": result completed before controlled release");
            if (cancelLate) {
                job.release.countDown();
                await(job.finished);
                require(future.get(LIMIT_SECONDS, TimeUnit.SECONDS) == 7, id + ": value");
            }
            if (cooperativeStop) job.stop.set(true);
            boolean cancelledNow = interrupt != null && future.cancel(interrupt);
            boolean doneAfterCancel = future.isDone();
            boolean finishedBeforeRelease = job.finished.getCount() == 0;
            boolean expectedCancelled = interrupt != null && !cancelLate;
            require(cancelledNow == expectedCancelled, id + ": cancel return");
            require(future.isCancelled() == expectedCancelled, id + ": cancelled flag");
            if (expectedCancelled) require(doneAfterCancel, id + ": logical completion");
            boolean expectInterrupt = carrier && Boolean.TRUE.equals(interrupt) && !cancelLate;
            if (expectInterrupt) {
                await(job.finished); // Prove worker exit independently of isDone().
            } else if (!cancelLate) {
                require(!finishedBeforeRelease, id + ": worker should still await release");
            }
            job.release.countDown();
            await(job.finished);
            String result;
            try {
                int value = future.get(LIMIT_SECONDS, TimeUnit.SECONDS);
                require(!expectedCancelled && value == 7, id + ": unexpected value");
                result = Integer.toString(value);
            } catch (CancellationException expected) {
                require(expectedCancelled, id + ": unexpected cancellation");
                result = "CancellationException";
            }
            int expectedEffects = expectInterrupt || cooperativeStop ? 0 : 1;
            require(job.effects.get() == expectedEffects, id + ": effect count");
            require(job.interrupted.get() == expectInterrupt, id + ": interrupt observation");
            System.out.printf("%s\t%s\t%s\t%s\t%s\t%d\t%s%n", id,
                    carrier ? "FutureTask" : "CompletableFuture", cancelledNow,
                    doneAfterCancel, job.interrupted.get(), job.effects.get(), result);
        } finally {
            job.release.countDown();
            pool.shutdown();
            if (!pool.awaitTermination(LIMIT_SECONDS, TimeUnit.SECONDS)) {
                pool.shutdownNow();
                require(pool.awaitTermination(LIMIT_SECONDS, TimeUnit.SECONDS), "executor leaked");
            }
        }
    }
    public static void main(String[] args) throws Exception {
        System.out.println("java=" + System.getProperty("java.runtime.version"));
        System.out.println("case\towner\tcancel_return\tdone_after_cancel\tinterrupted\teffects\tresult");
        run("C1", false, null, false, false);
        run("C2", false, false, false, false);
        run("C3", false, true, false, false);
        run("C4", true, true, false, false);
        run("C5", true, false, false, false);
        run("C6", false, true, true, false);
        run("C7", false, true, false, true);
        System.out.println("PASS_7_CASES");
    }
}

Trace C1 through the three latches. started begins at one and is decremented immediately on entry to Job.call(). The controller waits for that acknowledgement, verifies that release remains closed, and checks that the result has not completed. The finished latch also starts at one and is decremented in the job's finally block.

Because C1 passes null for the cancellation argument, short-circuit evaluation leaves cancelledNow false without invoking cancel. The controller confirms that the job has not reached its final boundary, opens release, and waits for finished. The job observes a false stop flag, increments the counter once, and returns the independently chosen success value 7.

The initial result check precedes any case's controlled gate opening. It rejects a setup that permits completion too early. If this assertion fails, retain the failure and inspect the ordering; continuing with a later cancellation would examine a different scenario. The baseline therefore checks the harness's ability to hold progress as well as its ability to release work.

Result retrieval and the counter check cover different mistakes. A value of 7 alone would not detect an extra increment. A count of one alone would not prove successful result delivery. The normal case requires both, followed by cleanup of the owned executor. Its supplied preflight outcome was one effect and result 7.

Use latches as ordering evidence, not timing guesses

The CountDownLatch memory-consistency and waiting contract orders actions preceding countDown() before actions following a corresponding successful await(). Its wait can also respond to interruption. The fixture uses these boundaries to establish progress without selecting a sleep interval that merely seems long enough.

Entry acknowledgement does not prove a sampled thread state of “parked.” The worker may be between started.countDown() and release.await(). The relevant fact for this experiment is established: the job has entered and cannot progress normally past the closed gate. Thus cancellation cannot be mistaken for preventing an unstarted supplier from entering.

The timed controller helper throws an AssertionError if an expected acknowledgement is missing. The worker's own gate wait is deliberately untimed. Increasing a controller deadline may help investigate a constrained test environment, but changing it creates a separately recorded run; it cannot repair an ownership contract.

Cancel a CompletableFuture without stopping its supplier

C2 repeats the baseline setup and changes one decision: the controller calls future.cancel(false) after entry acknowledgement while the release gate remains closed. It records the return value and isDone(), then independently requires isCancelled() to match the predetermined cancellation expectation. The supplied preflight reports successful cancellation.

At this checkpoint, the job's final latch has not reached zero. That observation has a specific cause available for inspection in the source: normal execution is still gated, and this cancellation path has not made the job take its interruption handler. The handle has changed state while the job remains responsible for unfinished execution.

The ordinary result is unavailable under the cancellation contract at this point. Be precise about the measurement: the exact driver does not call get() before opening the gate. It records cancelled completion before release, then confirms CancellationException through get() after the job reaches its final boundary. A review must not invent an earlier exception observation.

Next the controller opens the gate. Nothing in C2 has set stop, so the job increments effects and computes 7. The later retrieval still throws CancellationException. The counter reading of one establishes that the already-started supplier proceeded to its synthetic effect despite loss of normal result delivery.

C3 repeats those steps with cancel(true). Its supplied outcome remains no observed interruption, one effect and CancellationException. Comparing C2 with C3 isolates the Boolean argument while preserving entry, gate placement, job logic and executor ownership. The result supports the documented absence of an interrupt action on this CompletableFuture path.

This counterexample does not depend on a supplier that happened to stay in a queue. The driver waits for entry before making the request. It also keeps the gate closed until the cancellation checkpoint is inspected. A scheduler delay cannot explain away the distinction between the cancelled handle and unfinished job.

If the product requires only abandoning this result, either case can satisfy that narrow contract once continued execution has an approved owner. If the product requires terminating the running job, both expose an ownership defect. Changing the argument from false to true cannot repair the missing control path; the next qualification must exercise an actual stopping mechanism and an independent completion witness.

Inspect completion and termination through different witnesses

Read the observations as separate facts rather than synonyms for “done.” doneAfterCancel describes the future when the controller samples it. finishedBeforeRelease describes whether the job has reached its final latch at a particular checkpoint. The later bounded wait on finished supplies an application-boundary acknowledgement. Finally, executor termination concerns the lifetime of the owned execution facility.

C2 and C3 demonstrate why these distinctions matter: the handle is completed by cancellation while the final job boundary remains unreached. C4 adds another important case. Its immediate finishedBeforeRelease snapshot may be true or false depending on scheduling after the interrupt request. The harness does not require either transient value; it waits for the independent latch before opening the gate.

Be equally exact about that latch's strength. finished.countDown() executes inside Job.call()'s finally, before the method has completely returned and before its execution wrapper has necessarily unwound. The acknowledgement establishes that the preceding application actions have finished. There is no further application effect after that marker in this job, but the marker is not proof that an executor thread has terminated.

The source comment calling this “worker exit” should therefore be read as the final application boundary. The owned executor's later termination check supplies the stronger facility-level evidence. For a real task, place its acknowledgement after the cleanup it is intended to certify; a marker before resource release would leave a different gap.

The interrupted column has another precise meaning: the job caught InterruptedException from its cooperative wait. It is not a sample of the thread's current interrupt bit or a timestamp for the interrupt request. The handler records the observation, restores the interrupt status and returns through finally.

Keep the side-effect ledger outside the cancelled result

The controller retains job.effects independently of the future. It can therefore inspect the counter even when result retrieval throws. Keeping the only effect record inside the cancelled result would remove the evidence needed to distinguish abandoned output from completed work.

Read the final count after the final-boundary wait. In this controlled job, zero means the one increment was bypassed; one means it happened. Neither number reports the state of another system. The ledger has no hidden remote action, compensating operation or durable commit behind it.

An application needs the equivalent independent observation for its own effect boundary. If the only available fact is that its caller stopped waiting, leave effect reconciliation unresolved.

Compare an interruptible FutureTask on the same job

C4 preserves Job.call() and replaces the carrier. The controller constructs new FutureTask<>(job), retains it as future, and passes that same task to pool.execute(task). This makes the runnable submitted for execution and the object receiving cancellation visibly connected. No behavior is added to the callable to make this case easier.

The Java SE 21 FutureTask cancellation contract describes interruption of the known executing thread, when requested, as an attempt to stop an already-started task. This is the relevant contrast with the completion-only cancellation mechanism qualified in C2 and C3. An interrupt attempt still requires cooperation from the executing code.

After entry acknowledgement, C4 calls cancel(true) while normal progress remains gated. The cooperative wait throws InterruptedException, the handler sets job.interrupted, restores the thread's interrupt status and returns -1. The increment lies beyond the wait, so that path does not reach it. The controller waits for the final latch before it opens the release gate.

The CountDownLatch await contract covers an interrupt already present on entry as well as one received during waiting. Thus the small interval between the entry marker and the actual await does not invalidate C4. The experiment needs entry before cancellation and a cooperative boundary before the effect; it does not need to prove the worker had already parked.

The supplied C4 result is an observed interruption, zero effects and CancellationException. The callable's internal -1 is not the future's delivered result because the carrier was cancelled. Treat the handler observation and the final latch as the evidence of the job's response.

C5 supplies the negative control for interruption ownership. It uses the same explicit FutureTask but requests cancel(false). The job remains unfinished before release, then performs one increment when the gate opens. Its result handle nevertheless remains cancelled. Owning the appropriate carrier enables a request; choosing not to interrupt leaves this started job free to continue.

A useful review question is whether removing the interrupt request changes the observed path while preserving the workload. C4 and C5 answer that directly. C1 separately checks that the normal effect is reachable, so the zero-effect case has both a mechanism comparison and a functioning baseline supporting its interpretation.

This pair prevents a misleading conclusion that wrapping a callable in FutureTask automatically establishes termination. Acceptance for the controlled interruption path depends on the request, this job's responsive wait, the final-boundary acknowledgement and owned cleanup together. A replacement callable that catches interruption and continues toward its effect has a different contract and requires new evidence.

Place an explicit cooperative stop check at the intended boundary

C6 keeps the CompletableFuture path and introduces a separate application decision. While the gate remains closed, the controller sets job.stop to true. It then cancels the result with cancel(true), records the cancelled state and verifies that the final job boundary remains unreached. The token has not itself released the wait.

When the controller opens the gate, the job executes the deliberately placed stop.get() check. Because the stop request was established before this controlled release, the job takes the early return of -2 and bypasses the increment. Its final latch is still decremented. The supplied preflight reports no interruption, zero effects and CancellationException.

Attribute each part of that outcome to the right action. Future cancellation abandons the result. Gate release permits progress to the check. The stop flag changes the branch taken there. The final latch acknowledges that the job has passed its last application boundary. Calling cancellation alone would reproduce C3, including its eventual increment.

The comparison between C3 and C6 is therefore a narrow repair demonstration. It validates a stop check at a chosen point under ordered conditions. It does not show that the token can wake every blocking operation, that every piece of computation consults it, or that cancellation at an arbitrary instant excludes a later effect.

For application design, name both the request location and the observation location. A flag with no reachable check cannot implement prompt stopping. A check reached only after the sensitive operation cannot protect that operation. If normal progress can wait indefinitely, the owner must also establish how that wait ends so the check can be reached.

A stop flag is not a transactional rollback

Reading the token and performing the increment are separate operations. Consider a changed ordering: the worker reads false, the controller sets true, and the worker then performs its effect. Nothing in this source makes those two worker operations indivisible with respect to the stop request. Adding arbitrary work between them only gives the race more opportunity.

C6 avoids that race by fixing the request before opening the gate. Its zero is consequently evidence about that ordered scenario. It says nothing about undoing an earlier increment; the fixture contains no decrement or reconciliation mechanism.

When a real cancellation can race with an external commitment, the effect owner must establish what was accepted and what remains unresolved. A locally observed token cannot answer that question. Keep the stronger claim on HOLD_EXTERNAL_EFFECTS until the application has evidence for its actual boundary.

Handle cancellation after a result already exists

C7 tests a different decision point. It still acknowledges job entry and checks the closed gate first. Then cancelLate opens the gate, waits for the final job marker and retrieves 7 successfully before attempting cancellation. This ordering establishes completed result delivery without relying solely on a latch situated inside the callable.

That extra retrieval is essential. The final marker precedes the execution wrapper's completion, so cancelling immediately after the marker alone could still race with publication of the normal result. C7 removes that ambiguity by requiring the value before the cancellation call. The experiment therefore tests an already completed handle.

The subsequent cancellation attempt is expected to return false, leave isCancelled() false and retain isDone() as true. The fixture checks the cancellation return and cancelled flag, then retrieves 7 again. The supplied preflight reports cancellation returning false, one local effect and the retained result 7, consistent with the CompletableFuture completed-result contract.

Do not reinterpret that effect as prevented merely because a cancellation request was eventually issued. C7's purpose is to preserve the difference between request time and completion time. A policy that accepts cancellation only before a defined boundary must report what happened when the request arrived after that boundary.

For an application, decide how late abandonment is represented to the caller while keeping the underlying operation record accurate. A client may no longer need a delivered value, but that preference does not rewrite the computation's history. The owner should retain the completed outcome and any associated effect evidence through the normal reconciliation path.

This is also where API offloading and performance foundations meet resource ownership. Moving work away from an endpoint changes where it runs. The application must still define who owns completion and what a later caller cancellation means for work already accepted or completed.

Keep C7 in regression evidence after repairing an earlier cancellation path. Otherwise, a change that aggressively labels every late request “cancelled” could erase a valid completed result in application bookkeeping. The independent expected outcome for this control remains one effect and the value 7, with cancellation rejected.

C7 is deliberately ordered and does not exhaust every completion race. For a real task with competing completion actors, add separately identified tests around its actual boundary. Preserve the original case so that evidence for the completed-result behavior stays comparable.

Close the owned executor without concealing a leaked task

Teardown must both release laboratory resources and preserve the behavior being qualified. Each case records its result, interruption response and final effect count before leaving the try block. The finally block then opens the release gate, requests shutdown and checks executor termination. This ordering prevents teardown from being mistaken for the original cancellation mechanism.

The Java SE 21 ExecutorService lifecycle contract distinguishes initiating orderly shutdown from waiting for termination. shutdown() allows submitted tasks to finish and does not itself wait. shutdownNow() makes a best-effort stopping attempt and also needs a separate termination check; tasks that do not respond to interruption may continue.

The unconditional gate release matters when a diagnostic assertion fails early. It gives this cooperative laboratory job a route out of its wait before the controller starts shutting down its pool. That release can also let the synthetic increment occur. Consequently, an effect observed after emergency teardown cannot be attributed to the earlier experimental checkpoint without preserving the ordering.

The exact cleanup first waits up to five seconds after shutdown(). If that wait times out, it invokes shutdownNow() and waits again. Only failure of the second termination wait raises executor leaked. Successful emergency cleanup does not turn the first wait into a success.

There is an evidence limitation worth preserving: the driver does not print which cleanup path ran. A TSV row is emitted before that case's teardown, while PASS_7_CASES is printed only after all seven cases return. A complete pass therefore includes successful return through cleanup, but does not establish that every first termination wait succeeded or that rescue was never used.

If release qualification requires that distinction, add cleanup-path observations in a separately versioned variant and retain its new source hash. Do not relabel the supplied output as containing fields it never recorded.

Do not shut down a shared executor to cancel one request

This laboratory may shut down its pool because the case creates and owns the entire facility. That authority does not transfer to a request using an application's shared executor. Pool-wide shutdown would affect unrelated work and future submissions.

Give the request owner control over its task and assign facility shutdown to the executor's lifecycle owner. If a request cannot reach its execution carrier, record an ownership repair. Interrupting unrelated execution is not evidence that the intended task was controlled.

Use bounded cleanup and explicit failure reporting. Never call Thread.stop; arbitrary thread killing is outside this cancellation policy.

Package evidence that a reviewer can independently inspect

Bundle the source, its SHA-256, compiler identity, runtime identity, command, standard output, standard error and process exit status. Retain all seven case IDs. A reviewer should be able to connect each reported value to an assertion and each assertion to a declared requirement without having to trust a screenshot of a green test runner.

The following table combines two explicitly different forms of evidence. The cancellation returns, interruption observations, effect counts and results are reported by the supplied October 20 preflight; C1's stored false means cancellation was not called. The checkpoint isDone() values are independently derived expectations from the source. They are not presented as a newly captured TSV transcript.

Case

Request

Cancel return

Expected isDone()

Interrupt seen

Local effects

Future result

C1

CompletableFuture, no cancellation

Not called; stored false

false

No

1

7

C2

CompletableFuture, false

true

true

No

1

CancellationException

C3

CompletableFuture, true

true

true

No

1

CancellationException

C4

FutureTask, true

true

true

Yes

0

CancellationException

C5

FutureTask, false

true

true

No

1

CancellationException

C6

CompletableFuture, true, ordered stop flag

true

true

No

0

CancellationException

C7

CompletableFuture, true, after completion

false

true

No

1

7

The start and final-boundary assertions are in the source; they are not separate TSV columns. C2, C3, C5 and C6 check the unreached final boundary before release. C4 waits for that boundary before releasing the gate. C7 establishes ordinary result retrieval before its cancellation attempt.

The oracle must remain independent of the artifact under review. Here, expected effects come from fixed case settings: the interrupting carrier case and ordered stop case require zero; the remaining cases require one. The expected count is never learned by reading the counter and then accepting that same value. The success value 7 is likewise predetermined.

Require the complete PASS_7_CASES marker together with successful process completion and retained diagnostics. A partial TSV ending after a printed row is insufficient because cleanup occurs afterward. A missing marker or failed bound is evidence to investigate, not a reason to paste in the expected remainder.

Broader API testing and debugging practices provide context for retaining reproducible evidence. This particular package qualifies in-process behavior. Endpoint behavior, resource adapters and external effects need additional tests tied to their own contracts.

When suggesting those tests, label them as proposed qualification work. Do not merge their intended results into the supplied observations. Keep the original diagnostic harness intact so a reviewer can distinguish a reproduced counterexample from an application repair.

Translate the fixture into a bounded application cancellation policy

Turn the experiment into a short ownership specification attached to the operation. Identify the intake owner that receives cancellation, the computation owner that retains the execution handle, the effect owner that can reconcile outcomes, and the cleanup owner that observes resource release. One team may hold several roles, but each obligation still needs a named endpoint in the design.

The intake owner records which contract the caller requested. The computation owner applies the supported mechanism to the intended task and tracks its acknowledgement independently of result completion. The effect owner records what happened at the domain boundary. The cleanup owner keeps responsibility for execution that outlives the caller.

For result abandonment, the policy may accept continued computation only when that continuation is explicitly permitted and remains owned. For required worker termination, specify the relevant application exit boundary and the action taken when evidence fails to arrive within the application's bound. Choose that bound from the application's requirement; the laboratory's five seconds is not a service-level commitment.

Use C4 as evidence for a cooperative interrupt response and C6 as evidence for an ordered stop check. Preserve C3 as the counterexample that prevents future-state-only approval. The repair is complete only when the actual application task satisfies the stronger requirement under its own assumptions.

List the assumptions that the real task must satisfy

Check where the running code responds to interruption or reads its stop token. Establish whether every path to the sensitive effect passes the intended boundary, including early returns and exception handling. Identify any work delegated beyond the retained carrier; a local acknowledgement cannot cover execution it does not observe.

Inspect what the final acknowledgement actually follows. Resource closure that happens later, or on another independently scheduled path, requires its own witness. Establish how the owner identifies unfinished work after caller abandonment and who can resolve it.

Non-interruptible I/O, interrupt-swallowing code, shared executors, subclassed futures and cancellation racing a real commit need separate proofs. These changes affect the control or evidence model; a passing latch experiment cannot certify them.

Require a fresh run when the concrete future type, executor arrangement, task body, blocking operation, stop-check placement, acknowledgement placement or tested runtime changes materially. Keep the documented Java SE branch and tested build explicit in that record.

Finally, test refusal as well as cooperation. An application qualification should demonstrate that a missing acknowledgement produces repair or rerun status instead of silently converting a cancelled handle into a worker-exit claim. Those proposed tests extend the policy; they are not observations from this seven-case laboratory.

Choose accept, repair, rerun or hold from the evidence

Choose the decision against the named product contract. The same C3 observations can support result abandonment and reject a requirement to stop execution. That is consistent reasoning: the evidence stays fixed while the obligation changes. Do not change the requirement after seeing a convenient green state.

Decision

Declared contract and evidence

Operational owner and next action

ACCEPT_LOGICAL_CANCELLATION

Only normal-result abandonment is required; cancellation state is established, and permitted continued work remains owned.

Request/API owner records the narrow acceptance; computation owner retains cleanup responsibility.

REPAIR_TASK_OWNERSHIP

Actual worker termination is required, but the retained handle lacks a qualified control path or independent exit evidence.

Computation/platform owner establishes carrier control, responsive task behavior and resource acknowledgements, then requalifies.

RERUN

The declared cancellation contract cannot be assessed because source/runtime identity, entry, final-boundary or completion evidence is missing or contradictory.

Test/release owner restores controlled ordering, captures the complete run and investigates failed bounds.

HOLD_EXTERNAL_EFFECTS

Cancellation also requires effects to be accounted for, but the domain outcome or reconciliation remains unresolved.

Domain-effect owner obtains authoritative outcome evidence before approving the broader operation.

The hold can coexist with successful logical cancellation or a witnessed local exit. None of those observations substitutes for reconciliation. Conversely, do not demand an interruption when the approved requirement only concerns abandoning result delivery; qualify the actual promise.

C4 and C6 show mechanisms that can satisfy particular local stopping conditions. They do not introduce a fifth, unconditional acceptance label for end-to-end cancellation. Record the conditions they proved and the remaining application obligations beside the selected release decision.

This is part of d: an engineer must identify which component owns an outcome and what evidence makes it reviewable. A release reviewer should be able to ask for the missing witness and receive a concrete record, not another invocation of isCancelled().

Preserve rejected candidates in regression coverage. PASS_7_CASES means the diagnostic assertions matched, including the intentionally insufficient worker-stopping behavior of C2 and C3.

Build broader software-delivery skills around explicit ownership

The useful deliverable from this laboratory is a cancellation policy that another engineer can inspect. It names the promised outcome, retains the object capable of requesting it, places an acknowledgement at the relevant boundary and keeps effect evidence available after the result is abandoned.

The seven cases make that policy concrete. The normal baseline proves that the job can perform its effect. The two CompletableFuture cancellation cases preserve a counterexample to automatic stopping. The FutureTask pair isolates the interrupt request on the same cooperative job. The token case exposes a deliberate check boundary, and the late case protects an already completed result.

Carry that evidence discipline into code review. Ask where ownership moves when execution is submitted, what remains alive when the caller leaves, and which record can settle a disputed outcome. Keep cleanup responsibilities explicit enough that a cancelled handle does not erase unfinished work from the team's view.

A release is defensible when its evidence supports the promise actually made. Accept logical cancellation when result abandonment is the requirement; repair uncontrolled termination, rerun missing evidence, and hold unresolved effects. The local counter provides no certificate that remote work was undone.

For a broader framework around software delivery, performance and practical projects, review the Software Engineering Program and confirm its current prerequisites and curriculum.