Initial commit
This commit is contained in:
639
skills/jobs/MISSION_CONTROL_SETUP.md
Normal file
639
skills/jobs/MISSION_CONTROL_SETUP.md
Normal file
@@ -0,0 +1,639 @@
|
||||
---
|
||||
skill: jobs
|
||||
category: reference
|
||||
description: Mission Control Jobs setup and authentication patterns
|
||||
---
|
||||
|
||||
# Mission Control Jobs - Complete Setup Guide
|
||||
|
||||
Mission Control Jobs provides a production-ready web dashboard for monitoring and managing SolidQueue background jobs. This guide covers complete setup for development through production deployment with team access.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Add Gem
|
||||
|
||||
```ruby
|
||||
# Gemfile
|
||||
gem "mission_control-jobs"
|
||||
```
|
||||
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
### 2. Mount Engine with Authentication
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
# Production: Require admin authentication
|
||||
if Rails.env.production?
|
||||
authenticate :user, ->(user) { user.admin? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
else
|
||||
# Development/Staging: Open access or HTTP Basic Auth
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Configure (Optional)
|
||||
|
||||
```ruby
|
||||
# config/initializers/mission_control.rb
|
||||
MissionControl::Jobs.configure do |config|
|
||||
# Job retention periods
|
||||
config.finished_jobs_retention_period = 14.days # Default: 7 days
|
||||
config.failed_jobs_retention_period = 90.days # Default: 30 days
|
||||
|
||||
# Filter sensitive arguments from dashboard display
|
||||
config.filter_parameters = [:password, :token, :secret, :api_key]
|
||||
end
|
||||
```
|
||||
|
||||
### 4. Access Dashboard
|
||||
|
||||
Visit `http://localhost:3000/jobs` in your browser (development) or `https://yourapp.com/jobs` (production).
|
||||
|
||||
---
|
||||
|
||||
## Production Authentication Patterns
|
||||
|
||||
### Pattern 1: Devise Admin Users (Recommended)
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
authenticate :user, ->(user) { user.admin? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- User model with `admin?` method or `admin` boolean field
|
||||
- Devise authentication already configured
|
||||
|
||||
**Example User Model:**
|
||||
|
||||
```ruby
|
||||
# app/models/user.rb
|
||||
class User < ApplicationRecord
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
# Option 1: Boolean field
|
||||
def admin?
|
||||
admin # Assumes `admin` boolean column exists
|
||||
end
|
||||
|
||||
# Option 2: Role-based
|
||||
enum role: { user: 0, admin: 1, superadmin: 2 }
|
||||
|
||||
def admin?
|
||||
admin? || superadmin?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 2: Custom Authentication Logic
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
authenticate :user, ->(user) { user.can_access_mission_control? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
|
||||
# app/models/user.rb
|
||||
class User < ApplicationRecord
|
||||
def can_access_mission_control?
|
||||
admin? || role == "operations" || email.end_with?("@yourcompany.com")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 3: HTTP Basic Auth (Staging/Internal Tools)
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
# Add constraint for HTTP Basic Auth
|
||||
constraints(->(req) { authenticate_mission_control(req) }) do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
|
||||
# config/application.rb or initializer
|
||||
def authenticate_mission_control(request)
|
||||
return true if Rails.env.development?
|
||||
|
||||
authenticate_or_request_with_http_basic do |username, password|
|
||||
username == ENV['MISSION_CONTROL_USERNAME'] &&
|
||||
password == ENV['MISSION_CONTROL_PASSWORD']
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Set environment variables:**
|
||||
|
||||
```bash
|
||||
# .env or production secrets
|
||||
MISSION_CONTROL_USERNAME=admin
|
||||
MISSION_CONTROL_PASSWORD=secure_random_password_here
|
||||
```
|
||||
|
||||
### Pattern 4: IP Whitelist (Internal Networks)
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
constraints(->(req) { internal_ip?(req.remote_ip) }) do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
|
||||
# config/application.rb
|
||||
def internal_ip?(ip)
|
||||
allowed_ips = ENV.fetch('MISSION_CONTROL_IPS', '').split(',')
|
||||
allowed_ips.include?(ip) || ip.start_with?('10.', '192.168.')
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 5: Multi-Environment Configuration
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
case Rails.env
|
||||
when "production"
|
||||
# Production: Require admin user
|
||||
authenticate :user, ->(user) { user.admin? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
when "staging"
|
||||
# Staging: HTTP Basic Auth
|
||||
constraints(->(req) { authenticate_basic(req) }) do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
else
|
||||
# Development: Open access
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Team Access Management
|
||||
|
||||
### Granting Admin Access
|
||||
|
||||
```ruby
|
||||
# Rails console (production)
|
||||
rails console
|
||||
|
||||
# Grant admin access to user
|
||||
user = User.find_by(email: "teammate@company.com")
|
||||
user.update!(admin: true)
|
||||
|
||||
# Or using role enum
|
||||
user.update!(role: :admin)
|
||||
```
|
||||
|
||||
### Bulk Admin Creation
|
||||
|
||||
```ruby
|
||||
# db/seeds.rb or migration
|
||||
admin_emails = [
|
||||
"ops_lead@company.com",
|
||||
"dev_lead@company.com",
|
||||
"support_manager@company.com"
|
||||
]
|
||||
|
||||
admin_emails.each do |email|
|
||||
user = User.find_or_create_by(email: email)
|
||||
user.update!(admin: true)
|
||||
end
|
||||
```
|
||||
|
||||
### Team Roles Pattern
|
||||
|
||||
```ruby
|
||||
# app/models/user.rb
|
||||
class User < ApplicationRecord
|
||||
enum role: {
|
||||
user: 0,
|
||||
developer: 1,
|
||||
operations: 2,
|
||||
admin: 3
|
||||
}
|
||||
|
||||
def can_access_jobs_dashboard?
|
||||
developer? || operations? || admin?
|
||||
end
|
||||
end
|
||||
|
||||
# config/routes.rb
|
||||
authenticate :user, ->(user) { user.can_access_jobs_dashboard? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Features & Usage
|
||||
|
||||
### Jobs Overview Tab
|
||||
|
||||
**Features:**
|
||||
- View all jobs across all queues
|
||||
- Filter by status: pending, running, finished, failed
|
||||
- Real-time updates (auto-refresh)
|
||||
- Queue performance metrics
|
||||
|
||||
**Common Operations:**
|
||||
- Search jobs by class name
|
||||
- Filter by date range
|
||||
- Sort by created/finished time
|
||||
|
||||
### Queues Tab
|
||||
|
||||
**Metrics Displayed:**
|
||||
- Pending job count per queue
|
||||
- Active workers per queue
|
||||
- Throughput (jobs/minute)
|
||||
- Latency (average wait time)
|
||||
|
||||
**Use Cases:**
|
||||
- Identify bottlenecked queues
|
||||
- Verify queue priority configuration
|
||||
- Monitor worker capacity
|
||||
|
||||
### Failed Jobs Tab
|
||||
|
||||
**Features:**
|
||||
- Full error backtraces
|
||||
- Job arguments and context
|
||||
- Retry history and attempt counts
|
||||
- Bulk retry/discard operations
|
||||
|
||||
**Workflows:**
|
||||
|
||||
1. **Investigating Failures:**
|
||||
- Click failed job to see full backtrace
|
||||
- Review job arguments for invalid data
|
||||
- Check retry history for transient vs persistent failures
|
||||
|
||||
2. **Bulk Recovery:**
|
||||
- Select multiple failed jobs
|
||||
- Click "Retry Selected" to requeue
|
||||
- Or "Discard Selected" for jobs that can't be fixed
|
||||
|
||||
3. **Pattern Detection:**
|
||||
- Group by error type to find systemic issues
|
||||
- Filter by time range to correlate with deployments
|
||||
- Search by class name to find job-specific problems
|
||||
|
||||
### Individual Job Details
|
||||
|
||||
**Information Displayed:**
|
||||
- Job class and queue name
|
||||
- Enqueued/started/finished timestamps
|
||||
- Duration and execution time
|
||||
- Full arguments (with sensitive params filtered)
|
||||
- Error message and backtrace (if failed)
|
||||
- Retry count and next retry time
|
||||
|
||||
**Available Actions:**
|
||||
- Retry job (failed jobs only)
|
||||
- Discard job (remove from queue)
|
||||
- View full execution context
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Job Retention
|
||||
|
||||
Control how long finished and failed jobs are kept in the database:
|
||||
|
||||
```ruby
|
||||
# config/initializers/mission_control.rb
|
||||
MissionControl::Jobs.configure do |config|
|
||||
# Keep finished jobs for 2 weeks (default: 7 days)
|
||||
config.finished_jobs_retention_period = 14.days
|
||||
|
||||
# Keep failed jobs for 3 months (default: 30 days)
|
||||
config.failed_jobs_retention_period = 90.days
|
||||
end
|
||||
```
|
||||
|
||||
**Automatic Cleanup:**
|
||||
|
||||
SolidQueue automatically cleans up old jobs based on these settings. No manual intervention needed.
|
||||
|
||||
**Manual Cleanup:**
|
||||
|
||||
```ruby
|
||||
# Rails console
|
||||
SolidQueue::Job.finished.where("finished_at < ?", 14.days.ago).delete_all
|
||||
SolidQueue::Job.failed.where("failed_at < ?", 90.days.ago).delete_all
|
||||
```
|
||||
|
||||
### Parameter Filtering
|
||||
|
||||
Prevent sensitive data from appearing in the dashboard:
|
||||
|
||||
```ruby
|
||||
MissionControl::Jobs.configure do |config|
|
||||
# Filter these parameter keys from display
|
||||
config.filter_parameters = [
|
||||
:password,
|
||||
:token,
|
||||
:secret,
|
||||
:api_key,
|
||||
:private_key,
|
||||
:access_token,
|
||||
:refresh_token,
|
||||
:credit_card,
|
||||
:ssn
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
**Example Job Arguments:**
|
||||
|
||||
```ruby
|
||||
# Job enqueued with:
|
||||
SendEmailJob.perform_later(
|
||||
user_id: 123,
|
||||
password: "secret123",
|
||||
api_token: "sk_live_abc123"
|
||||
)
|
||||
|
||||
# Displayed in Mission Control as:
|
||||
{
|
||||
user_id: 123,
|
||||
password: "[FILTERED]",
|
||||
api_token: "[FILTERED]"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Routes
|
||||
|
||||
Mount at a different path:
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
mount MissionControl::Jobs::Engine, at: "/admin/background-jobs"
|
||||
```
|
||||
|
||||
Access at: `https://yourapp.com/admin/background-jobs`
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment Checklist
|
||||
|
||||
- [ ] `mission_control-jobs` gem added to Gemfile
|
||||
- [ ] Bundle installed and Gemfile.lock committed
|
||||
- [ ] Routes configured with authentication
|
||||
- [ ] Authentication tested in staging environment
|
||||
- [ ] Admin users granted access
|
||||
- [ ] Parameter filtering configured for sensitive data
|
||||
- [ ] Job retention periods configured
|
||||
- [ ] Team members notified of dashboard URL
|
||||
- [ ] Dashboard access verified in production
|
||||
- [ ] Monitoring alerts configured (optional)
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Alerting Integration
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
Expose job queue health for external monitoring:
|
||||
|
||||
```ruby
|
||||
# app/controllers/health_controller.rb
|
||||
class HealthController < ApplicationController
|
||||
skip_before_action :authenticate_user! # Public endpoint
|
||||
|
||||
def jobs
|
||||
pending_count = SolidQueue::Job.pending.count
|
||||
failed_count = SolidQueue::Job.failed.count
|
||||
oldest_pending = oldest_pending_job_age
|
||||
|
||||
status = if oldest_pending > 30 || failed_count > 100
|
||||
:service_unavailable
|
||||
else
|
||||
:ok
|
||||
end
|
||||
|
||||
render json: {
|
||||
status: status == :ok ? "healthy" : "degraded",
|
||||
pending_jobs: pending_count,
|
||||
failed_jobs: failed_count,
|
||||
oldest_pending_minutes: oldest_pending
|
||||
}, status: status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oldest_pending_job_age
|
||||
oldest = SolidQueue::Job.pending.order(:created_at).first
|
||||
return 0 unless oldest
|
||||
((Time.current - oldest.created_at) / 60).round
|
||||
end
|
||||
end
|
||||
|
||||
# config/routes.rb
|
||||
get '/health/jobs', to: 'health#jobs'
|
||||
```
|
||||
|
||||
### External Monitoring Setup
|
||||
|
||||
```bash
|
||||
# Uptime monitoring (Pingdom, UptimeRobot, etc.)
|
||||
GET https://yourapp.com/health/jobs
|
||||
|
||||
# Expected response (healthy):
|
||||
{
|
||||
"status": "healthy",
|
||||
"pending_jobs": 42,
|
||||
"failed_jobs": 3,
|
||||
"oldest_pending_minutes": 2
|
||||
}
|
||||
|
||||
# Alert on:
|
||||
# - status != "healthy"
|
||||
# - failed_jobs > threshold
|
||||
# - oldest_pending_minutes > 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Retry All Failed Jobs
|
||||
|
||||
```ruby
|
||||
# Rails console
|
||||
SolidQueue::Job.failed.find_each(&:retry!)
|
||||
|
||||
# Or with Mission Control UI:
|
||||
# 1. Navigate to Failed Jobs tab
|
||||
# 2. Select all jobs
|
||||
# 3. Click "Retry Selected"
|
||||
```
|
||||
|
||||
### Discard Specific Failed Jobs
|
||||
|
||||
```ruby
|
||||
# Rails console - discard jobs older than 1 week
|
||||
SolidQueue::Job.failed
|
||||
.where("failed_at < ?", 1.week.ago)
|
||||
.delete_all
|
||||
|
||||
# Or by job class
|
||||
SolidQueue::Job.failed
|
||||
.where(class_name: "ProblematicJob")
|
||||
.delete_all
|
||||
```
|
||||
|
||||
### Pause/Resume Queue Processing
|
||||
|
||||
```ruby
|
||||
# Not directly supported by SolidQueue
|
||||
# Instead, scale workers to 0 in queue.yml and restart
|
||||
|
||||
# config/queue.yml (temporary)
|
||||
production:
|
||||
workers:
|
||||
- queues: [critical, mailers]
|
||||
threads: 5
|
||||
processes: 0 # Paused
|
||||
```
|
||||
|
||||
### Monitor Specific Queue
|
||||
|
||||
```ruby
|
||||
# Rails console
|
||||
SolidQueue::Job.where(queue_name: "mailers").pending.count
|
||||
SolidQueue::Job.where(queue_name: "mailers").failed.count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dashboard Not Loading
|
||||
|
||||
**Symptom:** 404 or routing error
|
||||
|
||||
**Solutions:**
|
||||
1. Verify gem is installed: `bundle list | grep mission_control`
|
||||
2. Check routes: `rails routes | grep jobs`
|
||||
3. Restart server after adding gem
|
||||
4. Check authentication constraints aren't blocking access
|
||||
|
||||
### Authentication Loop/Redirect
|
||||
|
||||
**Symptom:** Redirected to login repeatedly
|
||||
|
||||
**Solutions:**
|
||||
1. Verify user is logged in: `current_user` in console
|
||||
2. Check authentication lambda: `user.admin?` returns true
|
||||
3. Verify Devise configuration allows access to mounted engines
|
||||
4. Check for conflicting before_action filters
|
||||
|
||||
### Slow Dashboard Performance
|
||||
|
||||
**Symptom:** Dashboard takes >5s to load
|
||||
|
||||
**Solutions:**
|
||||
1. Clean up old finished jobs:
|
||||
```ruby
|
||||
SolidQueue::Job.finished.where("finished_at < ?", 7.days.ago).delete_all
|
||||
```
|
||||
2. Add database indexes (if not present):
|
||||
```ruby
|
||||
add_index :solid_queue_jobs, [:queue_name, :status]
|
||||
add_index :solid_queue_jobs, [:status, :created_at]
|
||||
```
|
||||
3. Reduce retention periods in initializer
|
||||
|
||||
### Jobs Not Appearing
|
||||
|
||||
**Symptom:** Dashboard shows 0 jobs but jobs are running
|
||||
|
||||
**Solutions:**
|
||||
1. Verify SolidQueue is configured: `Rails.configuration.active_job.queue_adapter`
|
||||
2. Check queue database connection in `config/database.yml`
|
||||
3. Run queue migrations: `rails db:migrate:queue`
|
||||
4. Verify jobs are using SolidQueue, not inline adapter
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Production Hardening
|
||||
|
||||
1. **Always require authentication:**
|
||||
```ruby
|
||||
# ❌ NEVER do this in production
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
|
||||
# ✅ Always authenticate
|
||||
authenticate :user, ->(user) { user.admin? } do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
```
|
||||
|
||||
2. **Filter sensitive parameters:**
|
||||
```ruby
|
||||
config.filter_parameters = [:password, :token, :secret, :api_key]
|
||||
```
|
||||
|
||||
3. **Use HTTPS only:**
|
||||
```ruby
|
||||
# config/environments/production.rb
|
||||
config.force_ssl = true
|
||||
```
|
||||
|
||||
4. **Limit admin access:**
|
||||
- Grant admin rights only to operations team
|
||||
- Audit admin user list regularly
|
||||
- Use role-based access for granular control
|
||||
|
||||
5. **Monitor access logs:**
|
||||
```ruby
|
||||
# Track who accesses Mission Control
|
||||
class ApplicationController < ActionController::Base
|
||||
before_action :log_mission_control_access, if: :mission_control_request?
|
||||
|
||||
private
|
||||
|
||||
def mission_control_request?
|
||||
request.path.start_with?('/jobs')
|
||||
end
|
||||
|
||||
def log_mission_control_access
|
||||
Rails.logger.info(
|
||||
"Mission Control accessed by #{current_user&.email} " \
|
||||
"from #{request.remote_ip}"
|
||||
)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Mission Control Jobs GitHub](https://github.com/rails/mission_control-jobs)
|
||||
- [SolidQueue Documentation](https://github.com/rails/solid_queue)
|
||||
- [Rails Active Job Guide](https://guides.rubyonrails.org/active_job_basics.html)
|
||||
- [Rails 8 Release Notes - Solid Stack](https://edgeguides.rubyonrails.org/8_0_release_notes.html)
|
||||
Reference in New Issue
Block a user