Build a Self-Healing Content Pipeline with macOS LaunchAgents
Why This Matters
Cron-based pipelines are ubiquitous in content workflows, but they have a critical flaw: silent failures. When a script fails, cron doesn’t alert you. The first sign of trouble is often a gap in your data or content. Worse, LaunchAgents can unload silently, leaving you unaware that your automation has stopped.
This tutorial shows you how to build a self-healing content pipeline using macOS launchd and LaunchAgents. You’ll learn to:
- Create and manage LaunchAgents.
- Implement a watchdog pattern to monitor and restart agents.
- Ensure resilience with auto-recovery and idempotent scripts.
- Monitor your pipeline effectively.
By the end, you’ll have a robust system that minimizes downtime and alerts you to issues.
Prerequisites
- macOS: This tutorial assumes you’re using macOS, which uses
launchdfor managing daemons and agents. - Scripts: You should have scripts you want to run on a schedule (e.g., Python, Bash, Ruby).
- Basic Shell: Familiarity with shell scripting and command-line tools.
Step 1: The Naive Version
Start with a simple LaunchAgent that runs a script daily.
1.1. Create a Plist File
Create a .plist file in ~/Library/LaunchAgents/. Here’s a template using StartCalendarInterval:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.site-cron</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/username/scripts/site-cron.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</integer>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/site-cron.out</string>
<key>StandardErrorPath</key>
<string>/tmp/site-cron.err</string>
</dict>
</plist>
Key Points:
Label: Unique identifier for the agent.ProgramArguments: Command and arguments to execute.StartCalendarInterval: Schedule in cron-like format.StandardOutPathandStandardErrorPath: Log file paths.
1.2. Load the LaunchAgent
Use launchctl to load the agent:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.site-cron.plist
Note: gui/$(id -u) specifies the user domain.
1.3. Verify It’s Loaded
launchctl list | grep com.example.site-cron
Step 2: What Goes Wrong
A naive setup fails silently when things go wrong.
2.1. Example Failure
Consider a script that crashes due to a ModuleNotFoundError:
# site-cron.py
import nonexistent
print("Site cron running")
2.2. Silent Failure
The agent is “loaded” but not doing anything. You won’t see errors unless you check the logs.
2.3. Debugging
Use launchctl print to inspect the agent:
launchctl print gui/$(id -u)/com.example.site-cron
Redirected Logs:
Check site-cron.out and site-cron.err in /tmp/:
cat /tmp/site-cron.err
2.4. Reloading After Fixes
Even after fixing the script, the agent won’t restart unless you reload it:
launchctl unload gui/$(id -u)/com.example.site-cron
launchctl load ~/Library/LaunchAgents/com.example.site-cron.plist
Step 3: The Watchdog Pattern
To mitigate silent failures, implement a watchdog that monitors and restarts agents.
3.1. Create the Watchdog Script
Create site-cron-watchdog.sh:
#!/bin/bash
WATCHDOG_LABEL="com.example.site-cron-watchdog"
PRIMARY_LABEL="com.example.site-cron"
LOGFILE="/tmp/site-cron-watchdog.log"
function log {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOGFILE"
}
function check_agent {
if ! launchctl list | grep -q "$1"; then
log "Agent $1 is missing. Reloading."
launchctl load ~/Library/LaunchAgents/"$1".plist
fi
}
log "Watchdog started."
while true; do
check_agent "$PRIMARY_LABEL"
sleep 600 # Check every 10 minutes
done
3.2. Make the Script Executable
chmod +x site-cron-watchdog.sh
3.3. Create the Watchdog Plist
Create com.example.site-cron-watchdog.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.site-cron-watchdog</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/scripts/site-cron-watchdog.sh</string>
</array>
<key>StartInterval</key>
<integer>600</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/site-cron-watchdog.out</string>
<key>StandardErrorPath</key>
<string>/tmp/site-cron-watchdog.err</string>
</dict>
</plist>
3.4. Load the Watchdog Agent
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.site-cron-watchdog.plist
3.5. Explanation
- Watchdog Label: Unique identifier for the watchdog.
- Primary Label: Identifier of the agent to monitor.
- Logfile: Path to the watchdog log.
- check_agent Function: Checks if the primary agent is loaded; if not, reloads it.
- Loop: Runs every 10 minutes.
Step 4: Auto-Recovery on Reboot
Ensure your agents restart after a reboot.
4.1. Set RunAtLoad
Add RunAtLoad to every plist:
<key>RunAtLoad</key>
<true/>
4.2. Trade-Offs
- Pros: Agents restart after reboot.
- Cons: If a script depends on a service that’s not ready at boot, it may fail. However, the watchdog will attempt to reload it.
Step 5: The Watchdog-of-Watchdog
The watchdog itself can fail, so it needs to monitor itself.
5.1. Modify the Watchdog Script
Update site-cron-watchdog.sh to check its own label:
function check_self {
if ! launchctl list | grep -q "$WATCHDOG_LABEL"; then
log "Watchdog is missing. Reloading."
launchctl load ~/Library/LaunchAgents/"$WATCHDOG_LABEL".plist
fi
}
# Inside the loop
check_self
check_agent "$PRIMARY_LABEL"
5.2. Explanation
- check_self Function: Checks if the watchdog is loaded; if not, reloads it.
Step 6: Resilience Patterns
Ensure your scripts are idempotent to handle re-runs.
6.1. Idempotency Techniques
- File Locks: Use lock files to prevent concurrent executions.
- Atomic Renames: Use temporary files and atomic rename operations.
- SQLite UPSERT: Use database transactions to avoid duplicate entries.
6.2. Example: File Lock
import fcntl
lockfile = open('site-cron.lock', 'w')
fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
try:
# Your code here
finally:
fcntl.flock(lockfile, fcntl.LOCK_UN)
lockfile.close()
6.3. Explanation
- File Locks: Prevents multiple instances of the script from running simultaneously.
- Exception Handling: Ensures the lock is released even if the script crashes.
Step 7: What to Monitor
Implement monitoring to verify your pipeline’s health.
7.1. Log Tailing
Tail logs to catch issues early:
tail -F /tmp/site-cron.out /tmp/site-cron.err /tmp/site-cron-watchdog.out /tmp/site-cron-watchdog.err
7.2. Daily Verification
Set up a daily cron job to verify output:
0 0 * * * /Users/username/scripts/site-cron-verify.sh
7.3. Example Verification Script
Create site-cron-verify.sh:
#!/bin/bash
EXPECTED_OUTPUT="/path/to/expected/output"
ACTUAL_OUTPUT="/path/to/actual/output"
if diff "$EXPECTED_OUTPUT" "$ACTUAL_OUTPUT" >/dev/null; then
echo "Verification succeeded."
else
echo "Verification failed!" | telegram-send
fi
7.4. Explanation
- diff: Compares expected and actual output.
- telegram-send: Sends a message via Telegram if verification fails.
What’s Next
For a complete, working example, visit the source code on GitHub. The site-cron-watchdog.sh script there includes additional features like:
- Logging: Enhanced logging with timestamps and error codes.
- Configuration: External configuration files for easier management.
- Notifications: Email and Slack notifications for critical failures.
By implementing these strategies, you’ll create a self-healing content pipeline that minimizes downtime and keeps your data and content flowing smoothly.
This tutorial provides a solid foundation for building robust automation on macOS. Adapt the examples to fit your specific use case and expand upon the concepts to suit more complex workflows.
Summary Table
| Component | Description |
|---|---|
RunAtLoad |
Ensures agents start after reboot. |
launchctl print |
Inspects the state of agents. |
StandardOutPath |
Redirects standard output to a log file. |
StandardErrorPath |
Redirects standard error to a log file. |
| Watchdog Pattern | Monitors and restarts agents. |
| Idempotency | Ensures scripts can run multiple times without adverse effects. |
| Monitoring | Verifies the pipeline’s health and alerts on failures. |
Additional Resources
By following this guide, you’ll be well on your way to creating a resilient, self-healing content pipeline that ensures your automation tasks run smoothly and reliably.
Comments
Leave a message below. Your comment saves to your browser.