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:
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):
- Install Supervisor if you haven’t already.
- 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
- 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

