Cancellation, Interruption & Cleanup
Learn Java Error, Reliability & Observability Engineering - Part 016
Cancellation, interruption, dan cleanup di Java sebagai fondasi reliable lifecycle, graceful shutdown, timeout propagation, dan resource safety.
Part 016 — Cancellation, Interruption & Cleanup
1. Inti Pembelajaran
Banyak bug produksi Java bukan berasal dari algoritma yang salah, tetapi dari lifecycle yang tidak selesai dengan benar:
- request sudah timeout, tetapi worker masih berjalan,
- job dibatalkan, tetapi tetap menulis side effect,
- shutdown dimulai, tetapi executor tidak drain,
- thread di-interrupt, tetapi status interrupt ditelan,
- resource gagal ditutup, tetapi exception utama hilang,
- cleanup tidak idempotent,
- cancellation tidak dipropagasikan ke child task,
- timeout hanya terjadi di HTTP layer, bukan di operasi internal.
Target part ini:
Memahami cancellation, interruption, dan cleanup sebagai satu sistem lifecycle yang menjaga service tetap bisa berhenti, pulih, dan tidak meninggalkan kerja liar.
Definisi kerja:
| Konsep | Definisi |
|---|---|
| Cancellation | Permintaan agar pekerjaan berhenti sebelum selesai normal |
| Interruption | Mekanisme kooperatif Java untuk memberi sinyal ke thread agar berhenti/keluar dari blocking operation tertentu |
| Cleanup | Pelepasan resource dan penutupan side effect secara aman setelah success, failure, cancellation, atau shutdown |
| Deadline | Batas waktu absolut kapan pekerjaan tidak lagi berguna |
| Timeout | Mekanisme lokal untuk berhenti menunggu setelah durasi tertentu |
| Drain | Membiarkan pekerjaan yang sudah diterima selesai dalam batas waktu tertentu |
| Abort | Menghentikan atau menolak pekerjaan karena tidak aman/terlambat |
2. Kaufman Skill Deconstruction
Skill besar ini dipecah menjadi sub-skill praktis:
| Sub-skill | Kemampuan | Output Praktis |
|---|---|---|
| Interruption semantics | Paham interrupt sebagai signal, bukan force kill | Correct catch policy |
| Cancellation propagation | Membawa cancel/deadline dari caller ke child work | Cancellation-aware service |
| Blocking awareness | Tahu call mana yang interruptible dan mana yang tidak | Blocking operation inventory |
| Executor lifecycle | Shutdown, shutdownNow, awaitTermination, reject policy | Graceful executor helper |
| Cleanup discipline | Resource close, rollback, unlock, delete temp, release permit | Cleanup checklist |
| Idempotent cleanup | Aman jika cleanup dipanggil lebih dari sekali | Safe close pattern |
| Observability | Membedakan failure, timeout, cancellation, shutdown | Lifecycle telemetry |
| Testing | Membuktikan job berhenti saat diminta | Cancellation tests |
Prinsip Kaufman:
Jangan menghafal
InterruptedException. Latih kemampuan menjawab: “apa yang harus berhenti, kapan, bagaimana, dan bukti apa yang menunjukkan sudah berhenti?”
3. Mental Model: Cancellation Adalah Kontrak Kooperatif
Java tidak menyediakan cara aman untuk membunuh thread secara paksa dalam aplikasi normal. Interruption adalah mekanisme kooperatif: satu thread mengirim sinyal, thread lain harus memeriksa atau merespons sinyal tersebut.
Kontraknya:
- caller boleh meminta pembatalan,
- worker harus punya titik observasi cancellation,
- blocking operation harus diberi timeout/interrupt support,
- cleanup harus tetap berjalan,
- side effect harus tidak setengah-commit,
- telemetry harus membedakan cancellation dari error biasa.
Jika worker tidak kooperatif, cancellation hanya menjadi flag yang tidak berpengaruh.
4. Interruption di Java
4.1 Apa Itu Interrupt?
Interrupt adalah sinyal yang disimpan sebagai status pada thread.
Operasi tertentu seperti Thread.sleep, Object.wait, Thread.join, BlockingQueue.take, dan banyak API blocking lain dapat merespons interrupt dengan melempar InterruptedException.
Penting:
InterruptedExceptionbukan “error bisnis”. Itu sinyal lifecycle.
4.2 Interrupt Bukan Force Stop
Buruk mental model:
“Kalau thread di-interrupt, thread otomatis mati.”
Benar:
“Kalau thread di-interrupt, thread diberi kesempatan untuk berhenti di titik yang mendukung interruption atau saat ia memeriksa status interrupt.”
Contoh cooperative loop:
public final class Worker implements Runnable {
private final BlockingQueue<Job> queue;
public Worker(BlockingQueue<Job> queue) {
this.queue = queue;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Job job = queue.take();
process(job);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
cleanup();
}
private void process(Job job) {
// process one bounded unit of work
}
private void cleanup() {
// release resources, flush buffers, close handles
}
}
Aturan penting:
- tangkap
InterruptedException, - restore interrupt status jika tidak langsung throw,
- keluar dari loop atau propagate cancellation,
- jangan lanjut seperti tidak terjadi apa-apa.
4.3 Kenapa Restore Interrupt Status?
Saat banyak method blocking melempar InterruptedException, status interrupt thread bisa dibersihkan. Jika layer saat ini tidak bisa melempar InterruptedException ke caller, ia harus restore status:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OperationCancelledException("Operation was interrupted", e);
}
Tanpa restore, layer atas tidak tahu bahwa cancellation/shutdown sedang terjadi.
5. Policy untuk InterruptedException
Ada tiga respons valid.
5.1 Propagate
Jika method bisa deklarasi checked exception:
public Job takeNextJob() throws InterruptedException {
return queue.take();
}
Cocok untuk lower-level blocking abstraction.
5.2 Restore and Return/Exit
Jika berada di Runnable.run() yang tidak bisa throw checked exception:
@Override
public void run() {
try {
performLoop();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
cleanup();
}
}
5.3 Restore and Translate
Jika API domain tidak ingin expose InterruptedException:
public CaseExportResult export(CaseExportCommand command) {
try {
return exporter.export(command);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OperationCancelledException(
"CASE_EXPORT_INTERRUPTED",
"Case export was cancelled or service is shutting down",
e
);
}
}
Terjemahkan ke exception domain/lifecycle, bukan RuntimeException generik.
6. Anti-Pattern Interruption
6.1 Swallow Interrupt
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
Masalah:
- shutdown melambat,
- cancellation hilang,
- executor tidak terminate,
- service tampak hang,
- caller timeout tetapi pekerjaan masih jalan.
6.2 Wrap Tanpa Restore
catch (InterruptedException e) {
throw new RuntimeException(e);
}
Masalah:
Interrupt status hilang. Layer atas tidak bisa mendeteksi cancellation.
6.3 Continue After Interrupt
catch (InterruptedException e) {
logger.warn("interrupted", e);
}
// lanjut proses mutasi
repository.save(entity);
Jika interrupt berarti shutdown/deadline exceeded, melanjutkan mutasi bisa berbahaya.
6.4 Treat Cancellation as System Error
Cancellation bukan selalu error. Kadang itu expected:
- client disconnect,
- request deadline expired,
- shutdown drain finished,
- job explicitly cancelled,
- circuit/bulkhead rejecting work.
Jangan jadikan semua cancellation sebagai error alert.
7. Cancellation vs Timeout vs Deadline
7.1 Timeout
Timeout adalah durasi lokal.
client.callWithTimeout(Duration.ofMillis(500));
7.2 Deadline
Deadline adalah waktu absolut.
public record Deadline(Instant expiresAt) {
public Duration remaining(Clock clock) {
Duration remaining = Duration.between(clock.instant(), expiresAt);
return remaining.isNegative() ? Duration.ZERO : remaining;
}
public boolean expired(Clock clock) {
return !clock.instant().isBefore(expiresAt);
}
}
Deadline lebih baik untuk call chain panjang karena setiap layer tahu sisa waktu.
7.3 Cancellation
Cancellation adalah intent untuk menghentikan pekerjaan.
Bisa disebabkan oleh:
- timeout,
- explicit user cancel,
- shutdown,
- parent task failure,
- quota/budget exhausted,
- circuit open,
- client disconnected.
8. Cancellation Token Pattern
Java interruption berguna, tetapi tidak semua API memeriksa interrupt. Untuk domain workflow, token eksplisit sering membantu.
public final class CancellationToken {
private final AtomicBoolean cancelled = new AtomicBoolean(false);
private final AtomicReference<String> reason = new AtomicReference<>();
public void cancel(String reasonCode) {
this.reason.compareAndSet(null, reasonCode);
this.cancelled.set(true);
}
public boolean isCancelled() {
return cancelled.get() || Thread.currentThread().isInterrupted();
}
public void throwIfCancelled() {
if (isCancelled()) {
throw new OperationCancelledException(
reason.get() == null ? "INTERRUPTED" : reason.get(),
"Operation was cancelled"
);
}
}
}
Pemakaian:
public void exportCases(ExportCommand command, CancellationToken token) {
for (CaseId caseId : command.caseIds()) {
token.throwIfCancelled();
exportOneCase(caseId, token);
}
}
Manfaat:
- bisa dipakai di loop CPU-bound,
- bisa membawa reason code,
- bisa dites tanpa thread interrupt,
- bisa dikombinasikan dengan deadline,
- bisa dipropagasikan ke child operation.
Risiko:
- jika terlalu custom, tidak terintegrasi dengan blocking API,
- bisa lupa dicek,
- bisa berbeda semantics antar tim,
- tetap perlu interrupt handling untuk blocking call.
9. Cancellation Checkpoints
Task panjang harus punya checkpoint.
Buruk:
public void processLargeFile(Path file) {
List<Row> rows = parseEntireFile(file);
validateAll(rows);
writeAll(rows);
}
Lebih baik:
public void processLargeFile(Path file, CancellationToken token) {
try (Stream<String> lines = Files.lines(file)) {
Iterator<String> iterator = lines.iterator();
while (iterator.hasNext()) {
token.throwIfCancelled();
Row row = parse(iterator.next());
validate(row);
write(row);
}
} catch (IOException e) {
throw new FileProcessingException("FILE_PROCESSING_FAILED", e);
}
}
Checkpoint ideal berada:
- sebelum expensive operation,
- setelah blocking wait,
- antar batch,
- sebelum side effect,
- sebelum commit,
- sebelum publish event,
- setelah retry loop,
- sebelum mengambil resource baru.
10. Cancellation dan Side Effect
Pertanyaan paling penting:
Jika cancellation terjadi di tengah operasi, state apa yang sudah berubah?
Contoh bahaya:
public void approveCase(CaseId caseId) {
caseRepository.markApproved(caseId);
notificationClient.sendApprovalNotification(caseId);
auditClient.writeApprovalAudit(caseId);
}
Jika cancellation terjadi setelah markApproved tetapi sebelum audit, invariant rusak.
Solusi bukan sekadar catch exception. Solusi adalah desain side effect:
- transaction boundary jelas,
- outbox untuk event/audit,
- idempotency key,
- compensation jika perlu,
- reconciliation job,
- before-commit cancellation check,
- no cancellation inside critical section tertentu.
Contoh critical section:
@Transactional
public void approveCase(ApproveCaseCommand command, CancellationToken token) {
token.throwIfCancelled();
CaseFile file = caseRepository.lock(command.caseId());
approvalPolicy.ensureCanApprove(file);
token.throwIfCancelled();
file.approve(command.actorId());
caseRepository.save(file);
outboxRepository.append(AuditEvent.caseApproved(file.id(), command.actorId()));
// Setelah titik ini, cancellation tidak boleh membatalkan commit sebagian.
}
Catatan:
Cancellation harus dicek sebelum commit point. Setelah commit point, gunakan reconciliation/idempotent continuation, bukan rollback imajiner.
11. ExecutorService Lifecycle
ExecutorService adalah sumber utama lifecycle bugs.
Java menyediakan dua metode utama:
| Method | Makna |
|---|---|
shutdown() | Tidak menerima task baru, task yang sudah submitted boleh selesai |
shutdownNow() | Mencoba menghentikan task aktif dan mengembalikan task yang belum mulai |
awaitTermination() | Menunggu executor terminate sampai timeout |
Helper produksi:
public final class ExecutorShutdown {
private ExecutorShutdown() {}
public static void shutdownGracefully(
ExecutorService executor,
Duration quietPeriod,
Duration forcePeriod,
Logger logger
) {
executor.shutdown();
try {
if (!executor.awaitTermination(quietPeriod.toMillis(), TimeUnit.MILLISECONDS)) {
List<Runnable> dropped = executor.shutdownNow();
logger.warn("executor_force_shutdown droppedTasks={}", dropped.size());
if (!executor.awaitTermination(forcePeriod.toMillis(), TimeUnit.MILLISECONDS)) {
logger.error("executor_did_not_terminate");
}
}
} catch (InterruptedException e) {
List<Runnable> dropped = executor.shutdownNow();
logger.warn("executor_shutdown_interrupted droppedTasks={}", dropped.size());
Thread.currentThread().interrupt();
}
}
}
Prinsip:
- stop accepting new work,
- wait bounded time,
- request cancellation,
- wait bounded time again,
- restore interrupt if interrupted,
- log dropped tasks,
- expose metric termination failure.
12. Future Cancellation
Future.cancel(true) meminta cancellation dan dapat mengirim interrupt jika task sudah berjalan.
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<?> future = executor.submit(() -> runLongTask());
boolean cancelled = future.cancel(true);
Tetapi task harus kooperatif:
private void runLongTask() {
while (!Thread.currentThread().isInterrupted()) {
doOneSmallUnit();
}
cleanup();
}
Jika task CPU-bound tidak pernah memeriksa interrupt, cancellation tidak efektif.
13. CompletableFuture Cancellation Caveat
CompletableFuture memudahkan async composition, tetapi cancellation semantics sering disalahpahami.
Contoh:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> expensiveCall());
future.cancel(true);
Jangan berasumsi semua underlying work otomatis berhenti dengan cara yang sama seperti task interruptible tradisional. Desain async work tetap perlu:
- timeout eksplisit,
- cancellation token,
- executor yang dikelola,
- bounded queue,
- cooperative check,
- cleanup,
- observability.
Pattern yang lebih eksplisit:
public CompletableFuture<Report> generateReportAsync(
ReportCommand command,
CancellationToken token,
Executor executor
) {
return CompletableFuture.supplyAsync(() -> {
token.throwIfCancelled();
return reportGenerator.generate(command, token);
}, executor);
}
14. Cleanup Discipline
Cleanup harus terjadi untuk semua exit path:
- success,
- exception,
- cancellation,
- timeout,
- shutdown,
- partial initialization failure.
Gunakan try/finally atau try-with-resources.
public void writeExport(Path target, List<Row> rows) {
Path temp = target.resolveSibling(target.getFileName() + ".tmp");
try {
writeRows(temp, rows);
Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
throw new ExportFailedException("EXPORT_WRITE_FAILED", e);
} finally {
deleteIfExists(temp);
}
}
Cleanup juga bisa gagal. Jangan biarkan cleanup failure menutupi primary failure tanpa sengaja.
15. Try-With-Resources dan AutoCloseable
Untuk resource yang mengimplementasikan AutoCloseable, gunakan try-with-resources.
try (InputStream in = Files.newInputStream(input);
OutputStream out = Files.newOutputStream(output)) {
in.transferTo(out);
}
Manfaat:
- close otomatis,
- close order terbalik dari deklarasi,
- suppressed exception dipertahankan,
- code lebih pendek,
- failure path lebih aman.
Jika exception terjadi di body dan close() juga gagal, exception dari close menjadi suppressed exception.
Observability helper:
public static void logSuppressed(Throwable throwable, Logger logger) {
for (Throwable suppressed : throwable.getSuppressed()) {
logger.warn("suppressed_exception type={} message={}",
suppressed.getClass().getName(),
suppressed.getMessage());
}
}
Jangan abaikan suppressed exceptions saat debugging resource leak atau close failure.
16. Cleanup Harus Idempotent
Dalam sistem distributed, cleanup bisa dipanggil lebih dari sekali:
- normal finally,
- timeout handler,
- shutdown hook,
- retry compensation,
- manual operator action.
Pattern:
public final class SafeResource implements AutoCloseable {
private final AtomicBoolean closed = new AtomicBoolean(false);
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
release();
}
}
private void release() {
// actual release logic
}
}
Idempotent cleanup mengurangi risiko:
- double release,
- double refund,
- double unlock,
- double event publish,
- double delete.
Tetapi jangan gunakan idempotency untuk menyembunyikan lifecycle yang tidak jelas. Tetap desain ownership resource.
17. Resource Ownership
Pertanyaan penting:
Siapa yang bertanggung jawab menutup resource?
Anti-pattern:
public void process(InputStream input) {
try (input) { // menutup resource yang mungkin dimiliki caller
// process
} catch (IOException e) {
throw new ProcessingException(e);
}
}
Ini hanya benar jika kontrak method menyatakan method mengambil ownership.
Lebih eksplisit:
/**
* Reads all bytes from the given stream. This method does not close the stream.
*/
public byte[] readPayload(InputStream input) throws IOException {
return input.readAllBytes();
}
/**
* Opens and owns the stream lifecycle.
*/
public byte[] readPayload(Path path) {
try (InputStream input = Files.newInputStream(path)) {
return input.readAllBytes();
} catch (IOException e) {
throw new PayloadReadException("PAYLOAD_READ_FAILED", e);
}
}
Rule:
- pembuka resource biasanya penutup resource,
- transfer ownership harus eksplisit,
- jangan tutup resource yang masih dipakai caller,
- jangan biarkan resource tanpa owner.
18. Locks, Semaphores, dan Permits
Cleanup bukan hanya file/socket. Lock dan permit juga resource.
Semaphore semaphore = new Semaphore(10);
public void callDependency() {
boolean acquired = false;
try {
semaphore.acquire();
acquired = true;
dependency.call();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OperationCancelledException("DEPENDENCY_CALL_INTERRUPTED", e);
} finally {
if (acquired) {
semaphore.release();
}
}
}
Dengan Lock:
lock.lock();
try {
updateSharedState();
} finally {
lock.unlock();
}
Jika lock acquisition interruptible:
try {
lock.lockInterruptibly();
try {
updateSharedState();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OperationCancelledException("LOCK_ACQUIRE_INTERRUPTED", e);
}
19. Shutdown Hooks dan Cancellation
Shutdown hook bukan tempat untuk pekerjaan panjang tanpa batas.
Buruk:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
flushEverything();
callExternalServices();
waitForever();
}));
Lebih baik:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("shutdown_hook_started");
ExecutorShutdown.shutdownGracefully(
workerPool,
Duration.ofSeconds(20),
Duration.ofSeconds(5),
logger
);
logger.info("shutdown_hook_completed");
}));
Prinsip:
- bounded,
- idempotent,
- tidak menerima work baru,
- drain in-flight work,
- stop background workers,
- flush telemetry/logs jika aman,
- jangan bergantung pada dependency eksternal yang mungkin sudah mati,
- jangan deadlock.
20. Observability untuk Cancellation dan Cleanup
20.1 Logs
Gunakan event lifecycle:
logger.info("operation_cancelled operation={} reason={} correlationId={}",
operation,
reasonCode,
correlationId);
logger.warn("cleanup_failed resource={} operation={} correlationId={}",
resourceName,
operation,
correlationId,
exception);
Bedakan:
- cancelled_by_client,
- deadline_exceeded,
- shutdown_requested,
- interrupted,
- cleanup_failed,
- executor_force_shutdown,
- dropped_task.
20.2 Metrics
operation_cancelled_total{operation,reason}
operation_timeout_total{operation}
executor_shutdown_duration_seconds{executor}
executor_force_shutdown_total{executor}
cleanup_failed_total{resource,type}
interrupted_total{operation}
dropped_task_total{executor}
Jangan gunakan correlationId sebagai tag.
20.3 Traces
Span event:
Span.current().addEvent("operation.cancelled", Attributes.of(
stringKey("cancellation.reason"), reasonCode,
stringKey("app.operation"), operation
));
Untuk cleanup failure:
Span.current().addEvent("cleanup.failed", Attributes.of(
stringKey("resource.name"), resourceName,
stringKey("exception.type"), exception.getClass().getName()
));
21. Cancellation Testing
21.1 Test Interrupt Handling
@Test
void workerShouldStopWhenInterrupted() throws Exception {
BlockingQueue<Job> queue = new LinkedBlockingQueue<>();
Thread worker = new Thread(new Worker(queue));
worker.start();
worker.interrupt();
worker.join(Duration.ofSeconds(2).toMillis());
assertThat(worker.isAlive()).isFalse();
}
21.2 Test Restore Interrupt
@Test
void shouldRestoreInterruptStatusWhenInterrupted() {
Thread.currentThread().interrupt();
try {
assertThatThrownBy(() -> service.blockingOperation())
.isInstanceOf(OperationCancelledException.class);
assertThat(Thread.currentThread().isInterrupted()).isTrue();
} finally {
Thread.interrupted(); // clear for test runner
}
}
21.3 Test Cleanup on Failure
@Test
void shouldCloseResourceWhenProcessingFails() {
FakeResource resource = new FakeResource();
assertThatThrownBy(() -> processor.process(resource, failingInput()))
.isInstanceOf(ProcessingException.class);
assertThat(resource.isClosed()).isTrue();
}
21.4 Test Executor Shutdown
@Test
void shouldTerminateExecutorOnShutdown() {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// simulate work
}
});
ExecutorShutdown.shutdownGracefully(
executor,
Duration.ofMillis(100),
Duration.ofMillis(100),
logger
);
assertThat(executor.isTerminated()).isTrue();
}
22. Decision Matrix
| Situation | Recommended Action |
|---|---|
Method declares throws InterruptedException | Propagate |
Runnable.run() catches InterruptedException | Restore interrupt and exit |
| Domain API does not expose checked exception | Restore and translate to cancellation exception |
| CPU-bound loop | Check interrupt/token periodically |
| Blocking IO | Use timeout and close resource if interrupt not enough |
| Executor shutdown | shutdown, awaitTermination, shutdownNow, restore interrupt |
| Critical commit section | Check cancellation before commit; after commit use reconciliation |
| Cleanup fails after primary failure | Preserve primary, inspect/log suppressed/cleanup failure |
| Client disconnect | Stop useless work if safe |
| Background job cancellation | Mark job cancelled, cleanup, no partial success |
23. Production Checklist
Untuk setiap long-running operation:
- Apakah punya deadline?
- Apakah deadline dipropagasikan ke dependency?
- Apakah operation bisa dibatalkan?
- Apakah ada cancellation checkpoint?
- Apakah blocking call interruptible?
- Apakah blocking call punya timeout?
- Apakah
InterruptedExceptiondipropagasikan atau status interrupt direstore? - Apakah cleanup selalu terjadi?
- Apakah cleanup idempotent?
- Siapa owner resource?
- Apakah side effect aman jika cancellation terjadi di tengah?
- Apakah commit point jelas?
- Apakah executor bisa shutdown gracefully?
- Apakah dropped task dicatat?
- Apakah cancellation dibedakan dari failure?
- Apakah cleanup failure observable?
- Apakah test membuktikan task berhenti?
- Apakah shutdown hook bounded?
- Apakah retry loop menghormati cancellation?
- Apakah manual operator bisa melihat status cancelled/degraded?
24. Latihan 20 Jam — Cancellation, Interruption & Cleanup
Jam 1–3: Inventory Blocking Operation
Cari di service:
Thread.sleep,BlockingQueue.take,Future.get,- DB call,
- HTTP call,
- file IO,
- lock/semaphore,
- scheduler/background job.
Tandai mana yang interruptible dan mana yang hanya timeout-based.
Jam 4–6: Interrupt Policy Review
Cari semua catch (InterruptedException).
Klasifikasikan:
- propagate,
- restore and exit,
- restore and translate,
- broken/swallowed.
Refactor yang broken.
Jam 7–10: Add Cancellation Checkpoints
Ambil satu batch/job panjang. Tambahkan checkpoint:
- antar item,
- antar batch,
- sebelum external call,
- sebelum commit,
- sebelum publish event.
Jam 11–13: Executor Shutdown Helper
Buat helper shutdownGracefully dan gunakan di satu background worker.
Jam 14–16: Cleanup Test
Buat test untuk:
- resource close saat success,
- resource close saat failure,
- resource close saat cancellation,
- cleanup idempotent.
Jam 17–18: Observability
Tambahkan metric/log untuk:
- cancellation,
- timeout,
- cleanup failure,
- dropped task,
- executor force shutdown.
Jam 19–20: Failure Injection
Simulasikan shutdown saat:
- worker idle,
- worker sedang blocking,
- worker sedang memproses batch,
- worker tepat sebelum commit,
- cleanup gagal.
Buktikan service berhenti dalam batas waktu.
25. Ringkasan
Cancellation, interruption, dan cleanup adalah fondasi reliability lifecycle.
Mental model utama:
- Interruption adalah signal kooperatif, bukan force kill.
InterruptedExceptionadalah sinyal lifecycle, bukan sekadar exception teknis.- Jangan swallow interrupt.
- Restore interrupt jika tidak propagate.
- Cancellation harus punya checkpoint.
- Deadline lebih kuat daripada timeout lokal untuk call chain panjang.
- Cleanup harus berjalan di success, failure, cancellation, dan shutdown.
- Cleanup harus idempotent.
- Resource ownership harus eksplisit.
- Executor shutdown harus bounded.
- Side effect harus punya commit point yang jelas.
- Cancellation harus observable dan tidak disamakan dengan error biasa.
Part berikutnya akan membahas error flow pada async dan reactive programming: bagaimana exception, cancellation, context, dan telemetry berubah ketika eksekusi tidak lagi linear di satu thread.
You just completed lesson 16 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.