The Old Man and the Sea

I really resonated with the story of the old man. He hasn't caught a fish in 84 days, and yet he remains hopeful and determined. There is sense of honesty and pride in his work. Despite his failings, he continues in his pursuit and work. And, finally he catches a gaint marlin, the biggest dolphin that he has ever seen. He struggles with the fish for three days, barely eating or sleeping, and finally succeeds to take down the biggest fish in his life. He is now filled with enthusiasm. However, on his way back to the shore, the fish gets attacked by the sharks. He encounters many sharks and fights them off, but slowly with, each fight, he loses more and more of the fish and also the resources he has at his disposal to fight these sharks off. But, despite all this, fully knowing he has been defeated, he still manages to will himself. He almost feels like it is his duty to put the fight. He doesn't hold any grudges against the sharks either.

I have struggled in my life, especially for the last two years. It is so difficult to keep going, and do the same job with that same level of dignity and sincerity. But, this story tells us, we have no option but to put up that fight. I may very well not succeed, and hopelessly fail, but nobody can take the pride in my struggle for my life.

Full text search in postgres

I just learnt about tsvector and tsquery in Postgres, which are used for full-text search.

This gives me a tsvector document that I can then use to search against.

SELECT 
       to_tsvector(post.title) ||
       to_tsvector(post.content) ||
       to_tsvector(author.name) ||
       to_tsvector(coalesce((string_agg(tag.name, ' ')), '')) as document
FROM post
JOIN author on author.id = post.author_id
JOIN posts_tags on posts_tags.post_id = post.id
JOIN tag on tag.id = posts_tags.tag_id
GROUP BY post.id, author.id;

Then I can use the @@ operator to match a tsquery against the tsvector.

SELECT pid, p_title
FROM (
    SELECT 
           post.id as pid,
           post.title as p_title,
           to_tsvector(post.title) ||
           to_tsvector(post.content) ||
           to_tsvector(author.name) ||
           to_tsvector(coalesce((string_agg(tag.name, ' ')), '')) 
    as document FROM post
    JOIN author on author.id = post.author_id
    JOIN posts_tags on posts_tags.post_id = post.id
    JOIN tag on tag.id = posts_tags.tag_id
    GROUP BY post.id, author.id
) p_search
WHERE p_search.document @@ to_tsquery('gym & work');

There is also language support, but I didn't look much into that.

Another feature is ranking the results using ts_rank function and settings weights to different parts of the document.

SELECT pid, p_title
FROM (
    SELECT 
           post.id as pid,
           post.title as p_title,
           setweight(to_tsvector(post.title), 'A') ||
           setweight(to_tsvector(post.content), 'B') ||
           setweight(to_tsvector(author.name), 'C') ||
           setweight(to_tsvector(coalesce((string_agg(tag.name, ' ')), '')), 'B')
    as document FROM post
    JOIN author on author.id = post.author_id
    JOIN posts_tags on posts_tags.post_id = post.id
    JOIN tag on tag.id = posts_tags.tag_id
    GROUP BY post.id, author.id
) p_search
WHERE p_search.document @@ to_tsquery('gym & work');
ORDER BY ts_rank(p_search.document, to_tsquery('gym & work')) DESC;

Automated Deployment with Git Post-Receive Hook

This Git post-receive hook automates the deployment of a Django application on a remote server. Upon pushing code to the bare repository, the hook:

  1. Checks out the latest code to the application directory.
  2. Restarts the Docker Compose services, rebuilding images if needed and removing orphan containers.
  3. Runs Django management commands to collect static files and apply database migrations.

This ensures that the server is always up-to-date with the latest code changes, streamlining deployments and reducing manual steps.

#!/bin/bash
set -e
APP_DIR=/your/app/dir
BARE_REPO=/your/bare/repo
echo "Deployment latest code ..." 
if ! git --work-tree=$APP_DIR --git-dir=$BARE_REPO checkout -f; then 
    echo "❌ Git checkout failed. Deployment aborted."
    exit 1
fi

cd $APP_DIR
echo "Restarting docker compose ..."

sudo docker compose down
sudo docker compose up --build --detach --remove-orphans
sudo docker compose run web python manage.py collectstatic --no-input
sudo docker compose run web python manage.py migrate 
echo "✅ Deployment finished!"

Using docker override to ease your development

I didn't know you could this. What I normally do when I have a service that that has its own configuration for each environment, say for instance, web and web-dev, I run the separate service instance for each environment and tag each of them as seperate profiles. And, on .env file, i specify the profiles that I need.

But, dev workflow is always changing and you don't want to clutter your compose.yml file. Also, these two services aren't seperate instance, and they share mostly the same configuration. So, with override this is a lot cleaner.

So, the way I am using it right now is to have a production configuration in compose.yml. And, whatever, I need for development, I put it in the compose.override.yml. And, I also don't have it in the version control.

So, for example, this is one the web service in compose.yml

services:
  web:
  build: .
  command: gunicorn config.wsgi:application --bind 0.0.0.0:${PORT:-4567}
  restart: always
  env_file:
    - .env
  ports:
    - "${PORT:-4567}:${PORT:-4567}"
  volumes:
    - ${STATIC_ROOT}:/app/staticfiles
  depends_on:
    - db

And, this is the override in compose.override.yml

services:
  web:
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
    volumes:
      - .:/app

Golang Chat Server

Simple proof of concept chat server

I found it relatively easy to create a simple terminal chat server. To interact, I am using telnet to connect to the server.

The idea is very simple. In our main thread, we are listening to incoming TCP connections. So, when telnet connects to our server, we create a separate goroutine handleConnection to handle that connection.

In handleConnection, we first ask the user for their name, and then create a client object and register it with the server. After that, we enter a loop where we continuously read messages from the client, and once the message is received, we broadcast it to all other connected clients.

The server maintains a list of connected clients and uses channels to handle the registration, unregistration, and broadcasting of messages. The run method of the server listens for these events and processes them accordingly.

Full go code:


package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

type Client struct {
    conn net.Conn
    name string
}

type Message struct {
    sender net.Conn
    text   string
}

type Server struct {
    clients    map[net.Conn]Client
    register   chan Client
    unregister chan Client
    broadcast  chan Message
}

func (s *Server) run() {
    for {
        select {
        case message := <-s.broadcast:
            for conn, client := range s.clients {
                if conn != message.sender {
                    fmt.Fprintln(client.conn, message.text)
                }
            }
            fmt.Println(message.text)

        case client := <-s.register:
            s.clients[client.conn] = client
            text := fmt.Sprintf("%s joined the chat", client.name)
            s.broadcast <- Message{sender: client.conn, text: text}
            fmt.Printf("%s connected\n", client.name)

        case client := <-s.unregister:
            if client, ok := s.clients[client.conn]; ok {
                delete(s.clients, client.conn)
                text := fmt.Sprintf("%s left the chat", client.name)
                s.broadcast <- Message{sender: client.conn, text: text}
                fmt.Printf("%s disconnected\n", client.name)
            }
        }
    }
}

func (s *Server) handleConnection(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    fmt.Fprint(conn, "Enter your name: ")
    name, _ := reader.ReadString('\n')
    name = strings.TrimSpace(name)

    client := Client{conn: conn, name: name}
    s.register <- client

    for {
        message, err := reader.ReadString('\n')
        if err != nil {
            break
        }
        message = strings.TrimSpace(message)
        if message == "" {
            continue
        }
        fullMsg := fmt.Sprintf("%s: %s", name, message)
        s.broadcast <- Message{sender: conn, text: fullMsg}
    }
    s.unregister <- client
}

func NewServer() *Server {
    return &Server{
        clients:    make(map[net.Conn]Client),
        register:   make(chan Client),
        unregister: make(chan Client),
        broadcast:  make(chan Message, 1),
    }
}

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    server := NewServer()

    go server.run()
    fmt.Println("Chat server started on :8080")
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("connection error:", err)
            continue
        }
        go server.handleConnection(conn)
    }
}

Simon Willison

I am blown away by this man. How could I have not heard of him before?

He has over 2000 blog posts in his website. Imagine, the sheer consitency and just the amount of knowledge he has. It is like a avalance of information hitting me all at once.

Datasette from the first impression looks so cool. I will definitely have to try it out.

Also, one thing that I noticed in his blog site was that all the views methods were function based views. Just love that, I feel kind of reassured that I was doing something right.

Also, I have had in my mind to write blogs for the past two years but just never got around to it. I think I will start writing a blog now.

You should start a blog. Having your own little corner of the internet is good for the soul!