← Back to list

Race Condition: Para Transferinde Eş Zamanlılığın Yönetimi — K6 Testi

İlk bölümde para transferi domaininde race condition durumunu nasıl yöneteceğimizi anlatmıştım. Bu bölümde ise yapılan çözümü anlık olarak…

Onur Nafi Güzel · 2026-06-15 16:19 · 0 claps · 7.0 min read
#k6 #testing
Open on Medium ↗
Wiki topics: SOC · Sociology & Politics

Race Condition: Para Transferinde Eş Zamanlılığın Yönetimi — K6 Testi

İlk bölümde para transferi domaininde race condition durumunu nasıl yöneteceğimizi anlatmıştım. Bu bölümde ise yapılan çözümü anlık olarak aynı anda bir çok kullanıcı ile test edip sürecin düzgün çalışıp çalışmadığını nasıl test edebileceğimiz göstereceğim.

K6 nedir? (en sade haliyle)

K6, bir sunucuya aynı anda çok sayıda istek gönderip sonuçları kontrol eden bir test aracıdır.

Biz onu hız ölçmek için değil, şunu kanıtlamak için kullanıyoruz:

Aynı hesaptan tam aynı anda 40 kişi para çekmeye çalışırsa, sistem yanlış davranıp hesabı eksiye düşürür mü, yoksa fazlalıkları reddeder mi?

Normal bir test (tek tek istek atan) bunu yakalayamaz, çünkü bu tür hatalar ancak aynı anda çok istek gelince ortaya çıkar. K6 tam da bunu yapar: çok sayıda isteği paralel atar, sonra “para doğru mu kaldı?” diye bakar.

Sıfırdan bir test için hangi dosyalar gerekli?

Mutlak minimum sadece iki şeydir:

  1. K6 programını çalıştırmak için Docker imajı.
  2. İçinde test mantığını yazdığın bir test dosyası .js uzantılı.

Yani teorik olarak tek bir .js dosyası yeterli. Ben projemde diğer testlerde de aynı şeyleri yapmamak ve düzen sağlamak için bir dosya daha ekledim:

| Dosya                                                         | Zorunlu mu? | Ne işe yarar
|---------------------------------------------------------------|-------------|--------------------------------------------------------------
| **Test dosyası** (`k6/01-race-withdrawals.js`)                | Evet        | Testin kendisi: ne yapılacak, ne kontrol edilecek
| **Yardımcı dosya** (`k6/lib/ledger.js`)                       | Hayır       | Tekrar eden işleri (hesap aç, para yatır...) tek yerde toplar
| **Docker ayarı** (`docker-compose.yml` içindeki `k6` servisi) | Hayır       | K6'yı kurmadan, tek komutla çalıştırmak için

Aşağıda önce test dosyasını, sonra yardımcı dosyayı satır satır açıklayacağım.

  • options: Testin ayarları. “Kaç sanal kullanıcı, her biri kaç kez istek atsın?”
  • setup(): Test başlamadan bir kez çalışır. Hazırlık burada yapılır (hesabı aç, parayı yatır). Buradan döndürdüğün veri, diğer aşamalara data diye geçer.
  • default(): Asıl test. Her sanal kullanıcı bu fonksiyonu çalıştırır. İşte “aynı anda çok istek” burada olur.
  • teardown(): Test bitince bir kez çalışır. “Sonuç doğru mu?” kontrolünü buraya koyarız.

1. Test dosyası : 01-race-withdrawals.js

Bu test şunu yapar: bir hesaba sadece 25 çekime yetecek para koyar, sonra 40 çekimi aynı anda atar. Doğru bir sistemde tam 25 tanesi başarılı olmalı, gerisi reddedilmeli, hesap eksiye düşmemeli.

Bölüm 1 Gerekli araçlar

import http from 'k6/http';
import { check } from 'k6';
import { BASE, headers, createAccount, deposit, balanceOf, sumEntries } from './lib/ledger.js';
  • Satır 1: http istek atma aracı (http.post, http.get). K6'nın içinde bulunur.
  • Satır 2: check "şu doğru mu?" kontrolü yapan araç. Yine K6'dan gelir.
  • Satır 3: Kendi yardımcı dosyamızdan (lib/ledger.js) hazır fonksiyonları alıyoruz: hesap aç, para yatır, bakiye oku, defteri topla.

Bölüm 2 Ayarlanabilir değerler

const STRATEGY = __ENV.STRATEGY || 'conditional';
const N = parseInt(__ENV.N || '40', 10);
const M = parseInt(__ENV.M || '25', 10);
const UNIT = 100;
const FUNDED = UNIT * M;
  • __ENV.STRATEGY: Dışarıdan verilen ortam değişkenini okur.
  • N = 40 : Aynı anda para çekim sayısı.
  • M = 25 : Bakiyenin karşılayabileceği çekim sayısı.
  • UNIT = 100 : Her çekimin tutarı (kuruş cinsinden).
  • FUNDED = UNIT * M : Başlangıç bakiyesi = 100 × 25 = 2500. Yani tam 25 çekime yeter. 40 çekim atıyoruz; ilk 25'i başarılı olmalı, kalan 15'i yetersiz bakiye kalıp reddedilmeli.
  • Not: “başarılı olan” çekimler illa ilk gönderilen 25 olmak zorunda değil eş zamanlı oldukları için hangi 25'inin kazandığı belirsizdir; önemli olan tam 25 tanesinin geçip 15'inin başarısız olmasıdır.

Bölüm 3 Ayarlar: Kaç kişi, kaç kez?

export const options = {
  scenarios: {
    storm: { executor: 'per-vu-iterations', vus: N, iterations: 1, maxDuration: '60s' },
  },
  thresholds: STRATEGY === 'naive' ? {} : { checks: ['rate==1.0'] },
};
  • scenarios : Testin nasıl koşacağını anlatan bölüm. İçine istediğin adı verebilirsin; ben storm dedim.
  • executor: 'per-vu-iterations' : "Her sanal kullanıcı belirli sayıda tur atsın" modu.
  • vus: N : Kaç sanal kullanıcı. vus = "virtual users".
  • iterations: 1 : Her kullanıcı 1 kez istek atsın. 40 kullanıcı × 1 = aynı anda 40 istek. Yarışı yaratan şey budur.
  • maxDuration: '60s' : Test en fazla 60 saniye sürsün.
  • thresholds : Testin geçti/kaldı kriteri. checks: ['rate==1.0'] = "tüm kontroller %100 geçmeli, yoksa test BAŞARISIZ".
  • STRATEGY === 'naive' ? {} : {...} kısmı şu demek: eğer strateji 'naive' ise kriter koyma, değilse kriteri uygula.

Bölüm 4 setup(): Hazırlık

export function setup() {
  const acc = createAccount('USD', false, STRATEGY);
  deposit(acc, FUNDED, STRATEGY);
  console.log(`[${STRATEGY}] account ${acc} funded ${FUNDED}; firing ${N} withdrawals of ${UNIT}`);
  return { acc };
}
  • createAccount('USD', false, STRATEGY) : Yeni bir hesap açar. 'USD' para birimi, false = eksiye düşmesine izin verme. Geri dönen hesap kimliğini acc'a koyar.
  • deposit(acc, FUNDED, STRATEGY) : Bu hesaba FUNDEDpara yatırır.
  • console.log(...) : Ekrana bilgi yazar (sadece takip için, teste etkisi yok). Tırnaklar ters tırnak (``); içine${değişken}` yazınca değer gömülür.
  • return { acc } : Hesabı döndürür. Bu sayede default ve teardown aşamaları bu hesaba data.acc ile ulaşır. Bu satır kritik: setup'ta üretileni sonraki aşamalara taşımak için kullanıyorum.

Bölüm 5 default(): asıl test

export default function (data) {
  http.post(
    `${BASE}/withdrawals`,
    JSON.stringify({ account: data.acc, amount: UNIT, reason: 'race' }),
    { headers: headers(STRATEGY) },
  );
}
  • function (data)data, setup'ın döndürdüğü { acc } nesnesidir. Yani data.acc = hesabımız.
  • http.post(adres, gövde, ayarlar) : bir POST isteği atar. Üç parçası var:
  • ${BASE}/withdrawals : İstek adresi. BASE sunucu adresi; sonuna /withdrawals eklenir.
  • JSON.stringify({...}) : Gövde. JavaScript nesnesini, sunucunun anladığı JSON metnine çevirir.
  • { headers: headers(STRATEGY) } : İstek başlıkları (içerik tipi + zorunlu Idempotency-Key).
  • headers(...) fonksiyonu yardımcı dosyadan gelir.
  • Bu fonksiyon 40 sanal kullanıcının her biri tarafından aynı anda çalıştırılır → 40 eş zamanlı çekim. İşte yarış burada.

Bölüm 6 teardown(): sonucu kontrol et

export function teardown(data) {
  const apiBalance = balanceOf(data.acc);
  const ledgerSum  = sumEntries(data.acc);
  const successful = (FUNDED - ledgerSum) / UNIT;

  check(null, {
    [`[${STRATEGY}] self-audit tutar (bakiye == kayıtların toplamı)`]: () => apiBalance === ledgerSum,
    [`[${STRATEGY}] asla negatif değil`]:                              () => ledgerSum >= 0 && apiBalance >= 0,
    [`[${STRATEGY}] fazla harcama yok (başarılı <= M)`]:               () => successful <= M,
    [`[${STRATEGY}] tam olarak M başarılı`]:                           () => successful === M,
  });
}
  • balanceOf(data.acc) : Sunucunun gösterdiği bakiyeyi okur.
  • sumEntries(data.acc) : Hesabın tüm hareket kayıtlarını tek tek toplar. Bu, bakiyenin bağımsız doğrulamasıdır
  • successful = (FUNDED - ledgerSum) / UNIT : Kaç çekimin başardığını hesaplar. Başlangıç 2500, kalan ledgerSum; aradaki fark çekilen paradır, UNIT'e bölünce adet çıkar.
  • check(null, { ... }) : Kontrolleri yapar. İlk argüman null çünkü bir HTTP yanıtını değil, kendi hesapladığımız değerleri kontrol ediyoruz. Süslü parantez içindeki her satır bir kontrol:
  • Anahtar (köşeli parantez [...] içindeki metin) = kontrolün adı. Köşeli parantez, metni dinamik (${STRATEGY} gömülü) yapmak için.
  • Değer (() => koşul) = doğru/yanlış döndüren küçük fonksiyon. () => "şunu hesapla" demenin kısa yolu.
  • Dört kontrol şunu doğrular: (1) bakiye, kayıtların toplamıyla uyuşuyor; (2) hiçbir değer negatif değil; (3) fazla harcama yok; (4) tam 25 çekim başarılı oldu. Hepsi geçerse test yeşil.

2. Yardımcı dosya lib/ledger.js

Test dosyası birçok işi (hesap aç, para yatır, bakiye oku) bu dosyadaki fonksiyonlara referans ediyor. Böylece test dosyası kısa ve okunur kalıyor. İşte bizim test dsoyasının kullandığı parçalar:

Sunucu adresi ve istek başlıkları

import http from 'k6/http';
import { check } from 'k6';

export const BASE = __ENV.BASE_URL || 'http://localhost:8080';
  • BASE isteklerin gideceği sunucu adresi. Önce __ENV.BASE_URL bakılır, yoksa yerel adres (localhost:8080) kullanılır. export = "bu değeri başka dosyalar da kullanabilsin".
let _keySeq = 0;
export function newKey() {
  const vu = typeof __VU !== 'undefined' ? __VU : 0;
  const it = typeof __ITER !== 'undefined' ? __ITER : 0;
  return `k6-${vu}-${it}-${Date.now()}-${_keySeq++}-${Math.random().toString(16).slice(2)}`;
}
  • newKey() : Her isteğe benzersiz bir kimlik (Idempotency-Key) üretir. Bu kimlik, "aynı istek iki kez gitmesin" güvenliği içindir; her çekim ayrı bir işlem sayılsın diye her seferinde farklı olur.
  • __VU : O anki sanal kullanıcının numarası.
  • __ITER kaçıncı turda olduğu.
  • typeof ... !== 'undefined' kontrolü, bu değerlerin tanımsız olduğu aşamalarda hata vermesin diye konmuş güvenliktir.
  • Date.now() + artan sayaç + rastgele sayı birleşip benzersizliği garanti eder.
export function headers(strategy, idemKey) {
  const h = { 'Content-Type': 'application/json', 'Idempotency-Key': idemKey || newKey() };
  if (strategy) h['X-Concurrency-Strategy'] = strategy;
  return h;
}
  • headers(...) : Her isteğin başlıklarını hazırlar.
  • 'Idempotency-Key': idemKey || newKey() : Dışarıdan kimlik verildiyse onu, yoksa yeni bir tane kullanır.
  • if (strategy) ... : Eğer bir strateji belirtildiyse, onu özel bir başlıkla ekler (sunucuya "şu kilitleme yöntemini kullan" der; sadece test ortamında geçerli).

Hesap açma, para yatırma, bakiye okuma

export function createAccount(currency, allowsNegative, strategy) {
  const body = JSON.stringify({
    ownerRef: `k6-${__VU}-${Date.now()}-${Math.random()}`,
    currency,
    allowsNegative: !!allowsNegative,
  });
  const res = http.post(`${BASE}/accounts`, body, { headers: headers(strategy) });
  check(res, { 'account created (201)': (r) => r.status === 201 });
  return res.json('id');
}
  • Yeni hesap açmak için /accounts adresine POST atar.
  • ownerRef : Hesabın sahibi için benzersiz bir etiket.
  • !!allowsNegative : Değeri kesin true/false'a çevirir.
  • check(res, {...}) : Sunucu 201 döndü mü diye bakar.
  • return res.json('id') : Yanıttaki hesap kimliğini döndürür.
export function deposit(accountId, amount, strategy) {
  const res = http.post(
    `${BASE}/deposits`,
    JSON.stringify({ account: accountId, amount, reason: 'k6 seed' }),
    { headers: headers(strategy) },
  );
  check(res, { 'deposit ok (201)': (r) => r.status === 201 });
  return res;
}

export function balanceOf(accountId) {
  return http.get(`${BASE}/accounts/${accountId}/balance`).json('balance');
}
  • deposit(...)/deposits adresine POST atıp hesaba para yatırır; 201 bekler.
  • balanceOf(...)/accounts/{id}/balance adresine GET atıp bakiye değerini döndürür.

Defteri toplama (amount değeri bir tabloda kayıt olarak duruyor)

export function sumEntries(accountId) {
  let sum = 0;
  let cursor = null;
  do {
    const url = `${BASE}/accounts/${accountId}/entries?size=200` + (cursor ? `&cursor=${cursor}` : '');
    const body = http.get(url).json();
    for (const e of body.entries) sum += e.amount;
    cursor = body.nextCursor;
  } while (cursor);
  return sum;
}
  • sumEntries(...) : Hesabın tüm hareket kayıtlarını gezip toplar. Neden? Çünkü "bakiye doğru mu?" sorusunu, bakiyeden bağımsız bir kaynakla (kayıtların kendisiyle) doğrulamak için.
  • Kayıtlar sayfa sayfa gelir.
  • for (const e of body.entries) sum += e.amount : Her kaydın tutarını toplama ekler.

3. Testi çalıştırma

docker-compose.yml içinde hazır bir k6 servisi var; scriptleri /scripts klasörüne bağlar ve sunucu adresini ayarlar. Önce uygulamayı ayağa kaldır, sonra testi çalıştır:

  k6:
    image: grafana/k6:latest
    profiles: [test]
    environment:
      BASE_URL: "http://api:8080"
    volumes:
      - ./k6:/scripts
    depends_on:
      - api
# Uygulamayı başlat (sunucu + veritabanı)
docker compose up --build -d

# Testi çalıştır
docker compose run --rm k6 run /scripts/01-race-withdrawals.js

4. Çıktıyı okuma

çıktı

çıktı

  • geçen, kalan kontrol.
  • checks: 100.00%tüm kontroller geçti. options'taki thresholds kuralı gereği, %100 olmazsa K6 hata koduyla kapanır (otomatik testlerde bu "test kaldı" demektir).

Özet

  • Sıfırdan bir K6 testi için tek gereken: K6 programı + bir .js dosyası. Bu proje, düzen için ayrıca bir yardımcı dosya ve Docker ayarı kullanır.
  • Her test 4 aşamadır: options (ayar) → setup (hazırlık) → default (asıl yük) → teardown (kontrol).
  • 01-race-withdrawals.js, bir hesaba 40 çekimi aynı anda atıp tam 25'inin başarılı olduğunu, bakiyenin eksiye düşmediğini ve kayıtlarla uyuştuğunu kontrollerle kanıtlar.

메타데이터
post_id
48c4f41016a5
slug
race-condition-para-transferinde-eş-zamanlılığın-yönetimi-k6-testi-48c4f41016a5
url
https://medium.com/@ongguzel/race-condition-para-transferinde-e%C5%9F-zamanl%C4%B1l%C4%B1%C4%9F%C4%B1n-y%C3%B6netimi-k6-testi-48c4f41016a5
canonical_url
https://medium.com/@ongguzel/race-condition-para-transferinde-e%C5%9F-zamanl%C4%B1l%C4%B1%C4%9F%C4%B1n-y%C3%B6netimi-k6-testi-48c4f41016a5
author_url
https://medium.com/@ongguzel
status
ok
fetched_at
2026-06-24 11:06:28