tutorial

26 Menangani Error dan Context pada Streaming RPC

Di era arsitektur cloud native, Remote Procedure Call (RPC) telah berevolusi dari sekadar permintaan-response sederhana menjadi arsitektur komunikasi canggih, salah satunya streaming RPC. Namun, semakin kompleks arsitekturnya, semakin menuntut pengembang memahami bagaimana menangani error dan context dengan baik. Dalam artikel ini, saya akan mengupas teknik menangani error dan context pada streaming RPC, terutama menggunakan gRPC, dan membungkusnya dengan studi kasus, contoh kode, simulasi, serta diagram alur untuk memperjelas konsepnya.


Apa Itu Streaming RPC?

Pada dasarnya, RPC adalah teknik agar sebuah aplikasi dapat memanggil fungsi/prosedur yang dijalankan di layanan berbeda (biasanya melalui jaringan). Pada streaming RPC, baik client, server, atau keduanya dapat mengirimkan sejumlah data secara berurutan (stream), bukan hanya satu permintaan dan satu jawaban.

Ada empat jenis RPC pada gRPC:

JenisClient StreamServer StreamContoh
UnaryTidakTidakLogin(username, password)
Client-side StreamingYaTidakUpload log secara batch
Server-side StreamingTidakYaNotifikasi update status
Bidirectional StreamingYaYaChat, game lobby, live data


Kenapa Error dan Context Penting pada Streaming RPC?

Sederhananya, streaming RPC berjalan dalam waktu yang lama atau berkelanjutan — misalnya client mengirim ribuan log ke server secara bertahap. Dalam proses itu, banyak hal bisa terjadi:

  • Client tiba-tiba disconnect.
  • Server overload lalu crash.
  • Timeout karena jaringan terputus.
  • Client ingin membatalkan proses (cancel).
  • Server menemukan error fatal dan harus memutuskan stream.

Mengabaikan error handling dan context management pada stream akan membuat sistem rentan, sulit di-maintain, atau bahkan terjadi memory leak.


Cara Menangani Error pada Streaming RPC

1. Mengenali Error gRPC

gRPC memiliki sistem status code yang serupa dengan HTTP (walau istilahnya berbeda). Ada beberapa kode umum yang sering ditemui:

Status CodePenjelasanSolusi Umum
OKTidak ada error (sukses)
CanceledClient membatalkan requestBersihkan resource
DeadlineExceededTimeout pada streamReview context/timeout
UnavailableServer tidak tersedia (down)Coba reconnect/backoff
InternalError internal di serverPeriksa log server
DataLossData stream corruptValidasi, retry

2. Menghandle Error pada Client Streaming

Misal, client streaming memberikan data secara bertahap ke server. Di sisi client Go, biasanya kita akan melihat pola:

stream, err := client.SendLogStream(ctx)
if err != nil {
    log.Fatalf("gagal membuka stream: %v", err)
}

for _, entry := range logs {
    if err := stream.Send(entry); err != nil {
        log.Printf("gagal mengirim log: %v", err)
        break
    }
}

resp, err := stream.CloseAndRecv()
if err != nil {
    log.Fatalf("gagal mendapatkan respons: %v", err)
}
log.Printf("Sukses upload: %v", resp.Total)

Bagaimana Jika Server Mengembalikan Error Saat Proses?

Ketika stream.Send() error, client wajib cek: apakah context sudah cancelled? Apakah error karena server sudah menutup koneksi lebih dulu?

3. Error Handling pada Server Streaming

Di sisi server, biasanya akan rutin mengirimkan data ke client. Suatu saat client bisa saja disconnect, menyebabkan Send() di server return error.

func (s *server) StreamNotifikasi(req *pb.Request, stream pb.Service_StreamNotifikasiServer) error {
    for notif := range notifikasiCh {
        if err := stream.Send(notif); err != nil {
            // Biasanya context canceled atau deadline exceeded
            log.Printf("gagal kirim notifikasi: %v", err)
            return err
        }
    }
    return nil
}

Perhatikan: Server WAJIB menangani error send untuk menghindari goroutine leak atau resource leak.


Mengelola Context pada Streaming RPC

Apa Itu Context di gRPC?

context.Context adalah objek penting di Go/gRPC untuk membawa timeout, cancellation, dan deadline di seluruh proses request (termasuk stream). Context ini menjadi pengontrol utama untuk membatalkan stream, menyetop resource, menghentikan proses stream.

Merangkai Context pada Streaming

Setiap stream di gRPC terikat dengan context yang sama dengan request utama.

  • Di sisi client, context biasanya diberikan saat pembuatan stream.
  • Di sisi server, gRPC otomatis menyediakan context per stream (bisa diakses via stream.Context()).

Contoh Penggunaan Context pada Client

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

stream, err := client.SendLogStream(ctx)
// ...

Jika stream proses melebihi 5 detik, context otomatis canceled. Semua operasi pada stream akan mengembalikan error bertipe “context canceled” atau “deadline exceeded”.

Menangkap Context pada Server

for {
    select {
    case <-stream.Context().Done():
        // Client disconnected atau cancel
        log.Println("stream dibatalkan oleh client")
        return stream.Context().Err()
    case notif := <-notifikasiCh:
        if err := stream.Send(notif); err != nil {
            // Gagal kirim (misal client disconnect)
            return err
        }
    }
}

Diagram Alur: Error dan Context pada Server Streaming

flowchart TD
    A[Mulai Stream] --> B{Kirim Notifikasi}
    B -->|Notifikasi Baru| C["stream.Send()"]
    C --> D{Error saat Send?}
    D -->|Ya| E["Periksa stream.Context().Err()"]
    D -->|Tidak| B
    E -->|Err Canceled/Deadline| F[Selesaikan stream]
    E -->|Error lain| G[Log error & return]

Studi Kasus: Upload File dengan Client Streaming

Bayangkan, kita ingin implementasi fitur upload file besar (misal log file) dari client ke server, secara chunked (streaming). Masalah: jaringan tidak stabil, client kadang cancel di tengah jalan, server overload.

1. Definisi Protobuf

service LogUploader {
    rpc UploadLogs(stream LogChunk) returns (UploadSummary);
}
message LogChunk {
    bytes content = 1;
}
message UploadSummary {
    int64 total = 1;
}

2. Implementasi di Server: Error dan Context Handling

func (s *server) UploadLogs(stream pb.LogUploader_UploadLogsServer) error {
    total := int64(0)
    for {
        select {
        case <-stream.Context().Done():
            // Client disconnect/timeout/cancel
            log.Printf("stream dibatalkan oleh client: %v", stream.Context().Err())
            return status.Error(codes.Canceled, "client membatalkan upload")
        default:
            chunk, err := stream.Recv()
            if err == io.EOF {
                return stream.SendAndClose(&pb.UploadSummary{Total: total})
            }
            if err != nil {
                log.Printf("error read stream: %v", err)
                return status.Error(codes.Internal, "gagal membaca data")
            }
            // Simulasi error: chunk terlalu besar
            if len(chunk.Content) > MAX_CHUNK { 
                return status.Error(codes.InvalidArgument, "chunk terlalu besar")
            }
            total += int64(len(chunk.Content))
        }
    }
}

3. Simulasi: Kasus – Client Cancel Upload

Jika client call cancel() pada context:

  • Operasi server pada stream.Context().Done() akan terpicu.
  • Server bisa segera membersihkan resource dan keluar.
  • Client akan menerima error Canceled.
StepClientServer
Mulai streamMembuka streamMenunggu/terima Recv
Upload sebagian chunkKirim chunkTerima, simpan, cek error
Cancel pada clientCancel contextStream Context Done triggered
Stream ditutupTerima error CanceledKeluar dari handler

Rekomendasi Praktis untuk Engineer

  1. Selalu check error pada setiap operasi stream — baik Send() maupun Recv().
  2. Observe context di server: Pantau channel <-stream.Context().Done() untuk cleanup cepat jika client disconnect atau timeout.
  3. Tangani status code dengan benar: Kembalikan error yang relevan (bukan hanya Internal). Gunakan status.Error di Go.
  4. Gunakan timeout sesuai kebutuhan: Jangan biarkan operasi streaming berjalan tanpa batas.
  5. Tes resilience: Simulasikan berbagai kondisi error saat pengembangan (client tiba-tiba disconnect, server overload, dsb).

Kesimpulan

Menangani error dan context secara benar pada streaming RPC tidak hanya membuat aplikasi Anda lebih tahan banting, tapi juga lebih efisien dan mudah di-maintain di production. Ingat, stream akan selalu menjadi rawan gagal karena berjalan “lama” dan melewati banyak edge case. Engineer yang baik tidak hanya membuat fitur berjalan, tapi mampu menangani segala kemungkinan error dengan elegan.

Selamat mencoba dan jangan lupa refactor! 🚀

comments powered by Disqus