Zamanlanmış Görev Ekleme

1.6k görüntülenme Markdown

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çaNeredeKural
Handler sınıfıcoremio/cronjobs, WISECP\CronJobsExecute sonek almaz, keşif Discover ile biter
Kayıtcoremio/cronjobs/registry.phpOtomatik 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 durumucronjob_runtimeGörev başına: son çalışma, sonraki çalışma, son slot, kapatma anahtarı
Görev ayarlarıcoremio/configuration/cronjobs.phpcronjobs/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

coremio/cronjobs/CronJobHandler.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;
}
handler'ın tanımlayabileceği sabitler
// 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 olurNe zaman kullanılır
truecompleted, telemetri yokGösterilecek bir şey olmayan başarı
falseGeri çekilmeli deneme, sonra failedTekrarın düzeltebileceği hata
Fırlatılan Throwablefalse ile aynı, mesaj yakalanırHatayı yutmak istemiyorsanız
['success' => bool, 'result' => array]completed ya da failed, sonuç saklanırTelemetri istiyorsanız
['success' => true, 'status' => 'cancelled', 'result' => array]cancelled, çocuksuz satır silinirBu 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ı

coremio/helpers/CronJobQueue.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.

isteğe bağlı, handler sınıfında
// $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.

coremio/cronjobs/AcmeQuotaSyncDiscover.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;
    }
}
coremio/cronjobs/AcmeQuotaSync.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.

coremio/modules/Addons/Acme/hooks.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.

kuyruğu kendiniz sürmek
// 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.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.