Automation Queue for Repetitive Web Tasks

January 25, 2026

A tiny drop-in “job queue” that automates repetitive tasks on a website — without cron and without a database.

Why you’d want it: On shared hosting you often can’t rely on cron jobs, and you don’t want to bolt on Redis/MySQL just to run simple maintenance. This script lets you define small tasks (cleanup, rebuild cache, rotate logs, ping endpoints, refresh JSON, prune old files) and run them automatically in the background when your site gets traffic.

What it does:

  • Runs tasks on a schedule (every 5 minutes, hourly, daily, etc.).
  • Stores last-run timestamps in a JSON file (no DB).
  • Locks so it won’t run twice at the same time.
  • Lets you add tasks as simple PHP functions.
  • Includes a secret manual trigger URL for testing.

Install:

  1. Create: /tools/auto/
  2. Save the script below as: /tools/auto/auto.php
  3. Make sure /tools/auto/ is writable (it will create auto_state.json).
  4. Include this ONE line in your site footer (or header):
    <?php require $_SERVER['DOCUMENT_ROOT']."/tools/auto/auto.php"; auto_tick(); ?>
  5. Optional manual run for testing:
    /tools/auto/auto.php?run=TOKEN
<?php
declare(strict_types=1);

/**
 * Tiny Automation Queue (No DB)
 * File: /tools/auto/auto.php
 *
 * Include on your site:
 *   require $_SERVER['DOCUMENT_ROOT']."/tools/auto/auto.php";
 *   auto_tick(); // runs due tasks
 *
 * Manual test:
 *   /tools/auto/auto.php?run=CHANGE_ME_TOKEN
 */

header('X-Content-Type-Options: nosniff');

$AUTO = [
  'state_file' => __DIR__ . '/auto_state.json',
  'lock_file'  => __DIR__ . '/auto_lock.lock',
  'token'      => 'CHANGE_ME_TOKEN',

  // Safety: do not run more than once per request
  'once_per_request' => true,
];

// ---------------- TASKS ----------------
// Add your repetitive tasks here.
// Each task has:
//  - id: unique string
//  - every: seconds between runs
//  - run: callable that returns a short status string (or empty)
function auto_tasks(): array {
  return [
    [
      'id'    => 'prune_tmp',
      'every' => 3600, // hourly
      'run'   => function(): string {
        // Example: delete old files in /tmp older than 7 days
        $dir = $_SERVER['DOCUMENT_ROOT'] . '/tmp';
        if (!is_dir($dir)) return 'tmp missing';

        $cut = time() - (7 * 86400);
        $n = 0;

        $it = new RecursiveIteratorIterator(
          new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
          RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($it as $f) {
          if (!$f->isFile()) continue;
          if ($f->getMTime() > $cut) continue;
          $path = (string)$f->getPathname();
          if (@unlink($path)) $n++;
        }

        return "pruned {$n}";
      }
    ],
    [
      'id'    => 'rotate_log',
      'every' => 86400, // daily
      'run'   => function(): string {
        // Example: cap a log file to last ~200KB
        $log = $_SERVER['DOCUMENT_ROOT'] . '/logs/app.log';
        if (!is_file($log)) return 'log missing';
        $max = 200 * 1024;

        clearstatcache(true, $log);
        $sz = filesize($log);
        if (!$sz || $sz <= $max) return 'log ok';

        $data = file_get_contents($log);
        if ($data === false) return 'read fail';

        $data = substr($data, -$max);
        file_put_contents($log, $data, LOCK_EX);
        return 'log trimmed';
      }
    ],
    [
      'id'    => 'warm_cache',
      'every' => 1800, // every 30 minutes
      'run'   => function(): string {
        // Example: ping a local endpoint to build caches (safe, same-host)
        $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
        $host  = $_SERVER['HTTP_HOST'] ?? '';
        if ($host === '') return 'host missing';

        $url = $proto . $host . '/sitemap.txt'; // or any endpoint
        $ctx = stream_context_create(['http'=>['timeout'=>5,'user_agent'=>'TinyAuto/1.0']]);
        $ok  = @file_get_contents($url, false, $ctx);
        return $ok === false ? 'ping fail' : 'ping ok';
      }
    ],
  ];
}
// --------------------------------------

function auto_load_state(string $file): array {
  if (!is_file($file)) return ['last'=>[], 'log'=>[]];
  $raw = @file_get_contents($file);
  if ($raw === false) return ['last'=>[], 'log'=>[]];
  $j = json_decode($raw, true);
  if (!is_array($j)) return ['last'=>[], 'log'=>[]];
  $j['last'] = (isset($j['last']) && is_array($j['last'])) ? $j['last'] : [];
  $j['log']  = (isset($j['log'])  && is_array($j['log']))  ? $j['log']  : [];
  return $j;
}

function auto_save_state(string $file, array $state): void {
  $out = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  if ($out === false) return;
  @file_put_contents($file, $out, LOCK_EX);
}

function auto_lock(string $lockFile) {
  $fh = @fopen($lockFile, 'c+');
  if (!$fh) return null;
  if (!flock($fh, LOCK_EX | LOCK_NB)) { fclose($fh); return null; }
  return $fh;
}

function auto_log(array &$state, string $id, string $msg): void {
  $state['log'][] = [
    't' => gmdate('c'),
    'task' => $id,
    'msg' => $msg,
  ];
  // keep last 50 log entries
  if (count($state['log']) > 50) $state['log'] = array_slice($state['log'], -50);
}

function auto_run_due(bool $force = false): array {
  $AUTO = $GLOBALS['AUTO'];

  // One-run per request guard
  static $ran = false;
  if (!empty($AUTO['once_per_request']) && $ran && !$force) return ['ran'=>0,'forced'=>false];
  $ran = true;

  // Acquire lock (skip if busy)
  $lock = auto_lock((string)$AUTO['lock_file']);
  if (!$lock) return ['ran'=>0,'forced'=>$force,'busy'=>true];

  $state = auto_load_state((string)$AUTO['state_file']);
  $tasks = auto_tasks();

  $now = time();
  $ranCount = 0;

  foreach ($tasks as $t) {
    $id = (string)($t['id'] ?? '');
    $every = (int)($t['every'] ?? 0);
    $run = $t['run'] ?? null;

    if ($id === '' || $every <= 0 || !is_callable($run)) continue;

    $last = (int)($state['last'][$id] ?? 0);
    $due = $force || ($now - $last >= $every);

    if (!$due) continue;

    // Mark last-run before running (prevents loops if task is slow / fatal)
    $state['last'][$id] = $now;
    auto_save_state((string)$AUTO['state_file'], $state);

    $msg = '';
    try {
      $msg = (string)call_user_func($run);
    } catch (Throwable $e) {
      $msg = 'error: ' . $e->getMessage();
    }

    auto_log($state, $id, $msg);
    auto_save_state((string)$AUTO['state_file'], $state);
    $ranCount++;
  }

  flock($lock, LOCK_UN);
  fclose($lock);

  return ['ran'=>$ranCount,'forced'=>$force,'busy'=>false];
}

/**
 * Call this from your footer/header.
 */
function auto_tick(): void {
  auto_run_due(false);
}

// Manual trigger (for testing)
if (PHP_SAPI !== 'cli' && isset($_GET['run'])) {
  $tok = (string)($_GET['run'] ?? '');
  if (!hash_equals((string)$AUTO['token'], $tok)) {
    http_response_code(403);
    header('Content-Type: text/plain; charset=utf-8');
    echo "Forbidden.\n";
    exit;
  }

  $r = auto_run_due(true);
  header('Content-Type: text/plain; charset=utf-8');
  echo "Tiny Automation Queue\n";
  echo "Ran tasks: " . (int)$r['ran'] . "\n";
  echo "Busy: " . (!empty($r['busy']) ? 'yes' : 'no') . "\n";
  echo "State file: " . $AUTO['state_file'] . "\n";
  exit;
}
?>

Comments (0)

No comments yet — be the first.

← Back to all scripts