Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ruby on Rails source walkthrough

The runnable source below is included from the same files packaged in the notes download. Lockfiles, configuration, and the shared browser UI are included in the download.

Database

A notes request reaches this adapter after input validation. DATABASE_ENGINE chooses PostgreSQL, MariaDB, or MongoDB; DATABASE_URL comes from the app’s attachment. SQL uses parameters rather than interpolating note text, and MongoDB uses driver operations. The initialization process creates a table or unique index. Records live in the database, so replacing a web instance leaves them available to its replacement.

  def database
    DB_MUTEX.synchronize do
      return @database if @database
      url = ENV.fetch("DATABASE_URL")
      @database = case ENGINE
      when "mongodb" then Mongo::Client.new(url, server_selection_timeout: 5)[:notes]
      when "postgresql", "mariadb"
        # Active Record uses the Trilogy adapter for MariaDB.
        url = url.sub(/\Amysql:/, "trilogy:") if ENGINE == "mariadb"
        ActiveRecord::Base.establish_connection(url)
      else raise "Unsupported DATABASE_ENGINE"
      end
    end
  end

  def migrate
    db = database
    if ENGINE == "mongodb"
      db.indexes.create_one({ id: 1 }, unique: true)
    else
      ActiveRecord::Base.connection_pool.with_connection do |c|
        c.execute("CREATE TABLE IF NOT EXISTS notes (id VARCHAR(36) PRIMARY KEY, body TEXT NOT NULL)")
      end
    end
    puts "Schema ready"
  end

  def notes
    db = database
    if ENGINE == "mongodb"
      db.find.projection(_id: 0).sort(id: 1).to_a
    else
      Note.order(:id).pluck(:id, :body).map { |id, body| { id: id, body: body } }
    end
  end

  def add_note(body)
    db = database
    note = { id: SecureRandom.uuid, body: body }
    ENGINE == "mongodb" ? db.insert_one(note.dup) : Note.create!(note)
    note
  end

Worker

Creating an export enqueues an ID and returns immediately. This loop reads the notes, writes an export under that ID, records its status, and acknowledges the stream entry. It also reclaims jobs abandoned before acknowledgement. Retrying writes the same export key, so interruption does not create an additional export file; the snapshot can reflect newer notes if they changed before the retry. See worker recovery.

stopping = false
Signal.trap("TERM") { stopping = true }
Signal.trap("INT") { stopping = true }
consumer = SecureRandom.uuid
NotesStore.queue do |q|
  begin
    q.call("XGROUP", "CREATE", "exports", "workers", "0", "MKSTREAM")
  rescue Redis::CommandError => error
    raise unless error.message.include?("BUSYGROUP")
  end
  puts "Worker ready"
  until stopping
    begin
      messages = q.call("XAUTOCLAIM", "exports", "workers", consumer, "30000", "0-0", "COUNT", "1")[1]
      if messages.empty?
        result = q.call("XREADGROUP", "GROUP", "workers", consumer, "COUNT", "1", "BLOCK", "5000", "STREAMS", "exports", ">")
        messages = result ? result[0][1] : []
      end
      messages.each do |message_id, values|
        id = Hash[*values]["id"]
        unless id&.match?(/\A[0-9a-f-]{36}\z/)
          q.call("XACK", "exports", "workers", message_id)
          next
        end
        unless q.get("export:#{id}") == "ready"
          NotesStore.save_export(q, id, JSON.pretty_generate(NotesStore.notes))
          q.set("export:#{id}", "ready")
        end
        q.call("XACK", "exports", "workers", message_id)
        q.call("XDEL", "exports", message_id)
        puts "Export ready: #{id}"
      end
    rescue StandardError
      warn "Export failed; unacknowledged jobs retry after 30 seconds. Check attachments and schema."
      sleep 1
    end
  end
end

Object storage

Without S3 configuration, exports are stored in Redis for the worker checkpoint. With an attached bucket, the worker writes JSON to exports/<id>.json and the web process reads that object for downloads. Both processes need their own bucket attachment. The client uses the supplied endpoint and region with path-style requests. Credentials stay on the server; a private bucket does not add authentication to the example’s public download route.

  def storage
    Aws::S3::Client.new(endpoint: ENV.fetch("S3_ENDPOINT"), region: ENV.fetch("S3_REGION"), force_path_style: true)
  end

  def save_export(q, id, data)
    if ENV["S3_BUCKET"]
      storage.put_object(bucket: ENV.fetch("S3_BUCKET"), key: "exports/#{id}.json", body: data, content_type: "application/json")
    else
      q.set("export:#{id}:data", data)
    end
  end

  def load_export(q, id)
    if ENV["S3_BUCKET"]
      storage.get_object(bucket: ENV.fetch("S3_BUCKET"), key: "exports/#{id}.json").body.read
    else
      q.get("export:#{id}:data")
    end
  end

Routes and controller

Rails.application.routes.draw do
  root "notes#index"
  get "/health", to: "notes#health"
  get "/notes", to: "notes#list"
  post "/notes", to: "notes#create"
  post "/exports", to: "notes#export"
  get "/exports/:id", to: "notes#status"
  get "/exports/:id/download", to: "notes#download"
end
class NotesController < ActionController::API
  rescue_from StandardError do
    render json: { error: "Check attachments, schema, and service availability" }, status: 503
  end

  def index
    render html: File.read(Rails.root.join("public/index.html")).html_safe
  end

  def health
    render plain: "ok\n"
  end

  def list
    render json: NotesStore.notes
  end

  def create
    body = params[:body]
    unless body.is_a?(String) && !body.strip.empty? && body.length <= 2000
      return render json: { error: "Enter a note of 1–2000 characters" }, status: 400
    end
    render json: NotesStore.add_note(body), status: 201
  end

  def export
    id = SecureRandom.uuid
    NotesStore.queue do |q|
      q.set("export:#{id}", "queued")
      q.call("XADD", "exports", "*", "id", id)
    end
    render json: { id: id }, status: 202
  end

  def status
    state = NotesStore.queue { |q| q.get("export:#{params[:id]}") }
    render json: { status: state || "unknown" }, status: state ? 200 : 404
  end

  def download
    id = params[:id]
    state = NotesStore.queue { |q| q.get("export:#{id}") }
    unless id.match?(/\A[0-9a-f-]{36}\z/) && state == "ready"
      return render json: { error: "Export not ready" }, status: 404
    end
    data = NotesStore.queue { |q| NotesStore.load_export(q, id) }
    send_data data, type: "application/json", filename: "notes.json"
  end
end

Review production preparation before adapting these handlers to real users.