# Zamanlanmış Görev Ekleme

https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme

Kendi kodunuzu zamanlanmış çalıştırın: kuyruğa bir handler sınıfı, yeniden deneme ve telemetriyle birlikte.

## Genel Bakış

Zamanlanmış iş bir crontab değil, bir kuyruktur. Zamanlayıcı vakti gelenleri seçer, her biri `cronjob_queue` içinde bir satıra dönüşür ve bir handler o satırı işe çevirir.

Görev ikiye ayrılır: keşif hedef başına bir iş kuyruklar, execute tek hedefin işini yapar. Yeniden deneme toplu değil hedef başınadır.

## Ön Koşullar

- Çalışan bir işçi; tık bu sisteme ulaşana kadar hiçbir şey çalışmaz.
- Handler'ın nerede yaşayacağı: çekirdek cron dizini ya da sahibi olan modül.
- Hedef için kararlı bir tanımlayıcı; yinelenmeyi durduran idempotency anahtarı o olur.

## Yapı

Dosya başına bir sınıf, dosya adı da sınıf adı.

| Parça | Nerede | Kural |
| --- | --- | --- |
| Handler sınıfı | `coremio/cronjobs`, `WISECP\CronJobs` | Execute sonek almaz, keşif `Discover` ile biter |
| Kayıt | `coremio/cronjobs/registry.php` | Otomatik keşif; `TYPE` tanımlayan her sınıf kaydedilir |
| Kuyruk satırları | `cronjob_queue` | İş başına bir satır: payload, deneme, log, sonuç |
| Zamanlama durumu | `cronjob_runtime` | Görev başına: son çalışma, sonraki çalışma, son slot, kapatma anahtarı |
| Görev ayarları | `coremio/configuration/cronjobs.php` | `cronjobs/tasks/{task}`: aşama soneki atılmış, noktaları tireye çevrilmiş tip |

## Adım Adım

### Handler'ı Yazın

1. Cron dizininde sınıf başına bir dosya, adı sınıfla aynı.
2. `TYPE`'ı `discover` ya da `execute` ile biten noktalı bir dize olarak tanımlayın.
3. `FREQUENCY`'yi yalnız zamanlayıcının tetikleyeceği handler'a ekleyin.
4. Tek arayüz metodunu uygulayın: mantıksal değer ya da zengin dizi.

### Kaydedin

1. Çekirdek görevi için başka bir şey gerekmez; kayıt dosyası dizini tarar.
2. Modül görevi kendini modülün kanca dosyasından kaydeder.
3. Dosyayı kendiniz dahil edin; yükleyici yalnız çekirdek cron ad alanını çözer.
4. Kayıtlı handler'ları listeleyin ve tipinizin göründüğünü doğrulayın.

### İşi Kuyruklayın

1. Keşifte hedefleri sorgulayın ve her hedef için bir execute işi kuyruklayın.
2. Her kuyruklamaya bir idempotency anahtarı verin.
3. Üst iş id'sini geçirin ve toplu işi sınırlayın.
4. Sonradan gösterilecek ne varsa payload'a fotoğrafını çekin.

### Koşuyu Doğrulayın

1. Otomasyon panosunu açın ve elle çalıştırma kontrolünü kullanın.
2. Kuyruk satırını okuyun: durum, deneme, hata günlüğü, sonuç payload'ı.
3. Hiçbir şey görünmüyorsa önce tıka bakın: işsiz tarama satır bırakmaz.
4. Sonuç payload'ı oturduktan sonra bir özet üretici ekleyin.

## Referans

### Handler Sözleşmesi

```php
namespace WISECP\CronJobs;

interface CronJobHandler
{
    // $payload  BU iş için dispatch()'e verilmiş, çözülmüş dizi
    // $job      kuyruk satırının tamamı: id, type, attempts, max_attempts, parent_id,
    //           idempotency_key, started_at, process_logs ve gerisi
    public function handle(array $payload, array $job): bool|array;
}
```

```php
// ZORUNLU. Bu olmadan sınıf bir görev değildir ve kayıt dosyası onu atlar.
// Son parça aşamadır: tarama için '.discover', atomik iş için '.execute'.
public const TYPE = 'acme.sync.discover';

// İSTEĞE BAĞLI; anlamı "bunu zamanlayıcı doğrudan tetikler".
// Değerler: 'minute' | 'hour' | 'day' | 'month'. Operatör görev başına ezebilir.
public const FREQUENCY = 'hour';

// İSTEĞE BAĞLI, varsayılan 3. Yeniden deneme bütçesi; gecikme dakika cinsinden
// 2'nin deneme kuvvetidir, yani 2, 4, 8. Deneme dış bir yan etkiyi tekrarlıyorsa 1 yapın.
public const MAX_ATTEMPTS = 1;

// İSTEĞE BAĞLI, varsayılan 120. İşçinin bu işi alarmla kestiği saniye sayısı.
// Yedekleme gibi uzun işler için yükseltin; işçi tıkı değil işi öldürür.
public const MAX_RUNTIME = 300;
```

- **WISECP\CronJobs\CronJobHandler**: Tek arayüz, tek metot. Uygulamayan sınıfı kayıt dosyası atlar.
- **CronScheduler::tick()**: Slotu gelen handler'ları kuyruklar; birden çok işçi güvenli.
- **register:cronjobs**: Modülün kendi tiplerini kaydettiği yer; dönüş değeri yok sayılır.
- **CronJobQueue::handler_available()**: Kaydı koşulsuz yapın; modül bu kurulumda kurulu değilse çekirdek görevi kuyruğa almaz ve pano "Modül Kurulu Değil" gösterir. Sunucu modülünde ölçüt o türde etkin bir sunucu kaydı, diğer tiplerde modül ayarındaki `status`. Başka bir ön koşul gerekiyorsa handler'a `public static function available(): bool` ekleyin.

### Dönüşünüz Ne Demek

| Dönüş | Satır ne olur | Ne zaman kullanılır |
| --- | --- | --- |
| `true` | `completed`, telemetri yok | Gösterilecek bir şey olmayan başarı |
| `false` | Geri çekilmeli deneme, sonra `failed` | Tekrarın düzeltebileceği hata |
| Fırlatılan `Throwable` | `false` ile aynı, mesaj yakalanır | Hatayı yutmak istemiyorsanız |
| `['success' => bool, 'result' => array]` | `completed` ya da `failed`, sonuç saklanır | Telemetri istiyorsanız |
| `['success' => true, 'status' => 'cancelled', 'result' => array]` | `cancelled`, çocuksuz satır silinir | Bu tıkta iş yoktu |

- **Log satırı değil, cancelled**: İşsiz bir tık, log yazmak yerine cancelled sinyali döndürür.
- **result.reason**: Kısa, makine okunur bir dize: `no-targets`, `disabled`, `already-exists`.
- **Hata mesajı**: Fırlatılan hatanın mesajı işlem günlüğüne yazılır. Çıplak `false`'ta kuyruk `error`, `gateway_error` ya da `message` arar.
- **Aşama ve sayaçlar**: Verimlilik yalnız `.execute` sayar; tarama her zaman `.discover`.

### Kuyruk Yardımcısı

```php
// Kuyruklar. Yeni satır id'sini, idempotency anahtarı zaten alınmışsa MEVCUT
// satırın id'sini döndürür. Sıfırdan büyük dönüş, ekleme yapıldığının kanıtı DEĞİLDİR.
public static function dispatch(string $type, array $payload = [], array $opts = []): int;

// Kayıt. Çekirdek handler'ları için kayıt dosyası çağırır; modül kendisi çağırır.
public static function register(string $type, string $handlerClass): void;

// Sahiplenilmiş bir satırı çalıştırır. $job id değil, kuyruk satırının tamamıdır.
public static function process(array $job): bool;

// İç gözlem.
public static function resolve_handler(string $type): ?string;
public static function registered_handlers(): array;
public static function resolve_task_type(string $task): string;

// Bakım; ikisi de platformun kendi görevlerine zaten bağlı.
public static function recover_stale(int $minutes = 5): void;
public static function cleanup(array $retention = []): array;
```

- **CronJobQueue::dispatch()**: Üçüncü argüman aşağıdaki seçenekler; payload JSON'a çevrilir, nesne olmaz.
- **opts.idempotency_key**: Tablonun tamamında tekil; zaten varsa ekleme olmaz ve eski id döner.
- **opts.parent_id**: Execute işini onu üreten keşif işine bağlar; üstü silmek çocukları da siler.
- **opts.priority**: Tam sayı, varsayılan 5, düşük önce; eşitlikte en eski önce.
- **opts.scheduled_at**: İşi gelecekteki bir ana kadar bekletir; boşsa bir sonraki tık.
- **opts.title**: Kuyruk listesindeki etiket, 255 karakter, saklanır.
- **opts.max_attempts**: Bu kuyruklama için handler sabitini ezer; ikisi de yoksa 3.
- **CronJobQueue::resolve_task_type()**: Görev adını handler tipine çevirir; tireleri noktaya kendiniz çevirmeyin.

### Sonuç Sekmesi

Handler'ınızdaki statik bir metottan üretilir; HTML saklanmaz.

```php
// $resultPayload, handle() metodunuzun döndürdüğü 'result' dizisinin ta kendisidir.
// Yalnız yönetici sekmeyi açtığında, kayıt dizini üzerinden çözülerek çağrılır.
public static function renderSummary(array $resultPayload): string;
```

- **Payload'ı okuyun, veritabanını asla**: Satır tarihsel bir kayıt; güncel durumu çekmek dünün tarihi altında bugünü verir.
- **Etiketler dil dosyalarından gelir**: İki dil dosyasını da yazın; eksik anahtar ham anahtar olarak görünmeli.
- **Yazdığınız her şeyi kaçırın**: Payload kullanıcı ve uzak metin taşır; üretici ham işaretleme döndürür.

## Örnek

Tam bir çift: saatlik tarama ve tek hizmeti uzlaştıran işçi.

```php
namespace WISECP\CronJobs;

class AcmeQuotaSyncDiscover implements CronJobHandler
{
    public const TYPE      = 'acme.quotasync.discover';
    public const FREQUENCY = 'hour';

    private const BATCH = 200;

    public function handle(array $payload, array $job): bool|array
    {
        // Operatör anahtarı. İlk satırda okumak, kapalı bir görevin hiçbir şey yapmamasını
        // sağlar; yıkıcı görevler için bu zorunludur.
        if ((int) (\Config::get('cronjobs/tasks/acme-quotasync/enabled') ?? 0) !== 1)
            return ['success' => true, 'status' => 'cancelled', 'result' => ['reason' => 'disabled']];

        $stmt = \WDB::select('id,name,owner_id')->from('users_products');
        $stmt->where('status', '=', 'active', '&&');
        $stmt->where('module', '=', 'AcmeCloud');
        $stmt->limit(self::BATCH);
        $rows = $stmt->build() ? $stmt->fetch_assoc() : [];

        if (!$rows)
            return ['success' => true, 'status' => 'cancelled', 'result' => ['reason' => 'no-targets']];

        $parentId   = (int) ($job['id'] ?? 0);
        $dispatched = [];

        foreach ($rows as $row) {
            $sid = (int) ($row['id'] ?? 0);
            if ($sid <= 0) continue;

            // Anahtar, hizmetin yanı sıra saat dilimini de taşır: sabit bir anahtar bir
            // sonraki saatte de var olurdu (tamamlanmış satırlar, saklama süresi işleyene
            // kadar kendi anahtarlarını korur) ve sonraki her dağıtım sessizce eski satırı
            // döndürürdü.
            $key = 'acmeq_' . $sid . '_' . date('YmdH');

            $childId = \CronJobQueue::dispatch(self::stage_execute(), [
                'service_id'    => $sid,
                'service_label' => trim((string) ($row['name'] ?? '')),
                'owner_id'      => (int) ($row['owner_id'] ?? 0),
            ], ['idempotency_key' => $key, 'parent_id' => $parentId]);

            $dispatched[] = ['service_id' => $sid, 'child_job_id' => $childId ?: null];
        }

        return ['success' => true, 'result' => ['items' => $dispatched, 'count' => count($dispatched)]];
    }

    private static function stage_execute(): string
    {
        return AcmeQuotaSync::TYPE;
    }
}
```

```php
namespace WISECP\CronJobs;

class AcmeQuotaSync implements CronJobHandler
{
    // FREQUENCY yok: bu hiç zamanlanmaz, yalnız yukarıdaki tarama tarafından dağıtılır.
    public const TYPE         = 'acme.quotasync.execute';
    public const MAX_ATTEMPTS = 2;

    public function handle(array $payload, array $job): bool|array
    {
        $sid = (int) ($payload['service_id'] ?? 0);
        if ($sid <= 0) return false;              // bu kadar bozuk bir yük kendini düzeltmez

        $service = \Services::get($sid);
        if (!$service)
            return ['success' => true, 'status' => 'cancelled', 'result' => ['reason' => 'service-gone', 'service_id' => $sid]];

        $remote = \Modules::getInstance('Servers', 'AcmeCloud')->quota_of($sid);
        $before = (int) ($service['options']['quota'] ?? 0);
        $after  = (int) ($remote['quota'] ?? 0);

        if ($before === $after)
            return ['success' => true, 'status' => 'cancelled', 'result' => ['reason' => 'in-sync', 'service_id' => $sid]];

        // Seçenekler Services::set() üzerinden gidip gelir, o da diziyi sizin için JSON'a
        // çevirir. set_options() diye bir helper yoktur: okuyun, birleştirin, haritanın
        // tamamını yazın.
        $options = $service['options'] ?? [];
        $options['quota'] = $after;
        \Services::set($sid, ['options' => $options]);

        // renderSummary()'nin ihtiyaç duyacağı her şey, hâlâ doğruyken burada anlık görüntüye alınır.
        return ['success' => true, 'result' => [
            'service_id'    => $sid,
            'service_label' => (string) ($payload['service_label'] ?? ''),
            'quota_before'  => $before,
            'quota_after'   => $after,
        ]];
    }

    public static function renderSummary(array $r): string
    {
        $esc = static fn (string $s): string => htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $L   = static fn (string $k): string => (string) \Language::gc('admin/automation/' . $k);

        $sid  = (int) ($r['service_id'] ?? 0);
        $href = (string) \LinkGenerator::admin('services-1', ['detail'], '', ['id' => $sid]);
        $name = trim((string) ($r['service_label'] ?? ''));

        return '<dl class="row small mb-0">'
            . '<dt class="col-sm-4 text-muted fw-normal">' . $esc($L('telemetry-acme-service')) . '</dt>'
            . '<dd class="col-sm-8 mb-1"><a href="' . $esc($href) . '" target="_blank" rel="noopener">'
            . $esc($name !== '' ? $name : (string) $sid) . '</a></dd>'
            . '<dt class="col-sm-4 text-muted fw-normal">' . $esc($L('telemetry-acme-quota')) . '</dt>'
            . '<dd class="col-sm-8 mb-1">' . (int) ($r['quota_before'] ?? 0) . ' &rarr; ' . (int) ($r['quota_after'] ?? 0) . '</dd>'
            . '</dl>';
    }
}
```

Aynı çiftin modül içindeki hâli.

```php
use WISECP\Modules\Addons\Acme\CronJobs\QuotaSyncDiscover;
use WISECP\Modules\Addons\Acme\CronJobs\QuotaSyncExecute;

// Kayıt defteri yüklenirken, çekirdek işleyicileri yerine oturduktan sonra ateşlenir.
Hook::add('register:cronjobs', 1, function () {
    // Zorunlu: otomatik yükleyici WISECP\CronJobs ad alanını çözer ama bir modül
    // dizinindeki iç içe ad alanını çözmez, bu yüzden dosya elle dahil edilir.
    require_once __DIR__ . DS . 'cronjobs' . DS . 'QuotaSyncDiscover.php';
    require_once __DIR__ . DS . 'cronjobs' . DS . 'QuotaSyncExecute.php';

    CronJobQueue::register(QuotaSyncDiscover::TYPE, QuotaSyncDiscover::class);
    CronJobQueue::register(QuotaSyncExecute::TYPE, QuotaSyncExecute::class);
});
```

Kuyruğu kendiniz sürmek.

```php
// Tip kayıtlı mı? Kayıtlı olmayan bir tip açılışı değil, işi başarısız kılar.
$handler = CronJobQueue::resolve_handler('acme.quotasync.execute');

// Kuyruğa bir iş ekleyin. Dönüşe dikkat: var olan bir anahtar size ESKİ satırın id'sini verir.
$id = CronJobQueue::dispatch('acme.quotasync.execute',
    ['service_id' => 5001],
    ['idempotency_key' => 'acmeq_manual_5001', 'priority' => 1]);

// İşçiyi atlayarak burada ve şimdi çalıştırın. process() id'yi değil SATIRI ister.
$job = CronJobQueue::get($id);
$ok  = $job ? CronJobQueue::process($job) : false;

// İşleyicinin bildirdiğini geri okuyun; Sonuçlar sekmesinin kendi kaynağı budur.
$row    = CronJobQueue::get($id, 'status,attempts,result_payload');
$result = Utility::jdecode((string) ($row['result_payload'] ?? ''), true) ?: [];
```

## Tuzaklar

> **Sabit anahtar ikinci koşuyu durdurur**
> 
> Tamamlanmış satır anahtarını yedi gün korur ve aynı anahtar eski id'yi döndürür. Hedef meşru olarak iki kez işlenebiliyorsa anahtara zaman slotunu koyun.

> **Yıkıcı görevler kapalı gelir**
> 
> Kayıt silen ya da dışarı veri gönderen görev kapalı gelir ve ilk satırda kendi anahtarını kontrol eder.

> **Boş dakikalar durmuş zamanlayıcı demek değil**
> 
> Çocuğu olmayan cancelled satır silinir, o yüzden kuyruk saymak boşluk gösterir. Kanıt zamanlama durumu tablosu.

> **Uzun işler iki dakika sonra öldürülür**
> 
> İşçi bir alarm kurar ve süreyi aşan işi keser; uzun işi tercihen kuyruklamalara bölün.

> **Yeniden deneme yan etkiyi de tekrarlar**
> 
> Hata üç kez denenir ve handler'ın yaptığını yeniden yapar. Idempotent yapın ya da bütçeyi bire çekin.

## İlgili Makaleler

- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
- [Modülden Kanca Kaydetme](https://dev.wisecp.com/tr/modulden-kanca-kaydetme)
- [Hata Yönetimi](https://dev.wisecp.com/tr/hata-yonetimi)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
- [Faturalama Davranışını Değiştirme](https://dev.wisecp.com/tr/faturalama-davranisini-degistirme)
- [Çekirdeğe Dokunmadan Çalışma](https://dev.wisecp.com/tr/cekirdege-dokunmadan-calisma)
