# VC-082 · Çok adımlı iş yarıda kalıyor, tamamlanan adımlar telafi edilmiyor

Dosya oluşuyor ama ona bağlı kayıt yazılamıyor ve işlem hata vererek bitiyor. Tamamlanan adımın etkisi sistemde kalırken bunu geri alacak veya yeniden uzlaştıracak kalıcı bir iş kaydı bulunmuyor.

- Önem: YÜKSEK. Etkisi büyük. Trafik artınca ya da istek tekrarlanınca tetiklenir.
- Önem notu: Yük altında bir adımı tamamlanan, sonraki adımı bozulan ve kullanıcı işini tutarsız bırakan akış temel alınır. Yalnız geçici dosya artığı daha düşük etki taşır. Ödeme, hak veya stok gibi etkilerde telafi sözleşmesi ayrıca doğrulanmalıdır.
- Eksen ve kategori: Sağlamlık, 12 Hata yolları ve dış çağrılar
- Yığın: Node.js, Her yığın
- Yapay zekâ kodunda: ölçülmedi. Dayanak: uzman görüşü.
- Ne zaman bakılır: İlk yayından önce, Her ay
- CWE: CWE-459
- Checklist ifadesi: Çok adımlı bir iş yarıda kalınca tamamlanan adımlar geri alınıyor ya da uzlaştırılıyor.
- Son inceleme: 4 Ekim 2026, Komünite editörlüğü
- Adres: https://vibecheck.komunite.com.tr/madde/cok-adimli-islem-yarida-kalinca-telafi-edilmiyor

## 60 saniyelik kontrol

Yalnız kendi uygulamanda ya da yazılı izin aldığın sistemde dene. Bu bir sızma testi değildir.

1. Birden fazla sisteme yazan işin adımlarını ve kalıcı etkilerini çıkar.
2. Yerel örnekte ilk adımı tamamla, sonraki yazıyı kontrollü biçimde boz.
3. Artık dosya veya yarım hakkın temizlendiğini ve iş durumunu kontrol et.
4. Telafi de hata verince işin bekleyen kayıt olarak korunduğunu doğrula.
5. Yeniden başlatmada telafiyi tekrarla ve tamamlanmış işi bozmadığını kontrol et.

## Ne oluyor

Uygulama bir çıktı dosyası oluşturuyor, ardından bu dosyayı kullanıcıya bağlayan kayıt yazıyor. Dosya yazımı başarılı, kayıt yazımı başarısız oluyor. İstek hata dönüyor ve ekranda işlem tamamlanmadı görünüyor. Ancak dosya yerinde duruyor. İlk adımın kalıcı etkisi, bütün işin başarısız olmasıyla kendiliğinden ortadan kalkmıyor.

Aynı mekanizma farklı sistemlere yayılan işlerde de görülür. Bir yerde kaynak ayrılır, başka yerde karşılık gelen durum oluşturulamaz. Akışın yalnız son yanıtına bakarsan başarısızlığı görürsün ama geride kalan işi göremezsin. Kullanıcı tekrar deneyince önceki etkinin üstüne yeni bir etki eklenebilir. İşin ne kadarının tamamlandığını bulmak giderek zorlaşır.

Bir hata bloğuna silme çağrısı koymak ilk boşluğu kapatır. Fakat silme de başarısız olabilir veya süreç bu satıra ulaşmadan kapanabilir. Yeniden başlatılan uygulama hangi kaynağın hangi yarım işe ait olduğunu bilmiyorsa temizlik güvenilir biçimde devam edemez. Telafi kararıyla birlikte bu kararı uygulamaya yetecek kalıcı kimlik ve durum bilgisi gerekir. Örnekte bu bilgi küçük bir iş tablosunda tutulur.

## Gerçek olay

Bu maddede kamuya açık bir ürün kesintisi örnek gösterilmiyor. [Microsoft telafi işlemi rehberi](https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction), ayrı adımlarda oluşan etkilerin işe özgü kurallarla giderilmesini anlatır. Telafinin de hata verebileceğini ve ilerlemenin korunması gerektiğini belirtir. Önceki veri görüntüsünü körlemesine geri yazmak, arada başka işlerin yaptığı değişiklikleri ezebilir.

Yerel örnek gerçek dosya sistemi ve SQLite kullanır. Bir tetikleyici dosyadan sonraki kayıt adımını bilerek bozar. İyi sürüm dosyayı temizler. Silme ayrıca bozulduğunda iş beklemede kalır. Veritabanı kapatılıp yeniden açıldıktan sonra telafi tamamlanır. Bu, belirli bir bulut sağlayıcısının davranışına ilişkin iddia değil, sınırları açık bir hata deneyidir.

## Yapay zekâ bunu neden üretiyor

**Adımlar tek fonksiyonda birleşir.** Ajan dosya yazımı ve veritabanı kaydını arka arkaya koyunca işlem tek bir bütün gibi görünür. İki çağrının aynı fonksiyonda olması aynı atomik sınır içinde oldukları anlamına gelmez. İkinci çağrı hata verdiğinde birincinin çıktısı yaşamaya devam eder. Bu sınır kodun girintisinden anlaşılmaz.

**Hata dönmek tamamlanmış çözüm sayılır.** Ajan ilk olarak kullanıcının başarısızlığı görmesini sağlar. Hata artık saklanmadığı için düzeltme bitmiş sanılabilir. Oysa önceki adımın etkisi hâlâ vardır. Yanıtın doğruluğu ile sistemin yeniden tutarlı duruma gelmesi ayrı koşullardır. İkisini birlikte sınamak gerekir.

**Bellekteki bilgi kalıcı sanılır.** İş sırasında elde edilen dosya adı yerel değişkende tutulur. Aynı süreç yaşamaya devam ederse temizlik yapılabilir. Süreç kapanınca bu değişken kaybolur ve sonraki çalışan neyi temizleyeceğini bilemez. Kurtarma için gereken küçük kayıt, asıl etkiden önce veya onunla güvenli biçimde ilişkilendirilerek saklanmalıdır.

**İkinci hata yolu unutulur.** Ajan silme çağrısını ekleyip yalnız başarılı temizlikle deneyebilir. Silme izni veya depolama erişimi bozulunca telafinin kendisi yarım kalır. Bu yolun kaydı ve yeniden denemesi de gerekir. Buradaki açıklamalar olası üretim mekanizmalarıdır. AI kodlarında ölçülmüş bir hata oranını veya belirli bir modelin alışkanlığını göstermiyor.

## Etki

Artık dosyalar depolama tüketir ve kayıtla ilişkilendirilemeyen içerik oluşturur. İş para, stok veya kullanım hakkı içeriyorsa karşılıksız ayırma ve yanlış hak durumu daha ağır sonuçlar doğurabilir. Bu örnek ödeme işlemi yapmaz. Parasal telafinin kuralları dosya silme örneğinden çıkarılamaz, sağlayıcı ve ürün sözleşmesine göre ayrıca kurulmalıdır.

Kötü tasarlanmış telafi de zarar verebilir. Başka bir işin dosyasını silmek veya eski stok değerini geri yazmak yeni kayıplara yol açar. Bu yüzden kaynağın bu işe ait olduğu bilinmeli ve tekrar çalıştırma güvenli olmalıdır. Tamamlanmış işi kurtarma sırasında yanlışlıkla geri almak da ayrı bir yarış oluşturur.

## Nasıl anlarsın

Akışın her kalıcı etkisini sırala. Sonraki adımı yerel ortamda bozduğunda önceki etki ne oluyor? Hata yanıtının yanında dosya, kayıt ve iş durumunu incele. Yeniden deneme başlatmadan önce eski işin bulunabildiğini kontrol et. Geçici değişken dışında bir kimlik yoksa süreç kapanması sonrası kurtarma yolu eksik olabilir.

Telafiyi de kontrollü olarak boz. İş kaybolmamalı veya tamamlandı sayılmamalıdır. Örnekte silme hatası dosyayı ve bekleyen kaydı korur. Veritabanını yeniden açınca aynı iş temizlenir. Telafiyi tekrar çağırmak zarar vermemeli, başarılı yayımlanmış dosya korunmalıdır. Bu olumlu ve olumsuz kontroller aynı işin yaşam döngüsünü birlikte gösterir.

## Nasıl düzeltirsin

1. **İş sınırını belirle.** Aynı veritabanındaki yazıları transaction ile birleştirebiliyorsan önce bunu değerlendir. Dosya veya dış servis bu sınırın dışındaysa hata sonrası ne olacağına ayrıca karar ver. Her işin doğru telafisi silmek değildir.
2. **Sahipliği kaydet.** Örnek sunucunun ürettiği kimlik ve dosya adını dosyadan önce iş tablosuna yazar. Dizin uygulamaya özeldir. Önceden bulunan dosya çakışması ayrı durum sayılır ve dosya silinmez. Kullanıcıdan gelen yolu doğrudan telafi hedefi yapma.
3. **Tamamlanmayı atomik tut.** Dosyayı bağlayan kayıt ve hazır iş durumu aynı veritabanı transaction'ında yazılır. Başarısızlıkta dosya temizlenir. Dosya zaten yoksa telafi tamamlanabilir. Diğer silme hatalarında bekleyen kayıt korunur ve hata görünür kalır.
4. **Kurtarmayı işlet.** Yalnız artık çalışmayan işleri ele alan bir kurtarma yolu kur. Örnek tek çalışan varsayar. Çok çalışanlı sistemde iş sahipliği ve eşzamanlı kurtarma denetimi gerekir. Sonucu belirsiz uzak yazıyı geri almadan önce sağlayıcıdaki gerçek durumla uzlaştır.

## Bir daha olmasın

Her çok adımlı iş için ilk hata ve telafi hatası deneyi tut. Bekleyen işlerin ne kadar süredir kaldığını izle ve çözülemeyenleri görünür hale getir.

## Sınır

Örnek yerel süreç düzeyinde kurtarma gösterir. Elektrik kesintisine karşı disk dayanımı, çok çalışan koordinasyonu ve uzak servis belirsizliği kanıtlanmaz. Yeniden açma testi gerçek süreç öldürme deneyi değildir. Tek atomik işlem bütün etkileri kapsıyorsa ayrıca telafi düzeni gerekmeyebilir. Başka işlerin değişikliklerini koruma kararı her ürünün kendi kurallarına dayanır.

## Düzeltme kodları

### Node.js: Dosya için kalıcı telafi kaydı

Önce:

```js
// cikti/yayinla.js, açıklama amaçlı. Özel dizin ve tek çalışan varsayılır.
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
export function compensate(db, root, id, remove = unlinkSync) {
  // Hata zaten çağırana döndü, kalan dosya için işlem yapılmaz.
}
export function publish(db, root, content, remove = unlinkSync) {
  if (typeof content !== 'string' || content.length > 10000) throw new Error('Geçersiz içerik');
  const id = randomUUID();
  const file = `${id}.txt`;
  db.prepare("INSERT INTO jobs VALUES (?, ?, 'pending')").run(id, file);
  writeFileSync(join(root, file), content, { flag: 'wx' });
  // Sonraki yazı hata verirse dosya geride kalır.
  db.exec('BEGIN');
  try {
    db.prepare('INSERT INTO artifacts VALUES (?, ?)').run(id, file);
    db.prepare("UPDATE jobs SET state = 'ready' WHERE id = ?").run(id);
    db.exec('COMMIT');
    return id;
  } catch (error) {
    db.exec('ROLLBACK');
    throw error;
  }
}
```

Sonra:

```js
// cikti/yayinla.js, açıklama amaçlı. Özel dizin ve tek çalışan varsayılır.
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
export function compensate(db, root, id, remove = unlinkSync) {
  const job = db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
  if (job?.state !== 'pending') return;
  try { remove(join(root, job.file)); }
  catch (error) { if (error.code !== 'ENOENT') throw error; }
  db.prepare("UPDATE jobs SET state = 'compensated' WHERE id = ?").run(id);
}
export function publish(db, root, content, remove = unlinkSync) {
  if (typeof content !== 'string' || content.length > 10000) throw new Error('Geçersiz içerik');
  const id = randomUUID();
  const file = `${id}.txt`;
  // Niyet dosyadan önce kalıcı veritabanına yazılır.
  db.prepare("INSERT INTO jobs VALUES (?, ?, 'pending')").run(id, file);
  try {
    writeFileSync(join(root, file), content, { flag: 'wx' });
  } catch (error) {
    if (error.code === 'EEXIST') {
      db.prepare("UPDATE jobs SET state = 'conflict' WHERE id = ?").run(id);
      throw error; // Önceden bulunan dosya bize ait sayılmaz.
    }
    compensate(db, root, id, remove);
    throw error;
  }
  try {
    db.exec('BEGIN');
    db.prepare('INSERT INTO artifacts VALUES (?, ?)').run(id, file);
    db.prepare("UPDATE jobs SET state = 'ready' WHERE id = ?").run(id);
    db.exec('COMMIT');
    return id;
  } catch (cause) {
    db.exec('ROLLBACK');
    try { compensate(db, root, id, remove); }
    catch (cleanup) { throw new AggregateError([cause, cleanup], 'Telafi bekliyor'); }
    throw cause;
  }
}
// Yeniden başlatmada yalnız artık çalışmayan pending işler telafi edilir.
// Güç kaybı dayanımı ve birden çok çalışan için ek düzen gerekir.
```

Düzeltmeyi kanıtlayan test:

```js
// cikti/yayinla.test.mjs, açıklama amaçlı. Geçici dosyalar ve gerçek SQLite.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
const { publish, compensate } = await import(process.env.ORNEK_DOSYA);
test('başarısız kayıt telafi edilir, başarısız telafi yeniden açılınca sürer', () => {
  const dir = mkdtempSync(join(tmpdir(), 'vc082-'));
  const root = join(dir, 'files'); mkdirSync(root);
  let db = new DatabaseSync(join(dir, 'state.sqlite'));
  try {
    db.exec(`CREATE TABLE jobs(id TEXT PRIMARY KEY, file TEXT, state TEXT);
      CREATE TABLE artifacts(id TEXT PRIMARY KEY, file TEXT);
      CREATE TRIGGER fail BEFORE INSERT ON artifacts BEGIN
        SELECT RAISE(ABORT, 'Yapay kayıt hatası'); END;`);
    assert.throws(() => publish(db, root, 'Yapay çıktı'), /Yapay kayıt hatası/);
    assert.deepEqual(readdirSync(root), []);
    assert.equal(db.prepare('SELECT state FROM jobs').get().state, 'compensated');
    assert.throws(() => publish(db, root, 'Yapay çıktı', () => {
      throw Object.assign(new Error('Yapay silme hatası'), { code: 'EACCES' });
    }), /Telafi bekliyor/);
    const pending = db.prepare("SELECT id FROM jobs WHERE state = 'pending'").get().id;
    assert.equal(readdirSync(root).length, 1);
    db.close(); db = new DatabaseSync(join(dir, 'state.sqlite'));
    compensate(db, root, pending); compensate(db, root, pending);
    assert.deepEqual(readdirSync(root), []);
    assert.equal(db.prepare('SELECT count(*) AS n FROM artifacts').get().n, 0);
    db.exec('DROP TRIGGER fail');
    const id = publish(db, root, 'Normal çıktı');
    const job = db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
    assert.equal(job.state, 'ready');
    assert.equal(readFileSync(join(root, job.file), 'utf8'), 'Normal çıktı');
    compensate(db, root, id);
    assert.equal(readdirSync(root).length, 1);
    assert.equal(db.prepare('SELECT file FROM artifacts WHERE id = ?').get(id).file, job.file);
  } finally { db.close(); rmSync(dir, { recursive: true, force: true }); }
});
```

## Ajan kuralı (AGENTS.md)

```md
## Yarım işin telafisi yok (vibecheck VC-082)
- Çok adımlı işte her kalıcı etkinin başarısızlık kararını tanımla.
- Telafi için gereken kimliği ve durumu kalıcı olarak sakla.
- Telafi başarısızsa işi tamamlandı diye kapatma.
- Yeniden denenen telafi aynı etkiyi tekrar üretmesin.
- Başka işin verisini eski görüntüye dönerek ezme.
- Çalışan iş ile kurtarılan işin aynı anda değişmesini engelle.
```

## Kaynaklar

1. [Compensating Transaction Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction), Microsoft
2. [Node.js File System API](https://nodejs.org/api/fs.html), Node.js
3. [Node.js SQLite parameter binding](https://nodejs.org/api/sqlite.html), Node.js
4. [SQLite CREATE TRIGGER and RAISE](https://www.sqlite.org/lang_createtrigger.html), SQLite
5. [CWE-459 Incomplete Cleanup](https://cwe.mitre.org/data/definitions/459.html), MITRE

---

vibecheck · Komünite editörlüğü. Metin CC BY 4.0, prompt ve kural parçaları MIT-0. Kaynak: https://vibecheck.komunite.com.tr/madde/cok-adimli-islem-yarida-kalinca-telafi-edilmiyor
