Operasi I/O di dunia nyata tidak hanya gagal — kadang ia diam saja: server tidak menjawab, socket menggantung, dan program kita ikut menggantung bersamanya. Program async yang sehat harus bisa membatalkan task yang terlalu lama dan membatik batas waktu dengan rapi. Chapter ini membahas mekanisme pembatalan asyncio: CancelledError, Task.cancel(), asyncio.wait_for(), dan context manager asyncio.timeout().
CancelledError Bukan Exception Biasa #
Di asyncio, pembatalan dikirim sebagai exception — tetapi bukan Exception. Sejak Python 3.8, asyncio.CancelledError mewarisi langsung BaseException, satu level di atas Exception, tepat seperti KeyboardInterrupt:
import asyncio
print(asyncio.CancelledError.__mro__)
print(issubclass(asyncio.CancelledError, Exception))Output yang Dihasilkan:
(<class 'asyncio.exceptions.CancelledError'>, <class 'BaseException'>, <class 'object'>)
FalseHierarki ini sengaja dibuat demikian agar except Exception: yang umum dipakai untuk menangani error aplikasi tidak ikut menelan sinyal pembatalan. Konsekuensinya, issubclass(asyncio.CancelledError, Exception) bernilai False.
Membatalkan Task dengan cancel() #
Task.cancel() meminta event loop menaikkan CancelledError di titik await berikutnya di dalam task tersebut. Coroutine yang baik memanfaatkan except untuk pembersihan dan finally untuk penutupan resource:
import asyncio
async def proses_pesanan():
try:
print("proses dimulai")
await asyncio.sleep(60)
print("selesai diproses")
except asyncio.CancelledError:
print("dibatalkan: mengembalikan stok")
raise
finally:
print("finally: mencatat ke log")
async def main():
tugas = asyncio.create_task(proses_pesanan())
await asyncio.sleep(0.5)
tugas.cancel()
try:
await tugas
except asyncio.CancelledError:
print("main menangkap CancelledError")
asyncio.run(main())Output yang Dihasilkan:
proses dimulai
dibatalkan: mengembalikan stok
finally: mencatat ke log
main menangkap CancelledErrorMari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 4 (
try:): seluruh badan pekerjaan dibungkus supaya pembatalan bisa ditangani di tengah jalan, bukan menggugurkan task tanpa pembersihan. - Baris 6 (
await asyncio.sleep(60)): titik tunggu.mainmembatalkan task pada detik ke-0.5, jadisleeptidak pernah selesai dan baris 7 tidak pernah tercetak. - Baris 8 (
except asyncio.CancelledError:): menangkap sinyal pembatalan, menjalankan pembersihan domain (mengembalikan stok), laluraisepada baris 10 menaikkan ulang exception yang sama. - Baris 11 (
finally:): dijalankan setelah blokexcept, bahkan saat exception sedang merambat — di sinilah tempat menutup koneksi, menghentikan progress bar, atau mencatat log. - Baris 17 (
tugas.cancel()): permintaan pembatalan; exception baru diterbangkan ke titikawaitterdekat di dalam task.
Timeout dengan wait_for #
asyncio.wait_for(coro, timeout=n) membungkus coroutine dengan pembatas waktu: jika melebihi batas, coroutine dibatalkan (ia menerima CancelledError), lalu pemanggil menerima TimeoutError. Kedua exception tersebut terlihat di satu program:
import asyncio
import time
async def unduh_lambat():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print("unduh_lambat menerima CancelledError")
raise
async def main():
mulai = time.perf_counter()
try:
await asyncio.wait_for(unduh_lambat(), timeout=1)
except TimeoutError:
print(f"TimeoutError ditangkap setelah {time.perf_counter() - mulai:.1f} detik")
asyncio.run(main())Output yang Dihasilkan (nilai waktu dapat sedikit berbeda di mesin Anda):
unduh_lambat menerima CancelledError
TimeoutError ditangkap setelah 1.0 detikAlur lengkapnya: pada detik ke-1, wait_for membatalkan unduh_lambat; coroutine tersebut menerima CancelledError di sleep-nya, menaikkan ulang, lalu wait_for menerjemahkannya menjadi TimeoutError bagi main. Sejak Python 3.11, asyncio.TimeoutError adalah nama lain untuk TimeoutError bawaan, jadi except TimeoutError: menangkap keduanya.
Context Manager asyncio.timeout #
Untuk membatasi banyak operasi sekaligus dengan satu batas waktu, context manager asyncio.timeout() (Python 3.11+) lebih rapi — semua await di dalam blok berbagi jam yang sama:
import asyncio
async def main():
try:
async with asyncio.timeout(1):
await asyncio.sleep(10)
print("tidak pernah tercetak")
except TimeoutError:
print("blok dibatalkan setelah 1 detik")
asyncio.run(main())Output yang Dihasilkan:
blok dibatalkan setelah 1 detikSaat asyncio.sleep(10) melewati batas 1 detik, seluruh blok async with dibatalkan dan TimeoutError keluar dari blok — baris print("tidak pernah tercetak") tidak pernah tereksekusi. Pola try/finally di dalam coroutine tetap bekerja seperti biasa, sehingga pembersihan resource dari Chapter Async Context Managers tetap dijamin berjalan saat timeout terjadi.
Dengan pembatalan dan timeout di tangan, kita siap menghadapi sumber data yang paling sering menggantung itu sendiri: jaringan, pada Chapter Async Network I/O.