Eric Radman : a Journal

The Sidecomment Web Stack

sidecomment.io is a service that allows readers to select text and create a ticket with suggestions or corrections that the site owner can respond to. Like a commenting platform such as Hyvor Talk, JavaScript is included from an external CDN, and is authorized by a site code.

This concept developed after I noticed how many people were willing to take the time to write an email to point out typos, broken links or errors on this online journal and other project pages.

Static Content and HTTP Routing: httpd/relayd

Since I'm using OpenBSD, httpd(8) is an my go-to daemon for static content

# httpd.conf

server "cdn.sidecomment.io" {
    log style forwarded
    listen on "*" port 8000

    root "htdocs/sidecomment.io/cdn"

    # Let's Encrypt
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    }

    location "/*" {
        directory auto index
    }
}

relayd(8) routes HTTP requests and provides TLS termination for https://

# relayd.conf
ext_addr = 0.0.0.0
table <httpd> { 127.0.0.1 }
table <puma> { 127.0.0.1 }

# TLS proxy all home services
http protocol "httpproxy" {
    tls keypair "sidecomment.io"

    match header set "X-Client-IP" value "$REMOTE_ADDR:$REMOTE_PORT"
    match header set "X-Forwarded-For" value "$REMOTE_ADDR"
    match header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"

    pass request forward to <puma>
    pass request path "/*.*" forward to <httpd>
    pass request path "/.well-known/acme-challenge/*" forward to <httpd>
    pass request header "Host" value "cdn.sidecomment.io" forward to <httpd>
}

relay "https" {
    listen on $ext_addr port 443 tls
    protocol httpproxy
    forward to <httpd> port 8000
    forward to <puma> port 9292
}

relay "http" {
    listen on $ext_addr port 80
    protocol httpproxy
    forward to <httpd> port 8000
    forward to <puma> port 9292
}

Web Stack: Ruby/Sinatra/Haml

Any language can serve HTTP requests. I considered a few different frameworks, but I settled on Ruby because it gives me access to Haml templates. Sinatra is not perfect, but it is a very readable DSL. In addition to providing separate routes for HTTP request type, custom rules can be built

# main.rb

# test to see if the 'otp' query string is set
set(:otp) {|value| condition { params[:otp].nil? == value.nil? }}

get '/account', :otp => true do
end

get '/account' do
end

post '/account' do
end

cpan/gem/pip/npm/cargo-like systems are prone to regressions during upgrades since there is no release engineering that works in tandem with the operating system. For ruby-gems there are two strategies for working around broken builds:

# select an older version
gem33 install nokogiri -v 1.14.2

# build from top of tree
gem33 install specific_install
gem33 specific_install https://github.com/puma/puma.git

Application Server

Puma was designed to be a fast and efficient, but I was primarily interested in a server the reports useful error messages if it cannot start. This web application (like most?) is not designed to be thread-safe, so Puma must be set to use one thread per worker

# config.ru
require './main'
run Sinatra::Application
# puma.rb
app_dir = __dir__
port 9292
pidfile "#{app_dir}/run/puma.pid"
state_path "#{app_dir}/run/puma.state"
directory "#{app_dir}/"
stdout_redirect "#{app_dir}/log/puma.out", "#{app_dir}/log/puma.stderr.log", true
workers 3
threads 1, 1
activate_control_app "unix://#{app_dir}/run/pumactl.sock"

It usually possible to use cron to run periodic actions, or to deploy a dedicated service to handle these tasks, but the deployment of small applications can also be simplified by starting up a sidecar process. In worker mode the Puma configuration file is Ruby and is able to fork a process

# puma.rb
require_relative 'notify'

def log(stream, label, message)
  ts = Time.now.strftime('%m-%d-%Y %H:%M:%S')
  stream.puts "#{ts} #{label} #{message}"
end

before_fork do
  fork do
    Process.setproctitle('puma: sidecomment scheduler')
    loop do
      log $stdout, 'notify_tickets', notify_tickets.to_a
      log $stdout, 'notify_replies', notify_replies.to_a
      sleep 600
    rescue StandardError => e
      log $stderr, 'error', e.full_message
      next
    end
  end
end

Defining before_fork() allows the application to do things before the application is started, in this case start a subprocess that wakes up every 5 minutes. Logging to STDOUT and STDERR is captured in the standard puma logs.

The startup rc.d script supports Puma's graceful restart

#!/bin/ksh

daemon="puma33 -C /var/www/htdocs/sidecomment.io/puma.rb -e production"
daemon_flags="--control-url tcp://127.0.0.1:9293 --control-token ********"
daemon_user="www"
rc_bg=YES

. /etc/rc.d/rc.subr

rc_reload() {
    ${rcexec} pumactl33 ${daemon_flags} phased-restart
}

rc_stop() {
    ${rcexec} pumactl33 ${daemon_flags} stop
}

rc_check() {
    ${rcexec} pumactl33 ${daemon_flags} status
}

rc_cmd "$1"

Database: PostgreSQL

I prefer a seven day log rotation. log_filename is formatted using strftime(3)

# postgresql.conf
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%a.log'
log_rotation_age = 1d
log_rotation_size = 0
log_truncate_on_rotation = on
log_min_duration_statement = 250ms
log_checkpoints = on
log_connections = on
log_lock_waits = on
log_temp_files = 0

pg_auto_failover was intended to be used as a coordination and supervision daemon, but it can also be used to set basic tuning parameters

PG_AUTOCTL_DEBUG=1 pg_autoctl do pgsetup tune --pgdata /var/postgresql/data/
# basic tuning computed by pg_auto_failover
track_functions = pl
shared_buffers = '251 MB'
work_mem = '16 MB'
maintenance_work_mem = '256 MB'
effective_cache_size = '755 MB'
autovacuum_max_workers = 3
autovacuum_vacuum_scale_factor = 0.08
autovacuum_analyze_scale_factor = 0.02

Mail Relay: smtpd

Sidecomment depends on email to validate site codes and user codes. A dedicated email relay is running smtpd(8)

# smtpd.conf

listen on egress port 25
listen on egress port 587

action "mbox" mbox alias <aliases>
action "relay" relay

match for local action "mbox"
match from any for domain sidecomment.io action "mbox"
match from src <trusted> for any action "relay"
match auth from any for any action "relay"

Hosts running the application are configured to forward to this mail relay

# smtpd.conf

table aliases file:/etc/mail/aliases

listen on socket
listen on localhost

action "mbox" mbox alias <aliases>
action "outbound" relay host smtp://relay1:587

match from local for local action "mbox"
match from local for any action "outbound"