You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel可启停循环任务实现咨询:基于Pusher的实时倒计时

Hey there! Let's break down how to build this feature step by step—your intuition to combine events, queues, and tasks is totally on the money. Here's a practical, actionable plan tailored to your requirements:

Core Architecture Overview

We’ll use three key components to make this work:

  • Long-running Task: Handles the continuous timer cycles and pushes updates via Pusher.
  • State Storage: Tracks the task’s status (running/stopped) and current timer metadata so new users get accurate remaining time.
  • Events & Pusher: Synchronize timer state in real-time for all connected users, including latecomers.
  • Queues: Manage admin-initiated start/stop actions asynchronously to avoid blocking requests.

1. Set Up Permission Control for Admin Actions

First, lock down the start/stop endpoints to only authorized users:

  • Use an authentication middleware (e.g., auth:admin) on your API routes for timer control.
  • Create a simple policy or check user roles in the controller to verify admin access before allowing state changes.

Example controller snippet:

public function startTimer(Request $request)
{
    // Verify admin access
    if (!$request->user()->isAdmin()) {
        abort(403, 'Unauthorized');
    }

    // Update task state in storage (Redis works great here for speed)
    Cache::put('timer_task_status', 'running', now()->addDays(7));

    // Trigger the long-running task (we'll cover this next)
    Artisan::queue('timer:run');

    return response()->json(['message' => 'Timer started']);
}

public function stopTimer(Request $request)
{
    if (!$request->user()->isAdmin()) {
        abort(403, 'Unauthorized');
    }

    Cache::put('timer_task_status', 'stopped', now()->addDays(7));
    return response()->json(['message' => 'Timer stopped']);
}

2. Build the Long-Running Timer Task

We’ll create an Artisan command that runs continuously (managed via Supervisor to keep it alive) to handle the timer cycles:

Step 2.1: Create the Command

Run php artisan make:command RunTimerCycle, then update the handle() method:

use Illuminate\Support\Facades\Cache;
use Pusher\Pusher;
use Carbon\Carbon;

public function handle()
{
    $pusher = new Pusher(
        config('broadcasting.connections.pusher.key'),
        config('broadcasting.connections.pusher.secret'),
        config('broadcasting.connections.pusher.app_id'),
        ['cluster' => config('broadcasting.connections.pusher.cluster')]
    );

    $totalCycleDuration = 60; // Your 60-second timer

    $this->info('Timer task initialized. Waiting for start signal...');

    while (true) {
        // Check if task is marked as running
        if (Cache::get('timer_task_status') !== 'running') {
            sleep(2); // Poll every 2 seconds to save resources
            continue;
        }

        $cycleStartTime = Carbon::now();
        Cache::put('current_cycle_start_time', $cycleStartTime->toISOString(), now()->addMinutes(2));
        $this->info('Starting new timer cycle at ' . $cycleStartTime);

        // Broadcast cycle start to all users
        $pusher->trigger('timer-channel', 'cycle-started', [
            'total_seconds' => $totalCycleDuration,
            'started_at' => $cycleStartTime->toISOString(),
            'remaining' => $totalCycleDuration
        ]);

        // Count down each second
        for ($i = 0; $i < $totalCycleDuration; $i++) {
            // Exit loop if task is stopped mid-cycle
            if (Cache::get('timer_task_status') !== 'running') {
                $pusher->trigger('timer-channel', 'cycle-stopped', []);
                $this->info('Timer cycle stopped prematurely');
                break;
            }

            $remainingSeconds = $totalCycleDuration - $i - 1;
            $pusher->trigger('timer-channel', 'timer-update', [
                'remaining' => $remainingSeconds,
                'started_at' => $cycleStartTime->toISOString()
            ]);

            sleep(1);
        }

        $this->info('Timer cycle completed');
    }
}

Step 2.2: Keep the Task Running with Supervisor

Configure Supervisor to ensure the command stays alive (even if the server restarts):

  1. Install Supervisor if you haven’t already.
  2. Create a config file (e.g., /etc/supervisor/conf.d/timer-task.conf):
[program:timer-task]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/project/artisan timer:run
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/path/to/your/project/storage/logs/timer-task.log
  1. Reload Supervisor: sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start timer-task

3. Handle Latecomers: Show Remaining Time on Connect

When a user joins after the cycle has started, they need to see the correct remaining time. Here’s how to do it:

Step 3.1: Add an API Endpoint for Current State

Create a route to fetch the latest timer metadata:

public function getCurrentTimerState()
{
    $status = Cache::get('timer_task_status', 'stopped');
    $cycleStartTime = Cache::get('current_cycle_start_time');

    if ($status !== 'running' || !$cycleStartTime) {
        return response()->json([
            'running' => false,
            'remaining' => 0
        ]);
    }

    $totalDuration = 60;
    $elapsed = Carbon::now()->diffInSeconds(Carbon::parse($cycleStartTime));
    $remaining = max(0, $totalDuration - $elapsed);

    return response()->json([
        'running' => true,
        'total_seconds' => $totalDuration,
        'started_at' => $cycleStartTime,
        'remaining' => $remaining
    ]);
}

Step 3.2: Frontend Integration

Use Pusher’s JS library to listen for updates and fetch the initial state on page load:

const pusher = new Pusher('your-pusher-key', {
    cluster: 'your-pusher-cluster'
});

const channel = pusher.subscribe('timer-channel');

// Fetch initial state when user loads the page
fetch('/api/timer/current')
    .then(res => res.json())
    .then(data => {
        if (data.running) {
            updateTimerDisplay(data.remaining);
        }
    });

// Listen for real-time updates
channel.bind('timer-update', data => {
    updateTimerDisplay(data.remaining);
});

channel.bind('cycle-started', data => {
    updateTimerDisplay(data.remaining);
});

channel.bind('cycle-stopped', () => {
    updateTimerDisplay(0);
});

function updateTimerDisplay(seconds) {
    document.getElementById('timer-display').textContent = `${seconds}s remaining`;
}

4. Optional: Use Events for Cleaner Pusher Integration

To decouple the task from Pusher logic, create Laravel events and listeners:

  • Create events like TimerCycleStarted, TimerUpdated, TimerCycleStopped.
  • Make listeners for each event that handle the Pusher broadcast.
  • Update the timer command to dispatch these events instead of calling Pusher directly.

Example event listener:

class BroadcastTimerUpdate
{
    public function handle(TimerUpdated $event)
    {
        broadcast(new TimerUpdated($event->remaining, $event->startedAt))->toOthers();
        // Or use Pusher directly if you prefer
    }
}

Key Notes for Reliability

  • State Persistence: Use Redis (or your database) for storing task status and cycle start time—avoid in-memory storage, as it’ll be lost on server restart.
  • Error Handling: Add try/catch blocks in the timer command to handle Pusher connection errors or unexpected crashes, ensuring the task keeps running.
  • Rate Limiting: Don’t forget to rate-limit the admin start/stop endpoints to prevent abuse.

内容的提问来源于stack exchange,提问作者Walter Woshid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 22:27:53