Skip to content

Conversation

@emanuelGitCodes
Copy link

✨ Turning the full-course rescan pipeline fully asynchronous ✨


This PR converts our “rescan an entire course” flow from a single, blocking request into a fan-out message/worker architecture that can scale horizontally and respect Canvas API rate limits. It also introduces environment-driven pool sizing so Docker images can spin up the exact number of PHP workers and Node page-scan workers you need at deploy time.


Why

  • A big course could take several minutes to scan synchronously, tying up PHP-FPM and occasionally blowing past the Canvas rate limit.

  • We want parallel workers (both PHP messenger consumers and Node page-scan tabs) to chew through content items independently.

  • DevOps needs an easy knob (SCAN_POOL_SIZE) to tune concurrency per environment.


What changed

Area Key additions / refactors
Messages & handlers New FullRescanMessage (+ API token & LMS meta)  New ScanContentItem  New FullRescanHandler  New ScanContentItemHandler
Controllers SyncController::fullCourseRescan() now simply dispatch()es a FullRescanMessage instead of doing work synchronously.
LMS layer CanvasLms::updateCourseContent() now fires off every endpoint with apiGetAsync() and streams responses; rate-limit logic extracted to checkXRateLimit() helper.
Rate-limit back-off Quick reactive leaky-bucket check: if x-rate-limit-remaining < 2 we sleep() just long enough for the bucket to refill.
Worker pool Supervisor conf now reads SCAN_POOL_SIZE → numprocs, and Node’s server.ts reads the same env var for its page pool.
ENV / Docker .env.example, docker-compose.yml, and messenger-concurrent.conf accept SCAN_POOL_SIZE.
Misc Lots of error_log() / ConsoleOutput breadcrumbs for easier tracing.


Flow – end-to-end

┌────────────┐                                             
│  Browser   │  POST /api/sync/rescan/:id                  
└────────────┘                                             
        │                                                  
        ▼                                                  
┌──────────────────┐  ① dispatch FullRescanMessage         
│ SyncController   │ ──────────────────────────────────▶   
└──────────────────┘                                        
        ▲                                                  
        │ ⑥ JSON: {status:"queued"}                        
        │                                                  
        ▼                                                  
┌──────────────────────────────┐                           
│  FullRescanHandler (worker)  │                           
│  ② refreshLmsContent         │                           
│  ③ foreach updated item      │                           
│     dispatch ScanContentItem │                           
└──────────────────────────────┘                           
            │                                              
            ▼  (fan-out)                                   
┌──────────────────────────────┐  ④ scan, create issues    
│ ScanContentItemHandler(*)    │  ────────────────────▶ DB  
└──────────────────────────────┘                           
            │                                              
            ▼                                              
     Doctrine / MySQL      ⑤ report updated                

Workers listening on:

  • async_priority_high – for the FullRescanMessage (serial)

  • async_concurrent      – for each ScanContentItem (parallel; numprocs == SCAN_POOL_SIZE)

  • async_priority_low    – existing low-priority jobs (unchanged)


How to test locally

  1. cp .env.example .env and set

SCAN_POOL_SIZE=6
COMPOSE_PROJECT_NAME=udoit


  1. docker compose up --build – you should see six messenger-concurrent_* workers and Node “page pool size = 6”.

  2. Hit Rescan in the UI → request returns instantly (“queued”).

  3. docker logs -f udoit3-php | grep ScanContentItem → each content item should be processed by different PIDs.

  4. Confirm reports & issues populate as before.


Deployment & rollout

  • Safe to deploy incrementally: old endpoints still work; new workers sit idle if no FullRescanMessage arrives.

  • SCAN_POOL_SIZE defaults to 1 if unset, so small instances won’t spawn extra processes accidentally.

  • Remember to reload Supervisor after changing the env var (docker compose up -d --build handles this).


Follow-ups (not in scope here)

  • Replace the quick sleep()-based back-off with a shared “token bucket” Redis semaphore so all worker pods coordinate.

  • Add functional tests for the message fan-out path.

  • Surface progress back to the front-end via WebSockets or polling.


Please review & let me know if you’d like any tweaks!

emanuelGitCodes and others added 30 commits February 4, 2025 08:23
Removed the disabled locks on the menu tabs to allow user to click to report once scan started.
API now calls the aysnc refresh function instead of normal.
Adds aysnc function which removes steps and calls correct function to start async scanning
- Gets course content asynchronously and then triggers scan as content is finished downloading

Co-authored-by: Emanuel <[email protected]>
In the update course content in CanvasLMS it previously was creating new item, but also updating it later in code.
Durning creation added the rest of the fields needed to avoid duplicate assignments.
Adds local scanner support and adds more print statements to test output
Adds to gitignore the scanner files that are downloaded from another repo.
Removes usage of AuthToken in functions and passing it through etc.
…ng. This files are not complete, there more like blueprints to what to do.
Adds messenger system to queue scans.
Currently queues print statement.
Messenger system starts workers and runs concurrently.
Fixes issue with logging not appearing by routing log output to normal stdout and stderr
Updates the messenger to scan the content item.
Adds wait code to wait until all the message workers have completed before finishing the report.
Adds delete function to the MessageHandler instead.
Adds new separatate async function to be called by the message to avoid editing current one.
Updates config and adds more detaches and flushes to try and fix database saving
Re-adds routing back to the async there.
Removes random return in the async refresh.
Removed equal access server to help future merges.
Uncommeted the ace container since removed the files for it.
…to dispatch full rescan jobs async.

Force Full Rescan works and scans everything, but it runs twice. First runs scans everything, second is redundant.
…ontent scanning

- Consolidate messenger worker configurations into a single directory.
- Introduce new messenger handlers for concurrent and high-priority content scanning.
- Update `ScanContentItem` to include content item ID and user ID.
- Modify `FullRescanHandler` to dispatch `ScanContentItem` messages for updated items.
- Adjust `LmsFetchService` to return updated content items for processing.

Working towards having the `Force Full Rescan` process to be divided between different workers to make it faster.
…umber of browsers available on the local scanner.
@emanuelGitCodes emanuelGitCodes added enhancement php Pull requests that update Php code review labels Jun 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement php Pull requests that update Php code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants