Files
mozo-backend/docs/broadcasting-migration.md
T
BenClaw 3e4bcc80c8 feat(counter): add Redis counter adapter, replace DrbCounter
- Add Mozo::Counter::Redis with same get/set/incr/decr interface
- Add redis gem (~> 5.0) to Gemfile
- Update cable.yml to use Redis adapter in production (shared with counters)
- Document DrbCounter → Redis migration in broadcasting-migration.md
- Redis installed and running on vmi3300327
- Leave Faye as current broadcaster; both switches are one-line changes

DrbCounter problems solved:
  - In-memory → persistent (RDB + AOF)
  - Single-process DRb → multi-process safe Redis
  - Atomic INCR/DECR across Puma workers
  - One less custom process to manage
2026-05-17 16:42:09 +02:00

4.5 KiB

Broadcasting: Faye → ActionCable Migration Guide

Current state

Model (SimplyStored::Couch)
  → ApplicationController.new.broadcast_user   # ⚠️ anti-pattern
    → Mozo.broadcast_user
      → Mozo::Broadcaster::Faye.new.broadcast  # HTTP POST to Faye
        → Faye server (Thin, port 9296)
          → WebSocket → browser clients

Target state

Model (Broadcastable concern)
  → Mozo.broadcast_user
    → Mozo::Broadcaster::ActionCable.new.broadcast  # in-process async
      → ActionCable (Rails built-in)
        → WebSocket → browser clients

What this branch adds

File Purpose
lib/mozo/broadcaster/action_cable.rb Drop-in ActionCable broadcaster adapter
config/cable.yml ActionCable configuration (async for single-server)
app/channels/application_cable/connection.rb WebSocket auth via auth_token
app/channels/mozo_channel.rb Channel authorization for user/supplier/employee
app/models/concerns/broadcastable.rb Clean module for models (replaces old monkey-patch)
config/routes.rb Mounts /cable WebSocket endpoint
config/initializers/model_broadcast.rb Fixed: delegates to Mozo directly (no more ApplicationController.new)

How to switch

1. Server (one-line change)

In config/initializers/mozo_settings.rb, change:

# Mozo.broadcaster = Mozo::Broadcaster::Faye.new  # old
Mozo.broadcaster = Mozo::Broadcaster::ActionCable.new  # new

2. Client (mozo-user / mozo-supplier)

Old Faye client (conceptual):

var client = new Faye.Client('https://events.mozo.bar/faye');
client.subscribe('/user/123', function(msg) { ... });

New ActionCable client:

// Using @rails/actioncable npm package
import { createConsumer } from "@rails/actioncable";

const consumer = createConsumer(
  `wss://mozo.bar/cable?auth_token=${authToken}`
);

consumer.subscriptions.create(
  { channel: "MozoChannel", id: "user_123" },
  {
    received(data) {
      // data = { event: "list_closed", data: { id: 42 } }
      handleEvent(data.event, data.data);
    }
  }
);

3. Remove Faye

Once stable:

  • Remove gem 'faye' from Gemfile
  • Remove faye/ directory
  • Remove nginx events.mozo.bar vhost
  • Stop the Faye Thin process

Benefits

  • No extra process — ActionCable runs inside Puma
  • Asyncbroadcast is non-blocking
  • Simpler deploys — one less service to manage
  • WebSocket native — no long-polling fallback complexity
  • Rails auth — cookies/sessions work automatically

Counter: DrbCounter → Redis Migration

Current state

Supplier::Counters (app/models/supplier/counters.rb)
  → Mozo::Counter.incr/decr/get/set
    → Mozo::Counter.connection (Mozo::DrbCounter.object)
      → DRb → druby://localhost:9022
        → InMemoryQCounter (separate Ruby process)
          → on startup: reloads counts from CouchDB
          → in-memory only (lost on restart)

Problems

  1. In-memory only — restart the DRb process = lose all counts until CouchDB reload
  2. Single-process — DRb runs one Ruby process, single point of failure
  3. Separate process — another thing to monitor, deploy, and restart
  4. Race conditions — between Puma workers, increment/decrement is not atomic across the DRb boundary
  5. Custom codeInMemoryQCounter is 100 lines of hand-rolled counter logic

Target state

Supplier::Counters
  → Mozo::Counter.incr/decr/get/set
    → Mozo::Counter.connection (Mozo::Counter::Redis.new)
      → Redis (localhost:6379)
        → persistent, atomic, multi-process safe

How to switch

In config/initializers/mozo_settings.rb, change:

# Mozo::Counter.connection = Mozo::DrbCounter.object  # old
Mozo::Counter.connection = Mozo::Counter::Redis.new    # new

That's it. All existing Mozo::Counter.get/set/incr/decr calls work unchanged.

What Redis provides

  • Atomic INCR/DECR — no race conditions
  • Persistence — RDB snapshots + AOF, survives restarts
  • Multi-process — all Puma workers share the same Redis
  • Already needed — ActionCable uses Redis for pub/sub in production
  • Battle-tested — millions of deployments

Migration steps

  1. apt-get install redis-server — already done on vmi3300327
  2. gem 'redis', '~> 5.0' — added to Gemfile
  3. Switch Mozo::Counter.connection — one-line change in mozo_settings.rb
  4. Stop the DRb counter process (drb_counter/drb_counter.rb)