Idempotency ve eşzamanlılık
Bu sayfa, ağ koptuğunda bir isteği iki kez uygulamadan nasıl tekrar edeceğinizi (idempotency, yani tekrar güvenliği) ve iki istemcinin aynı kaynağı aynı anda değiştirmesini iyimser sürüm denetimiyle (optimistic locking) nasıl yöneteceğinizi anlatır. Para, eşya ya da ödül veren her çağrıda bu iki kuralı kullanın.
Idempotency-Key
Değiştiren bir istek (POST, bazı PUT/PATCH/DELETE) Idempotency-Key başlığı taşıyabilir ya da taşımak zorundadır. Anahtarın kuralları:
| Kural | Değer |
|---|---|
| Uzunluk | 1 ile 200 karakter (boş olamaz) |
| Karakter kümesi | Boşluksuz yazdırılabilir ASCII (SDK yalnız bunu üretir ve kabul eder) |
| Önerilen değer | Rastgele UUID (v4); bazı uçlar UUID biçimini zorunlu kılar (klan, grup ve turnuva uçları: geçerli UUID olmalıdır) |
| Kapsam | Çekirdek komutlarda anahtar ortam genelinde benzersizdir: aynı ortamda iki farklı çağıran aynı anahtarı kullanırsa çakışır. Eklenti komutlarında anahtar eklenti ve çağıran (oyuncu ya da sunucu) başınadır. Bu yüzden her zaman rastgele UUID kullanın, oyuncu-1 gibi tahmin edilebilir değerler kullanmayın |
| Kayıt süresi | 24 saat (komut kayıtları); eklenti komutlarında işlemin receiptRetentionSeconds değeri (şimdilik 86 400 sn) |
Hangi uçta anahtarın zorunlu, hangisinde isteğe bağlı olduğunu uç nokta referansı gösterir. Örnek: karakter komutlarında isteğe bağlıdır, cüzdan aktarımında, envanter ve eşya işlemlerinde, ticarette, mağaza ve eklenti komutlarında zorunludur.
Anahtar nasıl çalışır
- İlk istek anahtarla gelir; sunucu işlemi uygular ve sonucu anahtara bağlı saklar.
- Aynı anahtar + aynı içerik tekrar gelirse işlem yeniden uygulanmaz; kayıtlı sonuç döner. Bazı yanıtlar bunu
replayed: truealanıyla bildirir (karakter komutları, mağaza satın alma). - Aynı anahtar + farklı içerik
422veidempotency mismatch(eklentideidempotency_mismatch) alır. - İlk istek hâlâ işleniyorsa ikincisi
409(idempotent request in progressya daidempotency_in_flight) alır; kısa süre sonra aynı anahtarla tekrar deneyin. - Kayıt süresi dolduysa
410döner. - Çekirdek komutlarda anahtar yalnız başarıda “yanar”: yetki reddi (ör. istemci anahtarıyla sunucu komutu), doğrulama hatası ya da geri alınan işlem anahtarı tüketmez; düzeltip aynı anahtarla tekrar gönderebilirsiniz.
Uyarı
Belirsiz sonuçlu bir isteği yeni anahtarla tekrarlamayın. Yeni anahtar yeni işlem demektir ve çift ödül, çift harcama yaratır. Anahtarı bir kez üretin, saklayın (istek ve yeniden denemeler boyunca aynı kalsın) ve gerekirse sonucu sorgulayın.
Ne zaman yeni anahtar üretilir
- Kullanıcı aynı işlemi bilerek yeniden başlattığında (ör. ikinci mağaza satın alması).
- İş kuralı reddinden (
422) sonra durumu değiştirip yeniden denediğinizde. 410(anahtar süresi doldu) sonrasında, durumu okuyup işlemin gerçekten uygulanmadığından emin olduktan sonra.
Örnek: güvenli yeniden deneme
Aşağıdaki yardımcı anahtarı bir kez üretir, ağ hatalarında ve 408, 429, 5xx yanıtlarında aynı anahtarla yeniden dener ve Retry-After süresini bekler:
/** `Retry-After` değerini milisaniyeye çevirir; saniye ya da HTTP tarihi olabilir, bilinmiyorsa `null`. */
function retryAfterMs(header: string | null): number | null {
if (header === null) return null;
if (/^\d+$/.test(header)) return Number(header) * 1000;
const date = Date.parse(header);
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
}
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
/**
* Bir değiştirici isteği AYNI `Idempotency-Key` ile güvenle yeniden dener.
* Anahtarı bir kez üretin ve her denemede aynen gönderin; yeni anahtar yeni işlem demektir.
* 429 ve 503'te sunucunun `Retry-After` süresini bekler; yoksa üstel geri çekilme kullanır.
*/
export async function postWithRetry(url: string, body: unknown, headers: Record<string, string>, maxAttempts = 4): Promise<Response> {
const idempotencyKey = crypto.randomUUID();
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let response: Response | undefined;
try {
response = await fetch(url, {
method: "POST",
credentials: "include",
headers: { ...headers, "content-type": "application/json", "idempotency-key": idempotencyKey },
body: JSON.stringify(body),
});
} catch (error) {
lastError = error; // ağ hatası: istek uygulanmış olabilir, aynı anahtarla tekrar güvenlidir
}
if (response !== undefined) {
if (!RETRYABLE_STATUS.has(response.status)) return response; // başarı ya da kalıcı hata
lastError = new Error(`HTTP ${response.status}`);
if (attempt === maxAttempts) return response;
const wait = retryAfterMs(response.headers.get("retry-after")) ?? Math.min(200 * 2 ** (attempt - 1), 5000);
await new Promise((resolve) => setTimeout(resolve, wait));
continue;
}
if (attempt < maxAttempts) await new Promise((resolve) => setTimeout(resolve, Math.min(200 * 2 ** (attempt - 1), 5000)));
}
throw lastError;
}
TypeScript SDK’sı bunu sizin yerinize yapar: CrxClient (sunucu anahtarı) GET isteklerini ve Idempotency-Key taşıyan istekleri 408, 429, 500, 502, 503, 504 durumlarında (varsayılan 2 deneme, maxRetries ile 0 ile 5 arası) üstel geri çekilmeyle ve Retry-After’a saygı göstererek yeniden dener. Eklenti komutlarında PlayerClient anahtarı hazırlama anında üretir ve tarayıcı günlüğüne (journal) yazar; sayfa yenilense bile aynı anahtarla devam eder (bağlamalar).
Sonucu sorgulama (eklenti komutları)
Eklenti komutu için yanıtı hiç alamadıysanız, tekrar göndermeden GET …/commands/{message_type}/result?idempotency_key=… ile sonucu sorgulayın. Yalnız 404 (kayıt yok) yanıtında aynı anahtarla yeniden gönderin (eklentileri çağırma). Çekirdek komutlarda ayrı bir sorgu ucu yoktur; aynı anahtar ve gövdeyle yeniden göndermek aynı kayıtlı sonucu döndürür.
İyimser sürüm denetimi
Sunucu, değiştirilebilir kaynaklara (karakter, envanter, eklenti kaynağı, mağaza ilanı) artan bir sürüm (version) tutar. İsteğinizde “şu sürümü görüp karar verdim” derseniz ve bu arada başkası kaynağı değiştirdiyse, istek uygulanmaz ve çakışma döner. Böylece iki istemci birbirinin değişikliğini sessizce ezmez.
| Kullanım yeri | Nasıl gönderilir |
|---|---|
Karakter komutları (POST /v1/characters/{id}/commands) | If-Match: 3 (tırnaklı "3" de kabul edilir) |
Envanter ve eşya işlemleri (If-Match destekleyen uçlar) | If-Match |
| Mağaza yönetim uçları (sayfa, ilan değişiklikleri) | If-Match (isteğe bağlı) |
| Eklenti komutları (HTTP) | If-Match: "12" |
Eklenti komutları (realtime Command çerçevesi) | expectedVersion: "12" (ondalık metin) |
Oyuncu satın alma (/v1/player/store/listings/{id}/buy) | If-Match kullanılmaz; ilan sürümü gövdedeki expected_version alanıdır (başlık gönderirseniz 400) |
If-Match biçimi: tam sayı sürüm numarası. * ve zayıf doğrulayıcılar (W/"3") reddedilir (400); negatif sayı ve sayısal olmayan değer 400 alır. Başlık verilmezse sürüm denetimi yapılmaz (son yazan kazanır); para ve eşya gibi kritik kaynaklarda göndermenizi öneririz.
Çakışma gelince
| Uç ailesi | Yanıt |
|---|---|
| Çekirdek REST | 409 ve version conflict: expected 3, actual 5 |
| Eklenti | 409 ve {"code":"conflict","message":"version conflict: expected 3, actual 5",…} |
| Mağaza oyuncu satın alma | 409 ve {"code":"listing_changed",…} |
| Realtime komutu | Error çerçevesi InvalidPayload (retryable: false); ayrıntı telden verilmez |
Çözüm döngüsü:
- Kaynağı yeniden okuyun (güncel
versionile). - Kullanıcının niyeti hâlâ geçerli mi karar verin (ör. hâlâ yeterli bakiye var mı).
- Yeni anahtarla (bu yeni bir deneme) güncel sürümle tekrar gönderin.
Not: sürüm çakışması iş kuralı değil, eşzamanlılık sonucudur; aynı isteği körlemesine tekrar göndermeniz yine çakışır. Idempotency anahtarı tekrar içindir, sürüm denetimi çakışma içindir; ikisini birlikte kullanabilirsiniz: aynı anahtar + aynı If-Match ile gönderilen tekrar kayıtlı sonucu döner.
Özet kural listesi
- Para, eşya, ödül ve komut gönderen her isteğe anahtar ekleyin; anahtarı bir kez üretin, yeniden denemelerde değiştirmeyin.
- Belirsiz sonucu sorgulayın ya da aynı anahtarla tekrarlayın; yeni anahtarla asla.
- Paylaşılan kaynak yazarken
If-Match/expectedVersiongönderin;409’da yeniden okuyun. 422iş kuralı reddidir; tekrar etmek sonuç değiştirmez.- Ayrıntılı hata tablosu için hata referansı.