implement alternative match-making approach with Lua scripts in Redis

This commit is contained in:
2026-10-25 15:57:24 +03:00
parent 582ecb7c74
commit acdaebd491
19 changed files with 796 additions and 139 deletions
+10
View File
@@ -0,0 +1,10 @@
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6380/0") %>
channel_prefix: ruby_sea_battle_production
+33 -12
View File
@@ -10,15 +10,25 @@ default: &default
timeout: 5000
development:
<<: *default
database: storage/development.sqlite3
primary:
<<: *default
database: storage/development.sqlite3
queue:
<<: *default
database: storage/development_queue.sqlite3
migrations_paths: db/queue_migrate
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: storage/test.sqlite3
primary:
<<: *default
database: storage/test.sqlite3
queue:
<<: *default
database: storage/test_queue.sqlite3
migrations_paths: db/queue_migrate
# PostgreSQL for production.
#
@@ -29,11 +39,22 @@ test:
# DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT
#
production:
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: <%= ENV["DB_NAME"] %>
username: <%= ENV["DB_USER"] %>
password: <%= ENV["DB_PASSWORD"] %>
host: <%= ENV["DB_HOST"] %>
port: <%= ENV["DB_PORT"] || 5432 %>
primary:
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: <%= ENV["DB_NAME"] %>
username: <%= ENV["DB_USER"] %>
password: <%= ENV["DB_PASSWORD"] %>
host: <%= ENV["DB_HOST"] %>
port: <%= ENV["DB_PORT"] || 5432 %>
queue:
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: <%= ENV["QUEUE_DB_NAME"] || ENV["DB_NAME"] %>
username: <%= ENV["QUEUE_DB_USER"] || ENV["DB_USER"] %>
password: <%= ENV["QUEUE_DB_PASSWORD"] || ENV["DB_PASSWORD"] %>
host: <%= ENV["QUEUE_DB_HOST"] || ENV["DB_HOST"] %>
port: <%= ENV["QUEUE_DB_PORT"] || ENV["DB_PORT"] || 5432 %>
migrations_paths: db/queue_migrate
@@ -3,6 +3,11 @@ require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Run background jobs (e.g. the matchmaking sweeper) through Solid Queue,
# supervised by the puma plugin.
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
# Make code changes take effect immediately without server restart.
config.enable_reloading = true
@@ -50,7 +50,8 @@ Rails.application.configure do
config.cache_store = :memory_store
# Replace the default in-process and non-durable queuing backend for Active Job.
config.active_job.queue_adapter = :async
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
@@ -0,0 +1,25 @@
# Matchmaking configuration. All values can be overridden via environment variables.
mm = Rails.application.config.x.matchmaking = ActiveSupport::OrderedOptions.new
mm.game_server_url = ENV.fetch("GAME_SERVER_URL", "ws://localhost:8080")
mm.default_rating = ENV.fetch("MATCHMAKING_DEFAULT_RATING", 1000).to_i
# How long a found match (and its game token) stays valid, in seconds.
mm.match_ttl = ENV.fetch("MATCHMAKING_MATCH_TTL", 120).to_i
mm.token_ttl = ENV.fetch("MATCHMAKING_TOKEN_TTL", 120).to_i
# How long a player waits in the queue before a timeout, in seconds.
mm.queue_ttl = ENV.fetch("MATCHMAKING_QUEUE_TTL", 60).to_i
# How long a found (but not yet finalized) pair is kept in the "pending"
# state before the players are put back into the queue, in seconds.
mm.pending_ttl = ENV.fetch("MATCHMAKING_PENDING_TTL", 15).to_i
# Rating tolerance: players within `base_range` are matched immediately;
# the tolerance grows by `range_per_second` of waiting up to `max_range`.
mm.base_range = ENV.fetch("MATCHMAKING_BASE_RANGE", 100).to_i
mm.range_per_second = ENV.fetch("MATCHMAKING_RANGE_PER_SECOND", 50).to_i
mm.max_range = ENV.fetch("MATCHMAKING_MAX_RANGE", 1000).to_i
# How often the background sweep job re-checks the queue, in seconds.
mm.sweep_interval = ENV.fetch("MATCHMAKING_SWEEP_INTERVAL", 2).to_i
+5 -1
View File
@@ -2,6 +2,10 @@
# Development/test: localhost:6379 (default)
# Production: set REDIS_URL env variable, e.g. redis://redis.example.com:6379/0
# Use an isolated Redis logical database in tests to avoid clobbering dev data.
url = ENV.fetch("REDIS_URL", "redis://localhost:6380/0")
url = url.sub(%r{/\d+$}, "/15") if Rails.env.test?
RedisConnection = ConnectionPool.new(size: 5, timeout: 5) do
Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6380/0"))
Redis.new(url: url)
end
+1 -1
View File
@@ -35,7 +35,7 @@ port ENV.fetch("PORT", 3000)
plugin :tmp_restart
# Run the Solid Queue supervisor inside of Puma for single-server deployments.
# plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || ENV.fetch("RAILS_ENV", "development") == "development"
# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
+18
View File
@@ -0,0 +1,18 @@
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 3
processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
polling_interval: 1
development:
<<: *default
test:
<<: *default
production:
<<: *default
+18
View File
@@ -0,0 +1,18 @@
# examples:
# periodic_cleanup:
# class: CleanSoftDeletedRecordsJob
# queue: background
# args: [ 1000, { batch_size: 500 } ]
# schedule: every hour
# periodic_cleanup_with_command:
# command: "SoftDeletedRecord.due.delete_all"
# priority: 2
# schedule: at 5am every day
production:
matchmaking_sweep:
class: MatchmakingSweepJob
schedule: every <%= ENV.fetch("MATCHMAKING_SWEEP_INTERVAL", 2) %> seconds
clear_solid_queue_finished_jobs:
command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
schedule: every hour at minute 12
+5 -1
View File
@@ -1,12 +1,16 @@
Rails.application.routes.draw do
mount ActionCable.server => "/cable"
namespace :api do
post "register", to: "registrations#create"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "me", to: "sessions#me"
post "matchmaking", to: "matchmaking#create"
delete "matchmaking", to: "matchmaking#destroy"
# Matchmaking starts over the MatchmakingChannel WebSocket
# (see app/channels/matchmaking_channel.rb).
end
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.