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 e-mail 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 e-mail 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"