Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ gem "thruster", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 2.0"

# job worker queue
gem "resque", "~> 2.6"
gem "redis"
gem "resque-scheduler"
gem "resque-web"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
Expand Down Expand Up @@ -70,6 +76,7 @@ gem "arclight"

group :development, :test do
gem "solr_wrapper", ">= 0.3"
gem "rspec-rails", "~> 8.0"
end
gem "rsolr", ">= 1.0", "< 3"
gem "bootstrap", "~> 5.3"
Expand Down
139 changes: 139 additions & 0 deletions Gemfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//= link_tree ../images
//= link_tree ../builds
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<link rel="apple-touch-icon" href="/icon.png">

<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>

Expand Down
16 changes: 16 additions & 0 deletions bin/rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'rspec' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "rubygems"
require "bundler/setup"

load Gem.bin_path("rspec-core", "rspec")
42 changes: 42 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,55 @@ services:
depends_on:
solr:
condition: service_healthy
redis:
condition: service_healthy
build:
context: .
ports:
- '3000:3000'
environment:
- SOLR_URL=http://solr:8983/solr/blacklight-collection
- REDIS_URL=redis://redis:6379/0
env_file:
- .env
redis:
image: redis:7
healthcheck:
interval: 30s
timeout: 10s
start_period: 30s
retries: 5
test: ["CMD", "redis-cli", "ping"]
ports:
- '6379:6379'
volumes:
- redis-data:/data
command: redis-server --appendonly yes
resque:
depends_on:
redis:
condition: service_healthy
build:
context: .
environment:
- SOLR_URL=http://solr:8983/solr/blacklight-collection
- REDIS_URL=redis://redis:6379/0
- QUEUE=*
env_file:
- .env
command: bundle exec rake environment resque:work
resque-web:
depends_on:
redis:
condition: service_healthy
build:
context: .
environment:
- REDIS_URL=redis://redis:6379/0
- RESQUE_NAMESPACE=resque:boxrunner
ports:
- '5678:5678'
command: bundle exec rackup -o 0.0.0.0 -p 5678 config/resque_web.ru
solr:
depends_on:
zookeeper:
Expand Down Expand Up @@ -48,3 +89,4 @@ services:
ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
volumes:
solr-data:
redis-data:
5 changes: 5 additions & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
# Cache assets for far-future expiry since they are all digest stamped.
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

# CSS is already compiled and prefixed by cssbundling-rails (yarn build:css),
# so disable Sprockets' Sass compressor (added by sassc-rails). Re-running
# SassC over the bundled application.css breaks on Bootstrap's url(...) assets.
config.assets.css_compressor = nil

# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"

Expand Down
6 changes: 6 additions & 0 deletions config/initializers/resque.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

# config/initializers/resque.rb
require "resque"

Resque.redis = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
Resque.redis.namespace = "resque:boxrunner"
14 changes: 14 additions & 0 deletions config/resque_web.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Rackup config for the Resque dashboard (Resque::Server).
#
# We run the dashboard directly via `rackup`/Puma instead of the `resque-web`
# binary, because resque 2.7's WebRunner relies on the old `Rack::Handler` API
# that was removed in Rack 3.
#
# bundle exec rackup -o 0.0.0.0 -p 5678 config/resque_web.ru
require "resque"
require "resque/server"

Resque.redis = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
Resque.redis.namespace = ENV.fetch("RESQUE_NAMESPACE", "resque:boxrunner")

run Resque::Server.new
131 changes: 131 additions & 0 deletions resque.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Resque on boxrunner — Recommendations

## Context

- **Rails:** 8.1.3
- **Ruby:** 3.4.9
- **Current Active Job adapter:** `:solid_queue` (see `config/environments/production.rb`)

Because this is a Rails 8 app already wired up with Solid Queue, the recommendation
below leads with "should you?" before "how".

## Should you use Resque on Rails 8?

For a **new** Rails 8 app, Resque is usually *not* the best choice anymore:

- **Solid Queue** (already your adapter) is the Rails 8 default. It's database-backed,
needs **no Redis**, and is maintained by the Rails team. You already have it wired up.
- **Sidekiq** is the most popular Redis-backed option — multithreaded, far more efficient
than Resque (which is process-per-worker), actively maintained, and the de-facto standard
if you *do* want Redis.
- **Resque** is mature and battle-tested but comparatively dated: it forks one process per
job (memory-heavy), and the ecosystem has largely moved to Sidekiq/Solid Queue.

**Only reach for Resque if you have a specific reason** (existing Resque infrastructure,
team familiarity, a plugin you depend on). Otherwise stick with Solid Queue or pick Sidekiq.

## Installing & configuring Resque (best practice)

### 1. Gems

```ruby
# Gemfile
gem "resque", "~> 2.6"
gem "redis"
# Optional but recommended:
gem "resque-scheduler" # cron-style + delayed jobs
gem "resque-web" # the dashboard (extracted from core in Resque 2.x)
```

```bash
bundle install
```

### 2. Use it *through* Active Job, not directly

The single most important best practice: **don't write `Resque.enqueue` and
`def self.perform` everywhere.** Use Active Job with the Resque adapter so your job code
stays backend-agnostic.

```ruby
# config/application.rb (or per-environment)
config.active_job.queue_adapter = :resque
```

```ruby
# app/jobs/welcome_email_job.rb
class WelcomeEmailJob < ApplicationJob
queue_as :default

def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end

WelcomeEmailJob.perform_later(user.id)
```

This way, if you later move to Sidekiq or Solid Queue, you change one config line,
not your jobs.

### 3. Redis connection — make it environment-driven

```ruby
# config/initializers/resque.rb
require "resque"

Resque.redis = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
Resque.redis.namespace = "resque:boxrunner"
```

Drive `REDIS_URL` from Rails credentials / env vars; never hardcode production hosts.

### 4. Workers — rake task + queue list

Run workers in priority order:

```bash
QUEUE=critical,default,low bundle exec rake resque:work
```

In production, supervise workers with **systemd**, **foreman/Procfile**, or your container
orchestrator — one process per worker, and run multiple processes for concurrency
(Resque is single-job-per-process).

### 5. Restart workers on deploy

Resque workers load your app code at boot and **don't pick up new code automatically**.
Your deploy must restart (or send `QUIT` for graceful shutdown of) workers, or you'll run
stale code. This is a common production gotcha.

### 6. Dashboard — mount it securely behind auth

```ruby
# config/routes.rb
require "resque/server"

authenticate :user, ->(u) { u.admin? } do # requires an admin flag on User
mount Resque::Server.new, at: "/resque"
end
```

Never expose `/resque` unauthenticated — it can delete/retry jobs.

### 7. Failure handling

- Configure the failure backend (Redis by default) and monitor the failed queue.
- For automatic retries, add `gem "resque-retry"` (Resque core has none built in — unlike
Sidekiq/Active Job's `retry_on`). Note that Active Job's `retry_on`/`discard_on` *do*
work through the adapter, which is another reason to go via Active Job.

### 8. Don't enqueue inside DB transactions

Enqueue jobs **after_commit**, not inside a transaction — otherwise a worker may pick up
the job before the record is committed (or after a rollback) and fail with
"record not found."

## Recommendation for this app

**Stay on Solid Queue** unless you have a concrete reason to need Resque/Redis. If you do
need a Redis-backed queue, prefer **Sidekiq** over Resque for new work.
82 changes: 82 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'rails_helper'

RSpec.describe User, type: :model do
subject(:user) { described_class.new(email: 'user@example.com', password: 'password123') }

describe 'validations' do
it 'is valid with an email and password' do
expect(user).to be_valid
end

it 'requires an email' do
user.email = ''
expect(user).not_to be_valid
expect(user.errors[:email]).to include("can't be blank")
end

it 'requires a valid email format' do
user.email = 'not-an-email'
expect(user).not_to be_valid
expect(user.errors[:email]).to be_present
end

it 'requires the email to be unique (case-insensitively)' do
described_class.create!(email: 'user@example.com', password: 'password123')
duplicate = described_class.new(email: 'USER@example.com', password: 'password123')
expect(duplicate).not_to be_valid
expect(duplicate.errors[:email]).to include('has already been taken')
end

it 'requires a password' do
user.password = nil
expect(user).not_to be_valid
expect(user.errors[:password]).to include("can't be blank")
end

it 'requires the password and its confirmation to match' do
user.password_confirmation = 'mismatch'
expect(user).not_to be_valid
expect(user.errors[:password_confirmation]).to be_present
end
end

describe 'devise modules' do
it 'enables the expected devise modules' do
expect(described_class.devise_modules).to include(
:database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:validatable
)
end

it 'authenticates with the correct password' do
user.save!
expect(user.valid_password?('password123')).to be(true)
expect(user.valid_password?('wrong')).to be(false)
end

it 'encrypts the password rather than storing it in plain text' do
user.save!
expect(user.encrypted_password).to be_present
expect(user.encrypted_password).not_to eq('password123')
end
end

describe 'attributes' do
it 'defaults guest to false' do
expect(described_class.new.guest).to be(false)
end
end

describe 'Blacklight integration' do
it 'exposes email as the displayable login key' do
expect(described_class.string_display_key).to eq(:email)
end

it 'includes Blacklight::User' do
expect(described_class.ancestors).to include(Blacklight::User)
end
end
end
Loading