Topik temu duga representatif

Temu Duga Backend: Bagaimana Anda Menyelesaikan Masalah Dwi-Tulis Pangkalan Data dan Broker Mesej?

BackendSukar
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Perkhidmatan pesanan menulis pesanan ke PostgreSQL dan menerbitkan OrderCreated ke broker mesej. Tanpa distributed two-phase commit, bagaimanakah anda memastikan bahawa pesanan yang diundur balik (rolled back) tidak mengeluarkan sebarang acara, pesanan yang dikomit akhirnya mengeluarkan sekurang-kurangnya satu acara, dan kerosakan geganti atau pengguna tidak menduplikasi kesan perniagaan? Jelaskan juga tentang susunan, operasi, dan pengesahan.

Keperluan dan Konteks Berkaitan

Perkhidmatan pesanan mesti melakukan dua perkara untuk satu arahan:

  1. menulis pesanan ke PostgreSQL; dan
  2. menerbitkan acara OrderCreated ke broker mesej.

Kontrak perniagaan adalah lebih ketat daripada sekadar "cuba kedua-dua panggilan." Jika transaksi pangkalan data diundur balik (rolls back), tiada acara boleh menerangkan pesanan yang tidak wujud itu. Jika transaksi dikomit, niat untuk menerbitkan mesti kekal walaupun proses terhenti (process crash) dan akhirnya sampai ke broker. Acara bagi pesanan yang sama mesti kekal mengikut susunan, manakala susunan global adalah tidak perlu. Geganti dan pengguna boleh terhenti pada bila-bila masa, dan broker mungkin menghantar semula. Distributed two-phase commit tidak tersedia.

Ini ialah masalah transactional outbox. Ia muncul setiap kali satu permintaan mengubah keadaan transaksi dan mesti mencetuskan kerja dalam sistem lain secara boleh dipercayai: tempahan inventori, pengebilan, pengindeksan carian, e-mel, webhook, atau analitik. Matlamatnya bukanlah penghantaran exactly-once yang ajaib. Matlamatnya adalah untuk mengenal pasti sempadan atomik yang tersedia, menjadikan niat rentas sempadan itu tahan lasak (durable), dan menjadikan percubaan semula (retries) selamat.

Perkara yang Dinilai oleh Penemu Duga

Isyarat pertama ialah penaakulan tetingkap kegagalan (failure-window reasoning). "Tulis pangkalan data, kemudian terbit" akan kehilangan acara jika proses terhenti selepas komit. "Terbit, kemudian komit" mendedahkan acara walaupun pangkalan data kemudiannya diundur balik. Panggilan balik selepas komit (after-commit callback) dalam memori masih akan hilang bersama proses tersebut. Jawapan yang kukuh menyatakan tetingkap-tetingkap ini sebelum mencadangkan corak reka bentuk.

Isyarat kedua ialah jaminan yang tepat. Baris perniagaan dan baris outbox boleh dikomit secara atomik dalam satu transaksi pangkalan data tempatan. Penerbitan broker berlaku kemudian. Ini menjamin niat acara yang tahan lasak untuk setiap mutasi yang dikomit; ia tidak menjadikan pangkalan data dan broker sebagai satu transaksi, dan ia tidak menjanjikan penghantaran exactly-once.

Isyarat ketiga ialah keselamatan percubaan semula hujung ke hujung (end-to-end retry safety). Jika broker menerima acara dan geganti terhenti sebelum merekodkan kejayaan, acara itu akan diterbitkan semula. Oleh itu, geganti menyediakan penerbitan at-least-once, dan setiap pengguna mesti menjadikan kesan perniagaannya idempoten. Jawapan yang baik juga membezakan kesan pangkalan data tempatan pengguna daripada kesan sampingan luaran seperti mengenakan caj pada kad.

Isyarat terakhir ialah susunan dan kebolehoperasian: nombor jujukan per-agregat, kunci pemecahan (partition keys), pemilikan geganti serentak (concurrent relay ownership), acara racun (poison events), dasar percubaan semula, pembersihan, pengekalan main semula (replay retention), metrik kelambatan (lag metrics), dan ujian suntikan kegagalan. Menamakan corak tanpa sempadan ini adalah tidak lengkap.

Soalan untuk Dijelaskan Sebelum Menjawab

  • Apakah jaminan yang diperlukan? Adakah penerbitan at-least-once dengan kesan perniagaan exactly-once

mencukupi, atau adakah pengesahan segerak (synchronous confirmation) diperlukan sebelum bertindak balas kepada pemanggil?

  • Mutasi dan acara mana yang saling berkaitan? Satu mutasi pesanan mungkin mencipta satu acara, atau satu

transaksi mungkin mencipta beberapa acara yang memerlukan nombor jujukan per-pesanan berturutan.

  • Apakah susunan yang diperlukan? Reka bentuk ini mengandaikan susunan per-pesanan, bukan satu susunan keseluruhan global

merentasi semua pesanan.

  • Apakah yang boleh dijamin oleh broker? Tanya tentang perakuan (acknowledgements), penghantaran semula, susunan pemecahan,

pengekalan (retention), dan keidempotenan pengeluar. Tiada satu pun daripadanya yang menghapuskan jurang serahan pangkalan data ke broker dengan sendirinya.

  • Berapa cepatkah sesuatu acara mesti muncul? Sasaran kependaman mempengaruhi selang pengundian (polling interval), beban pangkalan data,

dan sama ada change data capture adalah wajar.

  • Apakah yang dilakukan oleh pengguna? Kemas kini pangkalan data tempatan boleh berkongsi transaksi dengan baris inbox;

pembayaran atau e-mel luaran memerlukan kunci keidempotenan hiliran atau serahan tahan lasak yang lain.

  • Berapa lamakah main semula mesti kekal boleh dilakukan? Pembersihan rekod outbox dan penyahduplikasian pengguna mesti

mengekalkan ufuk percubaan semula dan main semula yang diperlukan.

  • Bolehkah kedua-dua sumber menyertai two-phase commit? Gesaan menyatakan tidak. Jika sistem sebenar benar-benar

memerlukan keatomikan rentas sumber secara segerak dan kedua-dua sumber menyokongnya, kos ketersediaan dan gandingannya masih perlu dinilai dan bukannya diisytiharkan sebagai mustahil secara sejagat.

Rangka Kerja Jawapan 30 Saat

"Saya akan menulis pesanan dan acara outbox yang tidak boleh diubah (immutable) dalam transaksi PostgreSQL yang sama. Suatu geganti berasingan menuntut baris outbox yang dikomit, menerbitkannya, dan menandakannya sebagai diterbitkan hanya selepas perakuan broker. Jika geganti mati sebelum terbit, baris itu kekal tergantung (pending); jika ia mati selepas broker menerima tetapi sebelum tanda diletakkan, ia menerbitkan semula, jadi penghantaran adalah sekurang-kurangnya sekali (at least once). Setiap acara mempunyai ID yang stabil, dan pengguna memasukkan ID tersebut ke dalam jadual penyahduplikasian dalam transaksi yang sama seperti kemas kini perniagaannya. Saya akan memperuntukkan jujukan per-pesanan, menggunakan ID pesanan sebagai kunci pemecahan broker, menghalang acara kemudian daripada memintas acara tergantung yang lebih awal, dan memantau usia tergantung tertua. Kemudian saya akan menyuntik kerosakan pada setiap sempadan komit, terbit, perakuan, dan pengguna untuk mengesahkan invarian tersebut."

Perbincangan Terperinci Langkah demi Langkah

Mulakan dengan membuktikan mengapa susunan panggilan yang ketara itu gagal. Dalam aliran utamakan pangkalan data (database-first flow), pangkalan data boleh dikomit pada masa T1 dan proses boleh terhenti sebelum broker menerima pada T2; pesanan wujud tetapi tiada acara yang wujud. Mencuba semula permintaan HTTP bukanlah pembaikan yang lengkap kerana klien mungkin tidak mencuba semula, dan percubaan semula boleh menduplikasi pesanan melainkan arahan itu sendiri adalah idempoten. Dalam aliran utamakan broker (broker-first flow), pengguna boleh memerhatikan acara sebelum transaksi pesanan gagal. Menyongsangkan panggilan hanya menyongsangkan ketidakkonsistenan.

Pindahkan niat tahan lasak itu ke dalam satu sempadan atomik yang dimiliki oleh perkhidmatan. Dalam satu transaksi PostgreSQL tunggal, sahkan arahan, mutasikan pesanan, peruntukkan jujukan seterusnya untuk pesanan tersebut, dan masukkan baris outbox yang tidak boleh diubah. Sama ada kedua-dua baris dikomit atau kedua-duanya tidak. Skema perwakilan adalah seperti berikut:

sql
CREATE TABLE outbox_events (
  event_id uuid PRIMARY KEY,
  aggregate_type text NOT NULL,
  aggregate_id text NOT NULL,
  aggregate_sequence bigint NOT NULL,
  event_type text NOT NULL,
  schema_version integer NOT NULL,
  payload jsonb NOT NULL,
  occurred_at timestamptz NOT NULL DEFAULT now(),
  available_at timestamptz NOT NULL DEFAULT now(),
  claimed_by text,
  claim_until timestamptz,
  published_at timestamptz,
  attempt_count integer NOT NULL DEFAULT 0,
  last_error text,
  UNIQUE (aggregate_type, aggregate_id, aggregate_sequence)
);

CREATE INDEX outbox_dispatch_idx
ON outbox_events (available_at, occurred_at)
WHERE published_at IS NULL;

event_id kekal stabil melalui setiap percubaan semula. schema_version menjadikan evolusi muatan jelas. Jujukan agregat yang unik menghalang dua acara daripada menduduki kedudukan logik yang sama. Jujukan mesti diperuntukkan di bawah peraturan transaksi dan penguncian yang sama seperti agregat; cap masa atau susunan pemprosesan geganti bukanlah pengganti yang selamat. Jika satu transaksi mengeluarkan beberapa acara, peruntukkan nilai jujukan berturutan mengikut susunan yang dimaksudkan.

Geganti pengundian (polling relay) harus menuntut kelompok kecil dalam transaksi yang pendek. Ia boleh memilih baris dengan FOR UPDATE SKIP LOCKED, mengemas kini claimed_by dan claim_until, dan kemudian melakukan komit; pajakan yang berterusan (persisted lease) menghalang pekerja lain daripada memproses baris yang sama secara sengaja selepas kunci baris dilepaskan. Ia harus menerbitkan di luar kunci pangkalan data yang dipegang lama dan menandakan baris sebagai diterbitkan hanya selepas broker memperakuinya. Tamat tempoh pajakan membolehkan pemulihan apabila pekerja mati. Pengunduran (backoff) dan available_at menghalang destinasi yang gagal daripada mencipta gelung percubaan semula yang ketat. Membiarkan transaksi pangkalan data terbuka merentasi penerbitan rangkaian meningkatkan perbalahan (contention) dan masih tidak mewujudkan keatomikan dengan broker.

Terdapat jurang perakuan yang tidak dapat dielakkan. Broker mungkin menerima acara E secara tahan lasak, selepas itu geganti boleh terhenti sebelum menetapkan published_at. Semasa pemulihan, E diterbitkan semula. Menandakannya sebelum penerbitan akan mewujudkan jurang kehilangan yang bertentangan. Oleh itu geganti mesti memilih bahagian yang selamat—kemungkinan pendua—dan pengguna mesti menyahduplikasi.

Bagi pengguna yang kesan perniagaannya berada dalam pangkalan data, simpan ID acara yang telah diproses dalam transaksi yang sama:

sql
CREATE TABLE processed_events (
  consumer_name text NOT NULL,
  event_id uuid NOT NULL,
  processed_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (consumer_name, event_id)
);

Pengguna memulakan transaksi dan menggunakan INSERT ... ON CONFLICT DO NOTHING RETURNING untuk (consumer_name, event_id). Ia menggunakan perubahan perniagaan hanya apabila sisipan mengembalikan baris, kemudian melakukan komit. Tiada baris dikembalikan bermakna acara telah pun digunakan, jadi pendua boleh diperakui tanpa mengulangi perubahan tersebut. Menulis baris penyahduplikasian dalam satu transaksi dan kesan perniagaan dalam transaksi lain hanya mencipta masalah dwi-tulis yang baharu. Rekod penyahduplikasian juga mesti dikekalkan sekurang-kurangnya selama mana acara lama boleh dimainkan semula.

Inbox tempatan ini tidak merangkumi kesan luaran bukan transaksi secara atomik. Bagi API pembayaran, hantar event_id sebagai kunci keidempotenan penyedia. Jika destinasi tidak mempunyai sokongan keidempotenan, perkenalkan arahan/outbox tahan lasak yang lain berserta penyesuaian (reconciliation), atau terima risiko pendua yang didokumenkan. Amaran yang sama berlaku untuk e-mel, webhook, dan panggilan tidak boleh balik (irreversible) yang lain.

Susunan harus sepadan dengan sempadan perniagaan. Tetapkan jujukan monotonik per-pesanan, jangan biarkan jujukan k + 1 memintas k yang belum diterbitkan, dan gunakan aggregate_id sebagai kunci pemecahan broker. Pertanyaan tuntutan boleh memilih hanya jujukan terendah yang belum diterbitkan bagi setiap agregat, atau pemilikan geganti boleh dipecahkan mengikut cincangan (hash) stabil bagi aggregate_id; mana-mana pilihan mesti memastikan satu laluan penerbitan tersusun bagi setiap agregat. Pengguna boleh menolak, menampan (buffer), atau menyesuaikan jurang jujukan mengikut domain. Memerlukan satu jujukan global akan mensirikan pesanan yang tidak berkaitan dan mengurangkan ketersediaan tanpa membantu invarian per-pesanan.

Pengundian ialah geganti mudah alih yang paling mudah dan menjadikan pemilikan kelihatan dalam pangkalan data aplikasi, tetapi selangnya menukar kependaman dengan beban pertanyaan. Indeks tergantung, kelompok kecil, pajakan, dan pembersihan terikat adalah penting apabila volum bertambah. Change data capture boleh mengekor (tail) log pangkalan data dan menghalakan baris outbox yang dimasukkan dengan tekanan pengundian yang lebih rendah dan selalunya kependaman yang lebih rendah. Ia menambah ofset penyambung, pengekalan log pangkalan data, penggunaan (deployment), dan pemulihan ke sempadan operasi. Menangkap perubahan jadual perniagaan sewenang-wenangnya juga mendedahkan mutasi storan dan bukannya acara domain yang disengajakan; outbox yang eksplisit memastikan kontrak kekal stabil.

Operasi melengkapkan reka bentuk. Pantau kiraan baris tergantung, usia baris belum diterbitkan yang tertua, daya pemprosesan dan kegagalan penghantaran, kiraan percubaan, pajakan tamat tempoh, kependaman perakuan broker, kiraan penyahduplikasian pengguna, acara yang dikuarantin, dan pertumbuhan jadual. Arkibkan atau padamkan baris yang diterbitkan dalam kelompok terikat hanya selepas ufuk main semula dan audit. Acara racun memerlukan dasar yang disengajakan: cuba semula, kuarantin, atau baiki. Melangkaunya mungkin melanggar susunan per-pesanan, jadi acara kemudian untuk agregat tersebut tidak boleh diteruskan secara senyap.

Pengesahan harus menyasarkan sempadan, bukan hanya laluan lancar (happy path). Suntik kegagalan sebelum komit pangkalan data, selepas komit tetapi sebelum respons, semasa tuntutan geganti, sebelum terbit, selepas penerimaan broker tetapi sebelum published_at, selepas komit perniagaan pengguna tetapi sebelum perakuan, dan semasa pembersihan. Ujian harus mewujudkan empat invarian:

  1. setiap mutasi perniagaan yang dikomit mempunyai tepat satu niat outbox yang tahan lasak;
  2. setiap mutasi yang diundur balik tidak mempunyai niat outbox;
  3. setiap niat tahan lasak akhirnya diterbitkan sekurang-kurangnya sekali selepas pemulihan; dan
  4. penghantaran pendua menggunakan kesan perniagaan yang boleh dilihat oleh pengguna sekali sahaja.

Hentikan juga geganti cukup lama untuk membina tunggakan (backlog), mulakan semula, dan sahkan pemulihan kelambatan, jujukan per-pesanan, beban pangkalan data terikat, dan tingkah laku amaran. Lakukan ujian ke atas acara racun, evolusi versi muatan, main semula acara lama, dan pembersihan di sekitar sempadan pengekalan.

Contoh Jawapan Berkualiti Tinggi

"Pangkalan data dan broker tidak berkongsi komit atomik, jadi saya akan terlebih dahulu menjadikan niat acara sebahagian daripada transaksi pangkalan data. Baris pesanan dan baris outbox yang tidak boleh diubah dikomit bersama. Jika transaksi diundur balik, kedua-duanya tidak wujud. Jika ia dikomit dan proses mati serta-merta, proses lain masih boleh melihat baris outbox tersebut.

Geganti menuntut baris tergantung dengan transaksi pangkalan data yang pendek dan pajakan yang mempunyai tempoh luput, menerbitkannya, dan menetapkan published_at hanya selepas perakuan broker. Saya tidak akan memegang kunci pangkalan data semasa menunggu rangkaian. Masih terdapat tetingkap kerosakan selepas broker menerima dan sebelum kemas kini status, jadi geganti boleh menerbitkan pendua. Itu adalah kecenderungan kegagalan yang betul: pendua boleh dipulihkan, manakala acara yang hilang tidak boleh.

Setiap acara mempunyai UUID yang stabil. Pengguna pangkalan data memasukkan UUID tersebut ke dalam jadual yang berkuncikan nama pengguna dalam transaksi yang sama seperti kemas kini perniagaannya. Pendua akan berkonflik dan menjadi no-op. Jika pengguna memanggil penyedia pembayaran atau e-mel, ia mesti menghantar UUID acara sebagai kunci keidempotenan atau menggunakan serahan tahan lasak yang lain, kerana transaksi penyahduplikasian tempatan tidak boleh merangkumi kesan jarak jauh tersebut.

Bagi susunan, saya memperuntukkan jujukan di bawah transaksi pesanan, menerbitkan dengan ID pesanan sebagai kunci pemecahan, dan menyekat jujukan kemudian daripada memintas acara tergantung yang lebih awal untuk pesanan tersebut. Saya tidak mengenakan susunan global. Saya akan bermula dengan pengundian melainkan sasaran kependaman dan beban mewajarkan CDC, kemudian memantau usia tergantung tertua, percubaan semula, pajakan tamat tempoh, kadar pendua, acara racun, dan pertumbuhan jadual.

Akhir sekali, saya akan mematikan proses pada setiap sempadan. Hasil yang diperlukan ialah: undur balik tidak menghasilkan sebarang niat, komit sentiasa meninggalkan niat, pemulihan menerbitkan setiap niat sekurang-kurangnya sekali, dan penghantaran pendua mengubah keadaan pengguna sekali sahaja. Outbox menyelesaikan serahan yang boleh dipercayai; keidempotenan permintaan, keidempotenan pengguna, evolusi skema, dan penyesuaian kekal sebagai bahagian eksplisit dalam sistem."

Kesilapan Biasa

  • Memanggil pangkalan data dan broker secara berurutan → salah satu panggilan boleh berjaya secara bersendirian → **Komit

mutasi perniagaan dan niat acara dalam satu transaksi pangkalan data tempatan.**

  • Memanggil penerbit dalam memori selepas komit → kerosakan akan menghilangkan panggilan balik dan keadaannya →

Kekalkan (persist) niat sebelum kembali.

  • Mendakwa outbox memberikan penghantaran exactly-once → jurang broker-diterima/status-tidak-direkodkan

mewujudkan pendua → Nyatakan penerbitan at-least-once dan reka bentuk kesan perniagaan exactly-once.

  • Menandakan baris diterbitkan sebelum perakuan broker → kerosakan boleh menghilangkan acara secara

kekal → Rekod kejayaan hanya selepas perakuan dan bertolak ansur dengan penerbitan semula.

  • Menulis keadaan penyahduplikasian secara berasingan daripada kesan pengguna → pengguna mencipta semula

jurang dwi-tulis yang sama → Letakkan kedua-duanya dalam satu transaksi tempatan.

  • Menganggap inbox tempatan sebagai perlindungan untuk caj jarak jauh → kesan jarak jauh tidak boleh menyertai

transaksi → Gunakan kunci keidempotenan hiliran, serahan tahan lasak, dan penyesuaian.

  • Menggunakan cap masa sebagai susunan → tingkah laku jam dan keserentakan tidak memperuntukkan kedudukan

kausal yang unik → Peruntukkan jujukan per-agregat transaksi dan gunakan agregat sebagai kunci pemecahan.

  • Menjalankan banyak pengundi tanpa tuntutan atau pajakan → pekerja bersaing secara sengaja pada baris yang sama →

Gunakan tuntutan pendek, tamat tempoh, kelompok kecil, dan indeks baris tergantung.

  • Memadamkan baris diterbitkan dan baris penyahduplikasian serta-merta → percubaan semula yang tertangguh dan main semula boleh mengulangi

kesan lama → Tetapkan pembersihan daripada ufuk main semula dan audit yang didokumenkan.

  • Menguji hanya penerbitan yang berjaya → jaminan reka bentuk berada dalam tetingkap kerosakan → **Suntik

kegagalan sebelum dan selepas setiap sempadan tahan lasak dan tegaskan invarian.**

Soalan Susulan dan Respons

Susulan 1: Bagaimana jika destinasinya ialah API luaran dan bukannya broker?

Transaksi sumber masih boleh menulis arahan outbox. Seorang pekerja memanggil API dengan event_id sebagai kunci keidempotenan dan merekodkan respons. Masa tamat (timeout) adalah samar-samar—perkhidmatan jarak jauh mungkin telah menyelesaikan panggilan tersebut—jadi cuba semula hanya dengan kunci yang sama. Jika API tidak menyediakan keidempotenan mahupun status operasi yang boleh ditanya, kesan exactly-once tidak dapat dijamin; tambah penyesuaian atau dedahkan risiko pendua dalam kontrak perniagaan.

Susulan 2: Bagaimana jika aliran kerja merangkumi beberapa perkhidmatan dan pangkalan data?

Outbox menerbitkan peralihan keadaan tempatan setiap perkhidmatan secara boleh dipercayai; ia tidak mengkomit keseluruhan aliran kerja berbilang perkhidmatan secara atomik. Modelkan aliran kerja sebagai saga dengan langkah ke hadapan yang eksplisit, keidempotenan, keadaan yang dikekalkan, dan tindakan pampasan (compensating actions). Setiap langkah saga boleh menggunakan transaksi tempatan tersendiri berserta outbox. Tentukan apa yang berlaku apabila pampasan juga gagal dan bukannya menerangkannya sebagai pengunduran balik semua pangkalan data.

Susulan 3: Bagaimana jika penemu duga memerlukan susunan acara global yang ketat?

Jelaskan mengapa agregat bebas memerlukan satu susunan dan apakah daya pemprosesan atau ketersediaan yang mungkin ditukar ganti untuknya. Pengatur jujukan (sequencer) tunggal atau satu pecahan broker boleh mewujudkan susunan menyeluruh (total order), tetapi ia menjadi kesesakan pensirian (serialization) dan kegagalan. Kebanyakan aliran kerja pesanan memerlukan susunan kausal hanya dalam satu pesanan, yang mana ia boleh disediakan dengan lebih murah oleh jujukan agregat transaksi dan kunci pemecahan agregat.

Susulan 4: Bagaimanakah reka bentuk pulih daripada gangguan penyambung CDC?

Baris outbox yang dikomit kekal sebagai punca kebenaran (source of truth). Berikan amaran tentang kelambatan penyambung dan ruang pengekalan log pangkalan data, kekalkan ofset penyambung secara tahan lasak, dan uji permulaan semula daripada ofset terakhir yang diperakui. Pangkalan data mesti mengekalkan log cukup lama untuk objektif gangguan; jika tidak, pasport (snapshot) atau pengisian semula terkawal (controlled backfill) diperlukan. Penyahduplikasian pengguna menjadikan tindakan memainkan semula julat yang bertindih sebagai selamat.

Susulan 5: Bilakah anda patut mengelakkan transactional outbox?

Gunakan reka bentuk yang lebih mudah apabila acara tersebut secara eksplisit adalah usaha terbaik (best effort), seperti telemetri pakai buang, atau apabila sistem hiliran boleh mengundi punca kebenaran dengan selamat dan sasaran kependaman membenarkannya. Jika kedua-dua sumber benar-benar menyokong two-phase commit dan keatomikan segerak adalah wajib, nilaikan pilihan tersebut dengan kos gandingan dan ketersediaannya. Event sourcing ialah alternatif lain, tetapi ia mengubah model punca kebenaran dan tidak seharusnya diperkenalkan semata-mata untuk mengelakkan satu serahan.

Sumber awam

Soalan berkaitan