Mode kompatibilitas

Kompatibilitas backward, forward, dan full beserta contoh evolusi skema yang lolos dan yang ditolak.

Daftar isi

Skema tidak pernah berhenti di versi pertama, karena field baru selalu muncul setelah sistem berjalan. Topik ini menguji evolusi skema Avro dengan tiga kasus nyata, yaitu penambahan field, penghapusan field, dan perubahan tipe, untuk menunjukkan kasus mana yang lolos dan yang ditolak. Kerangka penilainya adalah tiga mode kompatibilitas yang diterapkan Schema Registry pada setiap subject, beserta pengaturannya lewat REST.

Tiga arah membaca lintas versi#

Kompatibilitas selalu dinyatakan dari arah membaca, bukan dari arah menulis. Mode backward menguji apakah skema baru masih dapat membaca data lama, mode forward menguji apakah skema lama dapat membaca data baru, dan mode full menguji keduanya sekaligus. Schema Registry menerapkan pemeriksaan ini pada setiap pendaftaran versi baru pada subject, lalu menerima atau menolak pendaftaran berdasarkan mode yang berlaku.

ModeArah yang diujiContoh perubahan yang lolos
BACKWARDskema baru membaca data lamapenambahan field dengan default
FORWARDskema lama membaca data barupenghapusan field
FULLkedua arah sekaliguspenambahan dan penghapusan field dengan default

Mode bawaan pada Registry adalah BACKWARD, karena pola konsumsi paling lazim adalah consumer baru yang membaca pesan lama yang masih tersimpan di log. Setiap mode memiliki varian transitive, misalnya BACKWARD_TRANSITIVE, yang menguji versi baru terhadap seluruh versi sebelumnya, bukan hanya versi terakhir. Mode pada subject dapat diubah lewat REST, dan pemeriksaannya berlaku sejak pendaftaran berikutnya.

Urutan penerapan perubahan turut ditentukan oleh mode yang dipilih. Mode BACKWARD memungkinkan consumer diperbarui lebih dahulu, karena consumer baru tetap sanggup membaca pesan lama yang ditulis producer yang belum diperbarui. Mode FORWARD justru menuntut producer diperbarui terlebih dahulu, karena producer baru menghasilkan pesan yang hanya dapat dibaca consumer lama bila perubahannya diizinkan mode tersebut.

bash Tetapkan mode kompatibilitas pada subject
# tidak-dijalankan
# tetapkan mode kompatibilitas FULL untuk subject pesanan-value
curl -X PUT http://localhost:8081/config/pesanan-value \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"compatibility": "FULL"}'

Penambahan field dengan default#

Penambahan field baru yang membawa nilai default adalah perubahan yang paling aman. Registry melakukan pemeriksaan pada saat pendaftaran versi, sedangkan demo di bawah melakukannya secara lokal dengan resolusi fastavro, sehingga logika kedua sisi terlihat tanpa layanan mana pun. Uji berikut menambahkan field catatan bertipe string dengan default string kosong pada skema v1 sehingga menjadi v2, lalu menguji kedua arah membaca.

python kompat-tambah.py
import io
import fastavro

skema_v1 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"}
]}
skema_v2 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"},
    {"name": "catatan", "type": "string", "default": ""}
]}

v1 = fastavro.parse_schema(skema_v1)
v2 = fastavro.parse_schema(skema_v2)

def tulis(skema, record):
    buf = io.BytesIO()
    fastavro.schemaless_writer(buf, skema, record)
    return buf.getvalue()

def uji(label, data, skema_writer, skema_reader):
    try:
        hasil = fastavro.schemaless_reader(io.BytesIO(data), skema_writer, skema_reader)
        print(f"{label}: LULUS -> {hasil}")
    except Exception as e:
        print(f"{label}: GAGAL -> {type(e).__name__}: {e}")

data_v1 = tulis(v1, {"id": "A-1", "jumlah": 2})
data_v2 = tulis(v2, {"id": "A-1", "jumlah": 2, "catatan": "sudah dibayar"})

# arah backward: reader baru (v2) membaca data lama (v1), catatan diisi default
uji("reader v2 membaca data v1", data_v1, v1, v2)

# arah forward: reader lama (v1) membaca data baru (v2), field ekstra diabaikan
uji("reader v1 membaca data v2", data_v2, v2, v1)
text Keluaran
reader v2 membaca data v1: LULUS -> {'id': 'A-1', 'jumlah': 2, 'catatan': ''}
reader v1 membaca data v2: LULUS -> {'id': 'A-1', 'jumlah': 2}

Arah backward lolos karena default mengisi catatan pada data lama, sedangkan arah forward lolos karena reader lama mengabaikan field yang tidak dikenalnya. Perubahan ini memenuhi pemeriksaan BACKWARD dan sekaligus FORWARD, dan itulah alasan Registry menyetujuinya pada kedua mode tersebut. Penambahan field tanpa default sebaliknya hanya lolos arah forward, karena reader baru tidak memiliki apa pun untuk mengisi field yang absen pada data lama.

Penambahan ini tidak mengubah byte yang sudah tersimpan di log, karena pesan lama tetap tersusun menurut skema v1. Default "" baru bekerja pada saat pembacaan, yaitu ketika resolusi mengisi field yang absen dari payload. Karena perubahan hanya bekerja di sisi reader, producer lama tidak perlu diubah selama pendaftaran versi baru berlangsung.

Penghapusan field#

Penghapusan field adalah cermin dari penambahan, dan cermin itu diterapkan pada uji berikut dengan menghapus catatan dari v2 sehingga menjadi v3. Uji ketiga menambahkan reader pembanding yang memiliki field catatan tanpa default, untuk menunjukkan peran default pada arah forward. Penghapusan semacam ini umum terjadi ketika field dianggap usang, sedangkan pesan lama yang masih membawa fieldnya tetap ada di log selama masa retensinya.

python kompat-hapus.py
import io
import fastavro

skema_v2 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"},
    {"name": "catatan", "type": "string", "default": ""}
]}
skema_v2_tanpa_default = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"},
    {"name": "catatan", "type": "string"}
]}
skema_v3 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"}
]}

v2 = fastavro.parse_schema(skema_v2)
v2_tanpa_default = fastavro.parse_schema(skema_v2_tanpa_default)
v3 = fastavro.parse_schema(skema_v3)

def tulis(skema, record):
    buf = io.BytesIO()
    fastavro.schemaless_writer(buf, skema, record)
    return buf.getvalue()

def uji(label, data, skema_writer, skema_reader):
    try:
        hasil = fastavro.schemaless_reader(io.BytesIO(data), skema_writer, skema_reader)
        print(f"{label}: LULUS -> {hasil}")
    except Exception as e:
        print(f"{label}: GAGAL -> {type(e).__name__}: {e}")

data_v2 = tulis(v2, {"id": "A-1", "jumlah": 2, "catatan": "sudah dibayar"})
data_v3 = tulis(v3, {"id": "A-1", "jumlah": 2})

# arah backward: reader v3 membaca data v2, field ekstra diabaikan
uji("reader v3 membaca data v2", data_v2, v2, v3)

# arah forward: reader v2 (punya default) membaca data v3, default mengisi
uji("reader v2 membaca data v3", data_v3, v3, v2)

# pembanding: reader tanpa default tidak sanggup membaca data yang hilang fieldnya
uji("reader tanpa default membaca data v3", data_v3, v3, v2_tanpa_default)
text Keluaran
reader v3 membaca data v2: LULUS -> {'id': 'A-1', 'jumlah': 2}
reader v2 membaca data v3: LULUS -> {'id': 'A-1', 'jumlah': 2, 'catatan': ''}
reader tanpa default membaca data v3: GAGAL -> SchemaResolutionError: No default value for field catatan in Pesanan

Arah backward lolos karena reader v3 mengabaikan field ekstra pada data v2, sedangkan arah forward lolos karena default pada reader v2 mengisi data v3 yang kehilangan fieldnya. Reader pembanding tanpa default gagal dengan pesan No default value for field catatan, sehingga mode FORWARD akan menolak evolusi ini bila versi sebelumnya tidak menyimpan default. Default yang dirancang sejak versi awal itulah yang menentukan arah mana yang tetap terbuka.

Letak penyimpanan default juga menentukan hasilnya. Nilai default tercatat pada skema reader yang tersimpan di Registry, bukan pada payload, sehingga data v3 tidak membawa field catatan sama sekali dan tetap dapat dibaca bila skema lamanya menyimpan default. Karena pencocokan hanya dilakukan atas nama field, perbedaan urutan field pun tidak mengganggu resolusi.

Perubahan tipe yang ditolak#

Perubahan tipe hanya boleh mengikuti promosi yang disetujui spesifikasi Avro, misalnya dari int ke long. Perubahan int menjadi string tidak ada dalam daftar promosi, dan uji berikut menunjukkan penolakannya pada kedua arah dengan mengubah tipe jumlah. Pembatasan ini menjaga byte yang tersimpan tetap ditafsirkan dengan cara yang sama oleh kedua versi, sehingga tidak ada nilai yang tiba-tiba berubah makna.

python kompat-tipe.py
import io
import fastavro

skema_v1 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "int"}
]}
skema_v4 = {"type": "record", "name": "Pesanan", "fields": [
    {"name": "id", "type": "string"},
    {"name": "jumlah", "type": "string"}
]}

v1 = fastavro.parse_schema(skema_v1)
v4 = fastavro.parse_schema(skema_v4)

def tulis(skema, record):
    buf = io.BytesIO()
    fastavro.schemaless_writer(buf, skema, record)
    return buf.getvalue()

def uji(label, data, skema_writer, skema_reader):
    try:
        hasil = fastavro.schemaless_reader(io.BytesIO(data), skema_writer, skema_reader)
        print(f"{label}: LULUS -> {hasil}")
    except Exception as e:
        print(f"{label}: GAGAL -> {type(e).__name__}: {e}")

data_v1 = tulis(v1, {"id": "A-1", "jumlah": 2})
data_v4 = tulis(v4, {"id": "A-1", "jumlah": "2"})

# perubahan tipe int menjadi string tidak punya promosi di Avro, kedua arah ditolak
uji("reader v1 membaca data v4", data_v4, v4, v1)
uji("reader v4 membaca data v1", data_v1, v1, v4)
text Keluaran
reader v1 membaca data v4: GAGAL -> SchemaResolutionError: Schema mismatch: string is not int
reader v4 membaca data v1: GAGAL -> SchemaResolutionError: Schema mismatch: int is not string

Kedua arah gagal dengan pesan Schema mismatch, sehingga mode kompatibilitas apa pun akan menolak pendaftaran skema v4 tersebut. Daftar promosi pada spesifikasi Avro memuat pelebaran angka, yaitu int ke long, float, atau double, long ke float atau double, serta float ke double. Konversi bolak-balik antara string dan bytes juga diizinkan. Di luar daftar itu, tipe dianggap berbeda sepenuhnya, dan pesan error pada demo adalah wujud penolakannya.

Jalan keluarnya bukan mengubah tipe field lama, melainkan menambahkan field baru seperti jumlah_teks dengan default, atau menerbitkan record dengan nama baru untuk payload yang bentuknya berbeda. Dengan field tambahan, consumer lama mengabaikan jumlah_teks dan terus bekerja, sedangkan consumer baru yang sudah mengetahui field itu mulai memanfaatkannya. Data lama tetap terbaca dan perpindahan dapat dilakukan secara bertahap, sehingga tidak ada pesan yang terputus maknanya.

Ringkasan#

  • Kompatibilitas dinyatakan dari arah membaca, dengan BACKWARD sebagai mode bawaan Registry dan berlaku per subject.
  • Penambahan field dengan default lolos kedua arah, penghapusan field bergantung pada default yang tersimpan di reader lama, dan perubahan tipe di luar promosi Avro ditolak kedua arah.
  • Keputusan lolos atau ditolak ditentukan resolusi skema pada waktu baca, sehingga desain default sejak versi pertama menjaga arah evolusi yang tetap terbuka.

Referensi#