Masalah data hilang setelah migrasi PDS Bluesky sering kali bukan kehilangan permanen, melainkan permintaan yang masih diarahkan ke PDS lama. Jika endpoint layanan dalam dokumen DID sudah berubah tetapi resolver, gateway, atau aplikasi masih memakai cache lama, pembacaan dan operasi tulis dapat terpecah di antara dua server.

Artikel ini menyajikan studi kasus hipotetis yang terinspirasi oleh perpindahan data Bluesky ke Eurosky. Kasus, log, dan root cause di bawah tidak menyatakan bahwa bug tersebut terjadi pada Waag atau Eurosky.

Arsitektur yang Perlu Dipahami Sebelum Debugging

Dalam AT Protocol, identitas akun direpresentasikan oleh DID. Dokumen DID biasanya memuat layanan bertipe AtprotoPersonalDataServer dengan ID #atproto_pds. Nilai serviceEndpoint menunjukkan PDS yang seharusnya melayani akun tersebut.

{
  "id": "did:plc:example123",
  "service": [
    {
      "id": "#atproto_pds",
      "type": "AtprotoPersonalDataServer",
      "serviceEndpoint": "https://pds-new.example"
    }
  ]
}

Alur sederhananya adalah:

  1. Handle diubah menjadi DID.
  2. DID di-resolve menjadi dokumen DID.
  3. Klien mencari #atproto_pds.
  4. Permintaan repository, blob, dan operasi tulis dikirim ke endpoint tersebut.
  5. Layanan pengindeks seperti AppView mengambil dan menyajikan data dari repository pengguna.

Keberhasilan login tidak membuktikan bahwa seluruh jalur data sudah menggunakan PDS yang benar. Login mungkin dilakukan langsung ke host baru, sementara komponen lain masih memakai endpoint lama dari cache. Sebaliknya, PDS lama mungkin masih menerima kredensial atau menyajikan salinan repository yang belum dinonaktifkan.

Kronologi Debug Migrasi PDS Bluesky

Gejala awal

Tim memindahkan akun did:plc:example123 dari pds-old.example ke pds-new.example. Setelah migrasi, ditemukan gejala berikut:

  • Login ke PDS baru berhasil.
  • Profil dan posting lama terlihat, tetapi beberapa posting terbaru tidak muncul.
  • Media tertentu menghasilkan respons tidak ditemukan.
  • Posting dari satu klien berhasil, tetapi tidak terlihat dari klien lain.
  • Operasi tulis kadang mengubah repository di PDS lama, kadang di PDS baru.

Kombinasi tersebut mengarah pada dugaan split routing: dua jalur permintaan menggunakan endpoint PDS yang berbeda. Keterlambatan indeks AppView juga dapat menyebabkan tampilan tertunda, tetapi tidak menjelaskan mengapa head repository pada dua PDS terus berubah secara terpisah.

Menghubungkan gejala dengan request ID

Setiap permintaan pada gateway diberi request ID dan mencatat DID, hasil resolusi, status cache, target host, serta CID repository sebelum dan sesudah operasi tulis. Header request ID bukan bagian wajib AT Protocol; ini adalah instrumentasi internal yang perlu diteruskan antarlayanan.

2025-02-11T09:14:02Z level=info request_id=req-7f21
  did=did:plc:example123 operation=createRecord
  resolver_cache=hit target=pds-old.example
  repo_head_before=bafy-old-101 repo_head_after=bafy-old-102 status=200

2025-02-11T09:14:07Z level=info request_id=req-7f22
  did=did:plc:example123 operation=getLatestCommit
  resolver_cache=miss target=pds-new.example
  repo_head=bafy-new-088 status=200

Log tersebut memberikan bukti yang lebih kuat daripada laporan “posting tidak muncul”. Operasi tulis pertama masuk ke PDS lama, sedangkan pembacaan berikutnya masuk ke PDS baru dengan head repository berbeda.

Jangan mencatat access token, refresh token, kata sandi, atau isi record privat. Request ID, DID, nama operasi, target host, status, durasi, dan CID biasanya sudah cukup untuk korelasi.

Memeriksa DID, Cache Resolver, Repo Head, dan Blob

1. Verifikasi endpoint otoritatif pada dokumen DID

Ambil dokumen DID dari sumber yang sesuai dengan metode DID. Untuk did:plc, dokumen dapat diperiksa melalui PLC directory. Untuk did:web, ikuti mekanisme resolusi did:web melalui HTTPS.

DID='did:plc:example123'

curl -fsS "https://plc.directory/${DID}" \
  | jq '.service[] | select(.id == "#atproto_pds")'

Bandingkan hasil tersebut dengan endpoint yang digunakan aplikasi. Jangan hanya memeriksa satu mesin pengembang. Resolver dapat berada di gateway API, worker, layanan autentikasi, AppView internal, proxy, atau cache proses masing-masing instance.

Cache sebaiknya menggunakan DID sebagai kunci utama. Handle dapat berubah dan memiliki jalur cache sendiri. Catat setidaknya nilai endpoint, waktu pengambilan, umur entri, sumber resolusi, dan alasan invalidasi.

type CachedPdsEndpoint = {
  did: string;
  endpoint: string;
  fetchedAt: string;
  expiresAt: string;
  source: "plc" | "did-web";
};

Kesalahan umum adalah memberikan TTL panjang tanpa mekanisme invalidasi eksplisit saat migrasi. Kesalahan lain adalah menghapus cache pada satu instance, sementara instance lain dan worker latar belakang masih menyimpan nilai lama di memori.

2. Bandingkan repo head pada kedua PDS

Endpoint sinkronisasi dapat digunakan untuk membandingkan commit terbaru secara langsung. Jalankan pemeriksaan terhadap host lama dan baru, bukan melalui resolver yang sedang dicurigai.

DID='did:plc:example123'
OLD='https://pds-old.example'
NEW='https://pds-new.example'

curl -fsS "$OLD/xrpc/com.atproto.sync.getLatestCommit?did=$DID" | jq
curl -fsS "$NEW/xrpc/com.atproto.sync.getLatestCommit?did=$DID" | jq

CID adalah alamat berbasis konten. Jika head berbeda, hal itu belum otomatis berarti salah satu repository rusak: mungkin ada commit baru yang sah atau proses impor belum selesai. Namun, jika kedua head terus berubah setelah cutover, berarti operasi tulis masih terjadi pada dua sisi.

Catat pasangan cid dan rev jika tersedia, waktu observasi, serta host yang diperiksa. Jangan membandingkan jumlah posting saja karena penghapusan, record non-posting, dan status indeks dapat membuat angka tersebut menyesatkan.

3. Bedakan masalah repository dari masalah indeks

Lakukan pemeriksaan melalui dua jalur:

  • Jalur PDS langsung: memeriksa repository dan record sebagai sumber data akun.
  • Jalur AppView: memeriksa hasil yang sudah diindeks dan ditampilkan kepada pengguna.

Jika record ada di PDS baru tetapi belum terlihat melalui AppView, masalah kemungkinan berada pada ingestion atau keterlambatan indeks. Jika record hanya ada di PDS lama, masalahnya berada pada routing atau sinkronisasi migrasi. Diagnosis ini mencegah tim mengulang impor repository ketika yang tertinggal sebenarnya hanya indeks.

4. Buat inventaris blob

Repository dan blob merupakan dua lapisan berbeda. Record dapat berisi referensi CID ke gambar atau video, tetapi objek blob-nya tetap harus tersedia di PDS tujuan. Karena itu, keberhasilan impor repository tidak menjamin seluruh media dapat dibaca.

Inventaris blob perlu membandingkan:

  • CID blob yang direferensikan oleh record aktif.
  • Blob yang tersedia di PDS lama.
  • Blob yang tersedia di PDS baru.
  • Ukuran atau metadata yang dapat diverifikasi tanpa mengubah isi.

Endpoint com.atproto.sync.listBlobs dapat membantu enumerasi apabila didukung dan diizinkan oleh PDS. Hasil harus dipaginasi menggunakan cursor, bukan hanya mengambil halaman pertama.

curl -fsS \
  "https://pds-new.example/xrpc/com.atproto.sync.listBlobs?did=did:plc:example123&limit=100" \
  | jq '{cursor, cids}'

Bandingkan blob yang direferensikan, bukan sekadar jumlah total blob. Penyimpanan dapat memiliki blob yatim yang tidak lagi dipakai record, sehingga kesamaan jumlah bukan bukti kesamaan data.

Root Cause: Endpoint Lama Di-cache dan Cutover Tidak Aman

Root cause studi kasus ini terdiri dari dua kondisi yang saling memperkuat:

  1. Endpoint PDS lama masih berada dalam cache resolver. Sebagian gateway dan worker tetap mengirim permintaan ke server lama walaupun dokumen DID telah menunjuk PDS baru.
  2. Urutan cutover tidak aman. Endpoint DID diperbarui ketika sinkronisasi akhir belum dilakukan dan PDS lama masih menerima operasi tulis.

Akibatnya terbentuk dua cabang repository. PDS lama menerima beberapa record baru, sementara PDS baru menjadi endpoint otoritatif dengan snapshot yang lebih lama. Blob yang dibuat mendekati waktu cutover juga tidak seluruhnya tersalin.

Masalah ini tidak dapat diperbaiki hanya dengan memperpanjang TTL atau menunggu cache kedaluwarsa. Selama PDS lama masih menerima operasi tulis, divergence dapat bertambah. Tim harus menghentikan perubahan, menentukan sumber data yang benar, lalu melakukan rekonsiliasi terkontrol.

Prosedur Perbaikan dan Cutover yang Aman

1. Hentikan operasi tulis sementara

Aktifkan mode pemeliharaan untuk akun yang dimigrasikan atau batasi operasi tulis pada kedua PDS. Pembacaan dapat dipertahankan jika tidak mengganggu rekonsiliasi. Tujuannya adalah memperoleh batas konsisten sehingga head repository dan inventaris blob tidak terus berubah.

Jangan hanya menonaktifkan antarmuka pengguna. Worker, job terjadwal, integrasi API, dan klien lama juga harus dicegah menulis.

2. Tentukan head repository yang harus dipertahankan

Ambil head dari PDS lama dan baru, lalu telusuri perubahan yang terjadi selama jendela migrasi. Jika PDS lama menerima write setelah snapshot, ekspor ulang atau gunakan mekanisme sinkronisasi yang didukung implementasi PDS.

Hindari menyalin tabel database atau direktori internal secara manual. Repository AT Protocol memiliki struktur, referensi CID, dan tanda tangan yang harus tetap valid. Gunakan fasilitas ekspor, impor, atau migrasi yang disediakan implementasi PDS.

3. Sinkronkan repository dan blob

  1. Salin snapshot repository awal ke tujuan.
  2. Setelah write freeze, ambil delta atau snapshot final dari sumber.
  3. Impor ke PDS tujuan dan verifikasi head.
  4. Inventaris semua blob yang direferensikan.
  5. Salin blob yang hilang dan uji pengambilan beberapa sampel serta seluruh blob yang terdeteksi hilang.

Simpan manifest migrasi berisi DID, head sumber, head tujuan setelah impor, jumlah record berdasarkan koleksi, daftar blob yang gagal, waktu freeze, dan identitas proses migrasi. Manifest ini berguna untuk audit dan rollback.

4. Perbarui endpoint DID

Setelah PDS tujuan konsisten, ubah #atproto_pds ke endpoint baru menggunakan mekanisme yang sesuai dengan metode DID. Pastikan perubahan benar-benar dapat di-resolve dari luar jaringan internal.

PDS lama sebaiknya tidak kembali menerima operasi tulis normal setelah titik ini. Jika implementasi mendukung status migrasi atau pengalihan terkontrol, gunakan mekanisme tersebut. Proxy transparan yang dibuat sendiri berisiko mengubah semantik autentikasi dan sebaiknya tidak dijadikan solusi tanpa pengujian menyeluruh.

5. Invalidasi cache secara menyeluruh

Invalidasi harus mencakup:

  • Cache resolver DID bersama.
  • Cache lokal di setiap instance aplikasi.
  • Worker dan proses berumur panjang.
  • Cache pemetaan handle ke DID jika ikut berubah.
  • Gateway, proxy, atau service discovery internal yang menyimpan target lama.
  • Entri negatif yang sebelumnya menyatakan dokumen atau endpoint tidak tersedia.

Setelah invalidasi, lakukan resolusi ulang dan catat bahwa hasil berasal dari sumber otoritatif, bukan dari fallback statis. Bila cache pihak eksternal tidak dapat dikendalikan, pertahankan periode transisi yang aman: PDS lama tidak boleh menerima write independen yang menciptakan cabang baru.

6. Verifikasi read dan write

Lakukan pengujian menggunakan akun migrasi khusus atau record sentinel:

  1. Login melalui jalur yang digunakan pengguna.
  2. Pastikan DID sesi sesuai dengan akun yang diuji.
  3. Buat satu record unik pada PDS baru.
  4. Pastikan head repository baru berubah.
  5. Baca record langsung dari PDS tujuan.
  6. Pastikan PDS lama tidak menerima perubahan head.
  7. Unggah media kecil yang valid dan verifikasi blob dapat diambil.
  8. Periksa kemunculannya melalui jalur indeks/AppView secara terpisah.
  9. Hapus record pengujian jika prosedur operasional mengizinkannya.

Verifikasi harus menggunakan beberapa instance atau wilayah layanan bila arsitektur didistribusikan. Tes dari satu pod tidak akan menemukan cache lokal yang tertinggal pada pod lain.

7. Siapkan rollback tanpa kehilangan write baru

Rollback bukan sekadar mengembalikan endpoint DID ke PDS lama. Jika PDS baru sudah menerima write, pengalihan langsung akan menghilangkan perubahan tersebut dari tampilan pengguna dan kembali menciptakan divergence.

Rencana rollback harus menentukan:

  • Kondisi yang memicu rollback.
  • Apakah operasi tulis harus dibekukan kembali.
  • Cara mengekspor perubahan terbaru dari PDS baru.
  • Cara menyinkronkan perubahan ke target rollback.
  • Head CID yang diharapkan sebelum endpoint dialihkan.
  • Prosedur invalidasi cache kedua.

Snapshot sumber, manifest blob, konfigurasi resolver, dan log request ID perlu dipertahankan sampai masa observasi selesai.

Pencegahan: Integration Test, Metrik, dan Checklist

Integration test migrasi

Integration test harus memeriksa jalur resolusi dan data sekaligus. Pseudocode berikut menekankan invariant utama, bukan API pustaka tertentu:

test("cutover routes all writes to the new PDS", async () => {
  await freezeWrites(accountDid);
  await syncRepositoryAndBlobs(oldPds, newPds, accountDid);
  await updateDidServiceEndpoint(accountDid, newPds);
  await invalidateDidCaches(accountDid);
  await enableWrites(accountDid, newPds);

  const oldHeadBefore = await latestCommit(oldPds, accountDid);
  const newHeadBefore = await latestCommit(newPds, accountDid);

  const record = await createSentinelRecord(accountDid, {
    text: `migration-test-${crypto.randomUUID()}`
  });

  const newHeadAfter = await latestCommit(newPds, accountDid);
  const oldHeadAfter = await latestCommit(oldPds, accountDid);

  expect(await resolvePds(accountDid)).toBe(newPds);
  expect(await readRecord(newPds, record.uri)).toEqual(record);
  expect(newHeadAfter.cid).not.toBe(newHeadBefore.cid);
  expect(oldHeadAfter.cid).toBe(oldHeadBefore.cid);
});

Tambahkan skenario cache hit, cache miss, restart worker, kegagalan impor blob, kegagalan pembaruan DID, dan rollback. Test juga harus memastikan token tidak terkirim ke host yang bukan endpoint terverifikasi.

Metrik dan alert

Nama metrik berikut hanyalah contoh instrumentasi aplikasi, bukan field standar AT Protocol:

  • did_resolver_cache_hits_total dan did_resolver_cache_misses_total.
  • did_endpoint_mismatch_total ketika target aktual berbeda dari endpoint hasil resolusi.
  • pds_request_total berdasarkan operasi, target host, dan status.
  • migration_blob_missing_total.
  • migration_repo_head_mismatch_total.
  • did_resolution_duration_seconds.

Jangan menjadikan DID penuh sebagai label metrik jika jumlah akun besar karena dapat menghasilkan kardinalitas tinggi. DID dan request ID lebih cocok ditempatkan di log terstruktur atau trace, sedangkan metrik menggunakan status, host, atau fase migrasi yang jumlah nilainya terbatas.

Checklist migrasi PDS

  • Dokumentasikan DID, PDS sumber, PDS tujuan, dan penanggung jawab cutover.
  • Uji autentikasi dan kapasitas penyimpanan tujuan.
  • Ambil snapshot repository dan inventaris blob.
  • Lakukan sinkronisasi awal sebelum jendela pemeliharaan.
  • Bekukan semua jalur operasi tulis.
  • Lakukan sinkronisasi final repository dan blob.
  • Verifikasi head CID dan record penting.
  • Perbarui endpoint #atproto_pds.
  • Invalidasi seluruh lapisan cache resolver.
  • Pastikan PDS lama tidak menerima write independen.
  • Uji read, write, media, dan jalur AppView secara terpisah.
  • Pantau mismatch endpoint, error blob, dan perubahan head.
  • Simpan manifest serta prosedur rollback.

Prinsip utamanya adalah memperlakukan migrasi PDS sebagai cutover sistem terdistribusi, bukan sekadar pemindahan file. Repository, blob, identitas DID, cache resolver, autentikasi, dan indeks harus berpindah dalam urutan yang menjaga satu sumber kebenaran. Dengan write freeze, perbandingan CID, inventaris blob, invalidasi cache, dan verifikasi dua arah, gejala “login berhasil tetapi data hilang” dapat ditelusuri tanpa menebak-nebak.