Go
Context
Context is a concurrency pattern in Go. It allows request handlers in Go servers to pass cancellation signals to any goroutine that has started from this request.
For example, in a web server, the each request is typically handled by a different goroutine, which in turn may start multiple goroutines to execute database queries or send requests to other services.
Using the context pattern, a struct called the context
is passed from one function to the other containing a Done
channel, which any function starting goroutines
should wait on, typically with select
. If the Done
channel is closed, any goroutine should do a best effort to stop any operation and free resources used.
Without context, an http request may be cancelled, but gorotoutines pointlessly continue their operations. The resources spared in this way are typically small but in situations of heavy load, requests may become slow and with clients resending their requests when aborted, performance issues may compound.
- You can add values to it like a map using withValue but it's somewhat of an antipattern
- The main use is for canceling goroutines
- Let's say there is an http request coming, and you start an expensive operation
- In Node.js, you'd execute this operation, which may even take 10 min.
- But the client has left, not waiting for a response.
- You would continue processing it, and at the end put it in the request, which will not be sent anywhere.
- Using native promises, there is no way, for the one who started it, to cancel it.
- In Go, your goroutine can listen in the done channel, to know when to cancel.
- Also, you can define a context deadline as a time duration.
- If you switch to channels, it's good practice to always accept a context and always listen in the done channel.
- Also, for every function that does IO operations you can pass context and call the libraries by passing the context, expecting them to accept it. Eg. in database calls.
- In this way, your can separate between IO and pure functions too.
Docker
Run image with current directory mounted
docker run --rm -it -v $(pwd):/usr/src/project ubuntu:focal
Kubernetes
Don't use it.
GitHub Actions
Example configuration yaml for Django:
name: Django CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
run: |
python manage.py test
env:
SECRET_KEY: "thisisthesecretkey"
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/postgres"
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
needs: build
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh/
echo "$SSH_KEY" > ~/.ssh/heartfort.key
chmod 600 ~/.ssh/heartfort.key
cat >> ~/.ssh/config << EOF
Host heartfort
HostName heartfort.com
User root
IdentityFile ~/.ssh/heartfort.key
StrictHostKeyChecking no
EOF
echo DATABASE_URL=$DATABASE_URL > ~/sshenv
scp ~/sshenv heartfort:~/.ssh/environment
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Clone repository
run: |
ssh heartfort "rm -rf /opt/apps/srdce"
ssh heartfort 'cd /opt/apps/ && git clone git@github.com:sirodoht/srdce.git --config core.sshCommand="ssh -i ~/.ssh/id_rsa_github_deploy_key"'
- name: Install requirements
run: ssh heartfort 'cd /opt/apps/srdce && python3 -m venv venv && . venv/bin/activate && pip3 install -r requirements.txt'
- name: Collect static
run: ssh heartfort "cd /opt/apps/srdce && . venv/bin/activate && DATABASE_URL=$DATABASE_URL python3 manage.py collectstatic --noinput"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Run migrations
run: ssh heartfort "cd /opt/apps/srdce && . venv/bin/activate && DATABASE_URL=$DATABASE_URL python3 manage.py migrate"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Reload server
run: ssh heartfort 'touch /etc/uwsgi/vassals/srdce.ini'
You have jobs, and each job has steps. It can also depend on other jobs.
A step can be inheritable. Like Docker's FROM
. E.g. uses: actions/checkout@v2
. The uses
key means
that it will draw this code to execute: actions/checkout.
If your step has run
then it just runs that in the shell.
For using environment variable, one can add them in the repository settings and if they are a secret,
to use them like DATABASE_URL: ${{ secrets.DATABASE_URL }}
.
To run a job only on master, we use if: github.ref == 'refs/heads/master'
.
If you're doing a static website, then it can be like this:
name: Deploy mdBook
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v2
- name: Configure SSH
run: |
mkdir -p ~/.ssh/
echo "$SSH_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
cat >> ~/.ssh/config << EOF
Host venn
HostName wiki.venn.dev
User root
IdentityFile ~/.ssh/id_rsa
StrictHostKeyChecking no
EOF
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
- name: Install mdbook
run: |
wget https://github.com/rust-lang/mdBook/releases/download/v0.4.7/mdbook-v0.4.7-x86_64-unknown-linux-gnu.tar.gz
tar -xzvf mdbook-v0.4.7-x86_64-unknown-linux-gnu.tar.gz
- name: Build site
run: ./mdbook build
- name: Cleanup old files
run: |
ssh venn "rm -r /var/www/wiki.venn.dev/"
ssh venn "mkdir -p /var/www/wiki.venn.dev/html"
- name: Install requirements
run: rsync -rsP ./book/ root@wiki.venn.dev:/var/www/wiki.venn.dev/html
- name: Reload nginx
run: ssh venn "systemctl reload nginx"
Since it's ubuntu (runs-on: ubuntu-latest
) we can install stuff and use existing tools like wget
and tar
.
Dokku
Dokku is a self-hosted heroku-like service. You can git push
to start a deployment build with heroku-like buildpacks or Docker, and it will handle
updating load balancer (nginx) configuration, zero-downtime deployments, SSL certificates via let's encrypt, etc.
Manual deployment
In case you don't need code to be built, you can manually deploy a pre-built docker image, for example for deploying docker images built by others.
- Create a new app with
dokku apps:create APP_NAME
- Tag the pre-built docker image as
dokku/APP_NAME:VERSION
withdocker image tag IMAGE dokku/APP_NAME:VERSION
. Replace VERSION with 0.0.1,0.0.2 etc. - Deploy the docker image using
dokku tags:deploy APP_NAME VERSION
.
Example: Grafana deployment
In this example we deploy grafana version 7.3.7, using the /var/lib/dokku/data/storage folder for volumes as recommended by dokku.
$ dokku apps:create grafana
$ docker image pull grafana/grafana:7.3.7
$ docker image tag grafana/grafana:7.3.7 dokku/grafana:7.3.7
$ dokku storage:mount grafana /var/lib/dokku/data/storage/grafana:/var/lib/grafana
$ sudo mkdir /var/lib/dokku/data/storage/grafana
$ sudo chown 472:472 /var/lib/dokku/data/storage/grafana/
$ dokku tags:deploy grafana 7.3.7
$ dokku proxy:ports-set grafana http:80:3000
Monads
Probabilistic Programming (List monad)
Let's say we are playing a game, and we have a light switch.
In every turn, if it's on, there's a 50-50% chance to switch it off, but when it's off, there's 2/3 chance to switch it on.
let turn x = if x == "on" then ["off", "on"] else ["on", "on", "off"]
What happens if we start from an "on" position after one turn?
> ["on"] >>= turn
["off","on"]
The values in the list signify all the probable current states (and not a particular state nor the states that we passed from), starting from the on
state and after one turn.
So, 1/2 cases it was off, 1/2 it was off. Okay.
What about after two turns?
> ["on"] >>= turn >>= turn
["on","on","off","off","on"]
Okay, 2/3 chance to be on, 1/3 off.
> ["on"] >>= turn >>= turn >>= turn
["off","on","off","on","on","on","off","on","on","off","off","on"]
After three turns it's a bit hard to figure out what's going on so, let's build a function for asking the probability of a state.
count = fromIntegral . length :: [a] -> Float
(??) p x = 100 * (count (filter (== x) p)) / (count p)
??
returns the percentage of elements in p
that are equal to x
.
count
is just a wrapper around length that returns a Float instead of an Int.
> (["on"] >>= turn >>= turn >>= turn) ?? "off"
41.666666666666664
> (["on"] >>= turn >>= turn >>= turn) ?? "on"
58.333333333333336
A more interesting example, let's say that we have a pandemic (cough, cough) and everyday there's:
- 25% chance to get sick when healthy
- 17% chance to get hospitalized, 33% chance to recover when sick,
- and 50-50% chance to die or recover when hospitalized
let vprob x = if x == "healthy" then ["healthy", "healthy", "healthy", "sick"] else if x == "sick" then ["sick", "sick", "sick", "hospitalized", "healthy", "healthy"] else if x == "hospitalized" then ["healthy", "dead"] else ["dead"]
What's the probability one will die after 10 days?
> let p = ["healthy"] >>= gen >>= gen >>= gen >>= gen >>= gen >>= gen >>= gen >>= gen >>= gen >>= gen
> p ?? "dead"
2.2962844
Okay, okay, haskell is full of tricks, what do I care. Well, if you understand how this works, you can do this in python too.
We could call >>=
function apply
(haskellers: I know apply is a different function, sssh). Apply takes a function that transforms a list and the list and returns the new list.
The transforming function will take one element from the list and return all future states. So, for each previous state, we get a list of new states, therefore we will flatten the result.
def flatten(l): return sum(l, [])
def apply(p, f): return flatten([f(x) for x in p])
We can know define the previous vprob
probability function:
def vprob(x): return ["healthy", "healthy", "healthy", "sick"] if x == "healthy" else ["sick", "sick", "sick", "hospitalized", "healthy", "healthy"] if x == "sick" else ["healthy", "dead"] if x == "hospitalized" else ["dead"]```
>>> apply(["healthy"], vprob)
['healthy', 'healthy', 'healthy', 'sick']
>>> apply(apply(["healthy"], vprob), vprob)
['healthy', 'healthy', 'healthy', 'sick', 'healthy', 'healthy', 'healthy', 'sick', 'healthy', 'healthy', 'healthy', 'sick', 'sick', 'sick', 'sick', 'hospitalized', 'healthy', 'healthy']
Applying the function multiple times is somewhat ugly in python so we will make a wrapper function apply_times
that recursively calls apply:
def apply_times(p, f, c): return p if c == 0 else apply_times(apply(p, f), f, c-1)
>>> apply_times(["healthy"], vprob, 2)
['healthy', 'healthy', 'healthy', 'sick', 'healthy', 'healthy', 'healthy', 'sick', 'healthy', 'healthy', 'healthy', 'sick', 'sick', 'sick', 'sick', 'hospitalized', 'healthy', 'healthy']
And finally the function for determining the probability of a certain state:
def prob(p, s): return len([x for x in p if x == s])/len(p)
>>> prob(apply_times(["healthy"], gen, 2), "hospitalized")
0.05555555555555555
I tried running the apply_times with 10 parameter but python crashed =))
Postgresql
psql output
Sometimes when using psql, lines are really long.
One option is to enable column wrapping. This will wrap lines in columns keeping the max width of the total columns as long as the terminal total width. To enable:
\pset format wrapped
Another options is to enable extended view. This will show each row column in a separate row. To enable:
\x on
Docs link for all psql options: https://www.postgresql.org/docs/13/app-psql.html
nginx
Resources
Sane default configuration examples
/etc/nginx/sites-available/example-static.com.conf
for fully static website
- Two
server
directives, one SSL, one non-SSL. - non-SSL redirects everything except for Let's Encrypt challenge
(at
/.well-known/acme-challenge/
) to the SSLserver
. - SSL cert and key are located in default certbot
location:
/etc/letsencrypt/live/<website-name>/fullchain.pem
. - HTTP2 enabled.
- Some security headers enabled.
- No dotfiles serving.
- Separate log files for each
server
.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name wiki.venn.dev;
root /var/www/wiki.venn.dev/html;
# SSL
ssl_certificate /etc/letsencrypt/live/wiki.venn.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wiki.venn.dev/privkey.pem;
# security
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# dotfiles
location ~ /\.(?!well-known) {
deny all;
}
# restrict methods
if ($request_method !~ ^(GET|HEAD|CONNECT|OPTIONS|TRACE)$) {
return '405';
}
# logging
access_log /var/log/nginx/wiki.venn.dev.access.log;
error_log /var/log/nginx/wiki.venn.dev.error.log warn;
# favicon.ico
location = /favicon.ico {
log_not_found off;
access_log off;
}
# robots.txt
location = /robots.txt {
log_not_found off;
access_log off;
}
# assets, media
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm)$ {
expires 7d;
access_log off;
}
# svg, fonts
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
add_header Access-Control-Allow-Origin "*";
expires 7d;
access_log off;
}
}
server {
listen 80;
listen [::]:80;
server_name wiki.venn.dev;
location ^~ /.well-known/acme-challenge/ {
root /var/www/_letsencrypt;
}
location / {
return 301 https://wiki.venn.dev$request_uri;
}
}
/etc/nginx/sites-available/example-uwsgi-django.com.conf
for Django + uWSGI
- Two
server
directives, one SSL, one non-SSL. - non-SSL redirects everything except for Let's Encrypt challenge
(at
/.well-known/acme-challenge/
) to the SSLserver
. - SSL cert and key are located in default certbot
location:
/etc/letsencrypt/live/<website-name>/fullchain.pem
. - HTTP2 enabled.
- Some security headers enabled along with HSTS.
- No dotfiles serving.
- Separate log files for each
server
. - uWSGI configuration using unix sockets (nginx docs, uwsgi docs).
- Configured for serving Django static files. See here for how to configure Django for static files.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name venn.dev;
# deny requests with invalid host header
if ( $host !~* ^(venn.dev)$ ) {return 444;}
# SSL
ssl_certificate /etc/letsencrypt/live/venn.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/venn.dev/privkey.pem;
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# . files
location ~ /\.(?!well-known) {
deny all;
}
# logging
access_log /var/log/nginx/venn.dev.access.log;
error_log /var/log/nginx/venn.dev.error.log warn;
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/venn.sock;
uwsgi_param Host $host;
uwsgi_param X-Real-IP $remote_addr;
uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
}
# Django static
location /static/ {
alias /opt/apps/venn/static/;
}
# favicon.ico
location = /favicon.ico {
log_not_found off;
access_log off;
}
# robots.txt
location = /robots.txt {
log_not_found off;
access_log off;
}
}
server {
listen 80;
listen [::]:80;
server_name venn.dev;
# ACME-challenge
location ^~ /.well-known/acme-challenge/ {
root /var/www/_letsencrypt;
}
location / {
return 301 https://venn.dev$request_uri;
}
}
/etc/nginx/nginx.conf
user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;
include /etc/nginx/modules-enabled/*.conf;
events {
multi_accept on;
worker_connections 768;
}
http {
charset uf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
client_max_body_size 16M;
keepalive_timeout 65;
server_names_hash_bucket_size 64;
server_name_in_redirect off;
# MIME
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Virtual Host Configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Django
Static files
Django is too cool to serve static files.
We need either something like
whitenoise which
in the background
uses the kernel's sendfile
syscall.
Or, we can serve them above Django, from our web server / reverse proxy, if we have one (eg. nginx). See nginx for related configuration examples.
In both cases, one needs to have these two lines in their settings.py
:
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL
is included in the Django project generation. STATIC_ROOT
needs
to be added manually.
Django docs how-to guide on static files here and here.
Also, enabling manifest static files is usually a good idea for high-quality
cache busting. To do this, add this line as well in your settings.py
.
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
Git
Clone with non-default SSH key
Maybe you need a specific SSH key to clone from GitHub, and maybe you don’t want to add it as default. One can specify which SSH command Git will use to do git operations.
Given my ssh key at /Users/sirodoht/.ssh/id_ed25519_debug
, here is an
example:
GIT_SSH_COMMAND='ssh -i /Users/sirodoht/.ssh/id_ed25519_debug -o IdentitiesOnly=yes' git clone git@github.com:debug-org/api.git