<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Panos Matsinopoulos</title>
        <link>https://paragraph.com/@panagiotismatsinopoulos</link>
        <description>Humble Software Reader, Writer and DevOps Builder, with love for Classical Music.</description>
        <lastBuildDate>Sun, 19 Jul 2026 23:24:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Panos Matsinopoulos</title>
            <url>https://storage.googleapis.com/papyrus_images/54a3739e389e93a0bc759634f15270d630398f91d65365768d89fc7c74d07757.jpg</url>
            <link>https://paragraph.com/@panagiotismatsinopoulos</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Setting Up TimescaleDB on AWS]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/setting-up-timescaledb-on-aws</link>
            <guid>nECKmjDvnAfjzCnNE8wA</guid>
            <pubDate>Sun, 13 Jul 2025 10:25:59 GMT</pubDate>
            <description><![CDATA[TimescaleDB is a PostgreSQL extension for high-performance real-time analytics on time-series and event data. It is basically a custom Postgres. Important: This post uses Terraform to create and destroy resources on an AWS account. Make sure that you destroy the resources as soon and as long as you don’t need them any more. Otherwise, you will be charged more than you might want.How do you set up a TimescaleDB on AWS?Unfortunately, you can’t use, at the time of this writing (Jul 2025), the ma...]]></description>
            <content:encoded><![CDATA[<p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/timescale/timescaledb">TimescaleDB</a> is a PostgreSQL extension for high-performance real-time analytics on time-series and event data.</p><p>It is basically a <em>custom</em> Postgres.</p><p><strong>Important:</strong> This post uses <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform">Terraform</a> to create and destroy resources on an AWS account. Make sure that you destroy the resources as soon and as long as you don’t need them any more. Otherwise, you will be charged more than you might want.</p><h2 id="h-how-do-you-set-up-a-timescaledb-on-aws" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">How do you set up a TimescaleDB on AWS?</h2><p>Unfortunately, you can’t use, at the time of this writing (Jul 2025), the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://aws.amazon.com/rds/">managed AWS RDS Service</a>. Neither the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-custom.html">AWS RDS Custom Service</a>.</p><h3 id="h-subscribe-to-hosted-timescaledb-services" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Subscribe to Hosted TimescaleDB Services</h3><p>If you want to use TimescaleDB you have some options by companies that offer it as a hosting service.</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.tigerdata.com/cloud">TigerData Cloud</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://aiven.io">Aiven.io</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://scalegrid.io/">ScaleGrid</a></p></li><li><p>and more</p></li></ul><h3 id="h-set-it-up-manually-on-your-aws-account" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Set it Up Manually on your AWS Account</h3><p>This requires that you are more competent in terms of devops, but it allows you more flexibility.</p><p>This is the option I am going to go with in this blog post.</p><p>Here is an architecture diagram for the demo project I will develop to demonstrate how I can set up TimescaleDB on AWS.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0d6a23a03104c398ea4c7e1c4aff3eff64a1ea630c9a5b8cb2a8b92621b84a15.png" alt="Architecture Diagram - Hosting TimescaleDB on AWS" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Architecture Diagram - Hosting TimescaleDB on AWS</figcaption></figure><ul><li><p>I will need access to the EC2 machine in order to install TimescaleDB Server. That’s why the SSH access on port 22. That’s the easiest way to install the software on the EC2 machine. In subsequent blog posts, I will present other ways to install third-party software like TimescaleDB without requiring ssh access.</p></li><li><p>I will need access to the database server via <code>psql</code> from a remote machine. That’s why I will need to open the port <code>5432</code>. However, again, if it were a real production environment, it would make it more secure if direct access to the database was only possible from within the VPC and not from the public internet. This is another topic for another blog post.</p></li></ul><h2 id="h-timescaledb-on-my-aws-account" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">TimescaleDB on my AWS Account</h2><h3 id="h-project-setup" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Project Setup</h3><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws">Source code here</a>:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws</a></p><p><strong>Third-party Tools</strong></p><p>These are the tools and their version at the time of writing: (BTW, I am using <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://asdf-vm.com/">asdf</a> as my third-party tools version manager)</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform">Terraform</a>, version 1.12.2</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://direnv.net/">direnv</a>, version 2.35.0</p></li></ul><p><strong>Git - Don’t Go Without</strong></p><p>I don’t go without git. I also create a <code>.gitignore</code> file. Initially, I have the following folders and files ignored:</p><pre data-type="codeBlock" text=".env
.env.*
.envrc
*.pem
*.pem.pub

terraform/.terraform
*.tfstate
*.tfstate.backup
"><code>.env
.env.*
.envrc
<span class="hljs-operator">*</span>.pem
<span class="hljs-operator">*</span>.pem.pub

terraform<span class="hljs-operator">/</span>.terraform
<span class="hljs-operator">*</span>.tfstate
<span class="hljs-operator">*</span>.tfstate.backup
</code></pre><p><strong>Folder For Terraform Files</strong></p><p>I like to create the folder <code>terraform</code> and put everything inside</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ba676c748c7b550aca89a4685dfb5502c053c33c4a3058bd0a8c69b5fb3325ff.png" alt="\`" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">\`</figcaption></figure><p>So, all the Terraform related files will be inside this <code>terraform</code> sub-folder.</p><p>Also, when I will be telling you about shell commands that I execute, like <code>terraform validate</code>, they will be executed from within this sub-folder, which I will indicate with the <code>terraform &gt; </code>prefix at the beginning.</p><p><strong>Main Terraform Setup</strong></p><p>The <code>main.tf</code> file declares the version of Terraform and AWS Provider I will work with.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/main.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/main.tf</a></p><pre data-type="codeBlock" text="terraform {
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;6.0.0&quot;
    }
  }

  required_version = &quot;~&gt;1.12.0&quot;
}
"><code>terraform {
  required_providers {
    <span class="hljs-attr">aws</span> = {
      <span class="hljs-attr">source</span>  = <span class="hljs-string">"hashicorp/aws"</span>
      <span class="hljs-attr">version</span> = <span class="hljs-string">"6.0.0"</span>
    }
  }

  <span class="hljs-attr">required_version</span> = <span class="hljs-string">"~>1.12.0"</span>
}
</code></pre><p><strong>Backend To Save Terraform State</strong></p><p>I will leave the default backend (<code>local</code>). However, you might want to use other <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform/language/backend">backends</a> in your own projects.</p><p>I define the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/backend.tf">backend inside the file</a> <code>backend.tf</code></p><pre data-type="codeBlock" text="terraform {
  backend &quot;local&quot; {
    path = &quot;terraform.tfstate&quot;
  }
}
"><code>terraform {
  backend "local" {
    <span class="hljs-attr">path</span> = <span class="hljs-string">"terraform.tfstate"</span>
  }
}
</code></pre><p><strong>Initialize</strong></p><p>With the <code>main.tf</code> and <code>backend.tf</code> in place, I fire the <code>terraform init</code>.</p><pre data-type="codeBlock" text="terraform &gt; terraform init
"><code>terraform > terraform <span class="hljs-keyword">init</span>
</code></pre><h3 id="h-aws-region" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">AWS Region</h3><p>I will use Europe/Frankfurt, which is <code>eu-central-1</code>. I will set it as default value for the variable <code>region</code> inside <code>terraform/variables.tf</code> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/variables.tf">file</a>. But since this is declared as input variable, you can pass whichever value you want.</p><h3 id="h-aws-provider" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">AWS Provider</h3><p>I set up the AWS provider configuration as follows (inside the file <code>providers.tf</code>):</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/providers.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/providers.tf</a></p><pre data-type="codeBlock" text="# Without +alias+ this is the default +aws+ provider
#
provider &quot;aws&quot; {
  region = var.region
  default_tags {
    tags = {
      project     = var.project
      terraform   = &quot;1&quot;
      environment = var.environment
      tf_repo     = var.repository_url
      tf_folder   = &quot;terraform/${var.environment}&quot;
    }
  }
}
"><code># Without <span class="hljs-operator">+</span>alias<span class="hljs-operator">+</span> <span class="hljs-built_in">this</span> <span class="hljs-keyword">is</span> the default <span class="hljs-operator">+</span>aws<span class="hljs-operator">+</span> provider
#
provider <span class="hljs-string">"aws"</span> {
  region <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.region
  default_tags {
    tags <span class="hljs-operator">=</span> {
      project     <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.project
      terraform   <span class="hljs-operator">=</span> <span class="hljs-string">"1"</span>
      environment <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.environment
      tf_repo     <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.repository_url
      tf_folder   <span class="hljs-operator">=</span> <span class="hljs-string">"terraform/${var.environment}"</span>
    }
  }
}
</code></pre><p>This introduces the following three input variables:</p><ul><li><p><code>project</code></p></li><li><p><code>environment</code></p></li><li><p><code>repository_url</code></p></li></ul><p>which I have to declare inside the <code>variables.tf</code> file. I will also set some default values. Set your own values to match your settings:</p><pre data-type="codeBlock" text="variable &quot;project&quot; {
  description = &quot;The name of the project.&quot;
  type        = string
  default     = &quot;setting_up_timescaledb_on_aws&quot;
}

variable &quot;environment&quot; {
  description = &quot;The environment for the deployment (e.g., development, staging, production).&quot;
  type        = string
  default     = &quot;development&quot;
}

variable &quot;repository_url&quot; {
  description = &quot;The GitHub repository URL for the project.&quot;
  type        = string
  default     = &quot;https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws&quot;
}
"><code>variable "project" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The name of the project."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">default</span>     = <span class="hljs-string">"setting_up_timescaledb_on_aws"</span>
}

variable "environment" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The environment for the deployment (e.g., development, staging, production)."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">default</span>     = <span class="hljs-string">"development"</span>
}

variable "repository_url" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The GitHub repository URL for the project."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">default</span>     = <span class="hljs-string">"https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws"</span>
}
</code></pre><h3 id="h-vpc" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">VPC</h3><p>I will use the default VPC in the region selected. There is always a default VPC. It has plenty of IP addresses (172.31.0.0/16 - 65,536 addresses).</p><p>Since the VPC already exists and I will not create it, I just need to create a data source reference. I will put that inside the <code>data.tf</code> file:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/data.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/data.tf</a></p><pre data-type="codeBlock" text="data &quot;aws_vpc&quot; &quot;timescaledb_vpc&quot; {
  id = var.vpc_id
}
"><code>data <span class="hljs-string">"aws_vpc"</span> <span class="hljs-string">"timescaledb_vpc"</span> {
  id <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.vpc_id
}
</code></pre><p>I am using an input variable to reference the VPC by its id. Which means that I need the input variable definition inside the <code>variables.tf</code> file:</p><pre data-type="codeBlock" text="variable &quot;vpc_id&quot; {
  description = &quot;The ID of the VPC where the EC2 instance will be launched in.&quot;
  type        = string
}
"><code>variable <span class="hljs-string">"vpc_id"</span> {
  description <span class="hljs-operator">=</span> <span class="hljs-string">"The ID of the VPC where the EC2 instance will be launched in."</span>
  <span class="hljs-keyword">type</span>        <span class="hljs-operator">=</span> <span class="hljs-keyword">string</span>
}
</code></pre><h3 id="h-vpc-subnet" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">VPC - Subnet</h3><p>The default VPC in each region is coming with 3 default subnets each one created in one of the Availability Zones:</p><ul><li><p><code>eu-central-1a</code></p></li><li><p><code>eu-central-1b</code></p></li><li><p><code>eu-central-1c</code></p></li></ul><p>I will use the subnet deployed into <code>eu-central-1a</code>. I will pass its id via a variable</p><pre data-type="codeBlock" text="variable &quot;subnet_id&quot; {
  description = &quot;The ID of the subnet where the EC2 instance will be launched in.&quot;
  type        = string
}
"><code>variable <span class="hljs-string">"subnet_id"</span> {
  description <span class="hljs-operator">=</span> <span class="hljs-string">"The ID of the subnet where the EC2 instance will be launched in."</span>
  <span class="hljs-keyword">type</span>        <span class="hljs-operator">=</span> <span class="hljs-keyword">string</span>
}
</code></pre><p>I declare the data source for the subnet inside the <code>data.tf</code> file, in which I declare resources that I reference rather than create.</p><pre data-type="codeBlock" text="data &quot;aws_subnet&quot; &quot;timescaledb_subnet&quot; {
  id = var.subnet_id
}
"><code>data <span class="hljs-string">"aws_subnet"</span> <span class="hljs-string">"timescaledb_subnet"</span> {
  id <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.subnet_id
}
</code></pre><h3 id="h-ssh-access-security-group" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">SSH Access Security Group</h3><p>I will need two security groups, one to allow incoming SSH traffic on port <code>22</code>. And another one for postgres traffic on port <code>5432</code>.</p><p>Here is how I set up the SSH one. I write its configuration inside the file <code>security_groups.tf</code>.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/security_groups.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/security_groups.tf</a></p><pre data-type="codeBlock" text="resource &quot;aws_security_group&quot; &quot;ssh&quot; {
  name        = &quot;${var.project}-${var.environment}-security-group-ssh&quot;
  description = &quot;Allow SSH traffic from anywhere to anywhere&quot;
  vpc_id      = data.aws_vpc.timescaledb_vpc.id
  ingress {
    description = &quot;SSH from anywhere&quot;
    from_port   = 22
    to_port     = 22
    protocol    = &quot;tcp&quot;
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
  }
  tags = {
    &quot;Name&quot; = &quot;${var.project}-${var.environment}-security-group-ssh&quot;
  }
}
"><code>resource "aws_security_group" "ssh" {
  <span class="hljs-attr">name</span>        = <span class="hljs-string">"${var.project}-${var.environment}-security-group-ssh"</span>
  <span class="hljs-attr">description</span> = <span class="hljs-string">"Allow SSH traffic from anywhere to anywhere"</span>
  <span class="hljs-attr">vpc_id</span>      = data.aws_vpc.timescaledb_vpc.id
  ingress {
    <span class="hljs-attr">description</span> = <span class="hljs-string">"SSH from anywhere"</span>
    <span class="hljs-attr">from_port</span>   = <span class="hljs-number">22</span>
    <span class="hljs-attr">to_port</span>     = <span class="hljs-number">22</span>
    <span class="hljs-attr">protocol</span>    = <span class="hljs-string">"tcp"</span>
    <span class="hljs-attr">cidr_blocks</span> = [<span class="hljs-string">"0.0.0.0/0"</span>]
  }
  <span class="hljs-attr">tags</span> = {
    <span class="hljs-attr">"Name"</span> = <span class="hljs-string">"${var.project}-${var.environment}-security-group-ssh"</span>
  }
}
</code></pre><p>This is opening the incoming traffic from any IP address over the port 22.</p><h3 id="h-postgres-access-security-group" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Postgres Access Security Group</h3><p>In the same file, I create another security group that will allow me postgres traffic over the port <code>5432</code>.</p><pre data-type="codeBlock" text="resource &quot;aws_security_group&quot; &quot;postgres&quot; {
  name        = &quot;${var.project}-${var.environment}-security-group-postgres&quot;
  description = &quot;Allow postgres traffic from anywhere&quot;
  vpc_id      = data.aws_vpc.timescaledb_vpc.id
  ingress {
    description = &quot;postgres from anywhere&quot;
    from_port   = 5432
    to_port     = 5432
    protocol    = &quot;tcp&quot;
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
  }
  tags = {
    &quot;Name&quot; = &quot;${var.project}-${var.environment}-security-group-postgres&quot;
  }
}
"><code>resource "aws_security_group" "postgres" {
  <span class="hljs-attr">name</span>        = <span class="hljs-string">"${var.project}-${var.environment}-security-group-postgres"</span>
  <span class="hljs-attr">description</span> = <span class="hljs-string">"Allow postgres traffic from anywhere"</span>
  <span class="hljs-attr">vpc_id</span>      = data.aws_vpc.timescaledb_vpc.id
  ingress {
    <span class="hljs-attr">description</span> = <span class="hljs-string">"postgres from anywhere"</span>
    <span class="hljs-attr">from_port</span>   = <span class="hljs-number">5432</span>
    <span class="hljs-attr">to_port</span>     = <span class="hljs-number">5432</span>
    <span class="hljs-attr">protocol</span>    = <span class="hljs-string">"tcp"</span>
    <span class="hljs-attr">cidr_blocks</span> = [<span class="hljs-string">"0.0.0.0/0"</span>]
  }
  <span class="hljs-attr">tags</span> = {
    <span class="hljs-attr">"Name"</span> = <span class="hljs-string">"${var.project}-${var.environment}-security-group-postgres"</span>
  }
}
</code></pre><h3 id="h-ssh-key-pair" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">SSH Key Pair</h3><p>I will need to create an ssh key pair (private-public) and upload the public part of it on AWS key pairs.</p><p>I use <code>ssh-keygen</code> to create the key pair. But you can use any method described <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html">here</a>. Please, note that the EC2 instance that I will create to install the TimescaleDB server is going to be a Linux/Ubuntu server. Hence, I follow the instructions to create an ssh key pair that is compatible with Linux/Ubuntu.</p><p>This is how I generate the key on my Linux client:</p><pre data-type="codeBlock" text="ssh-keygen -m PEM -f timescaledb.pem
"><code>ssh<span class="hljs-operator">-</span>keygen <span class="hljs-operator">-</span>m PEM <span class="hljs-operator">-</span>f timescaledb.pem
</code></pre><p>Note that when I am being asked for a passphrase, I leave it empty.</p><p>This generates two files locally:</p><ul><li><p><code>timescaledb.pem</code></p></li><li><p><code>timescaledb.pem.pub</code></p></li></ul><p><strong>Important:</strong> The <code>.gitignore</code> file should include the entry <code>*.pem</code> to make sure the private part of the key is not checked in.</p><p><strong>Important:</strong> I use the following command to change the access mode of the private key file. This will make sure that the file is only readable by me.</p><pre data-type="codeBlock" text="terraform &gt; chmod 400 &quot;timescaledb.pem&quot;
"><code>terraform > <span class="hljs-built_in">chmod</span> 400 <span class="hljs-string">"timescaledb.pem"</span>
</code></pre><p>Now I need to declare the resource so that the public key is uploaded to AWS key pairs. I do that inside the file <code>ec2_key_pair.tf</code></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_key_pair.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_key_pair.tf</a></p><pre data-type="codeBlock" text="# This imports the public part of an OpenSSH key.
# The key has been generated using `ssh-keygen` in PEM format.
# Example:
#
# ssh-keygen -m PEM
#
# The key has been generated without a passphrase.
#
resource &quot;aws_key_pair&quot; &quot;timescaledb&quot; {
  key_name   = &quot;${var.project}-${var.environment}-timescaledb&quot;
  public_key = file(&quot;${path.module}/timescaledb.pem.pub&quot;)

  tags = {
    &quot;Name&quot; = &quot;${var.project}-${var.environment}-timescaledb&quot;
  }
}
"><code><span class="hljs-comment"># This imports the public part of an OpenSSH key.</span>
<span class="hljs-comment"># The key has been generated using `ssh-keygen` in PEM format.</span>
<span class="hljs-comment"># Example:</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment"># ssh-keygen -m PEM</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment"># The key has been generated without a passphrase.</span>
<span class="hljs-comment">#</span>
resource <span class="hljs-string">"aws_key_pair"</span> <span class="hljs-string">"timescaledb"</span> {
  key_name   = <span class="hljs-string">"<span class="hljs-variable">${var.project}</span>-<span class="hljs-variable">${var.environment}</span>-timescaledb"</span>
  public_key = file(<span class="hljs-string">"<span class="hljs-variable">${path.module}</span>/timescaledb.pem.pub"</span>)

  tags = {
    <span class="hljs-string">"Name"</span> = <span class="hljs-string">"<span class="hljs-variable">${var.project}</span>-<span class="hljs-variable">${var.environment}</span>-timescaledb"</span>
  }
}
</code></pre><h3 id="h-ec2-instance" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">EC2 Instance</h3><p>I am now ready to code for the EC2 instance resource that I will use to run my TimescaleDB server.</p><p>I write this inside the file <code>ec2_instance.tf</code>.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_instance.tf">https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_instance.tf</a></p><pre data-type="codeBlock" text="data &quot;aws_ami&quot; &quot;timescaledb&quot; {
  most_recent = true

  filter {
    name   = &quot;block-device-mapping.delete-on-termination&quot;
    values = [&quot;true&quot;]
  }

  filter {
    name   = &quot;is-public&quot;
    values = [&quot;true&quot;]
  }


  filter {
    name   = &quot;name&quot;
    values = [&quot;ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server*&quot;]
  }

  filter {
    name   = &quot;root-device-type&quot;
    values = [&quot;ebs&quot;]
  }

  owners = [&quot;099720109477&quot;] # Canonical
}

resource &quot;aws_instance&quot; &quot;timescaledb&quot; {
  ami               = data.aws_ami.timescaledb.id
  instance_type     = var.timescaledb_server_instance_type
  availability_zone = &quot;${var.region}${var.timescaledb_server_availability_zone}&quot;
  subnet_id         = var.subnet_id
  vpc_security_group_ids = [
    data.aws_security_group.timescaledb_vpc_default_security_group.id,
    aws_security_group.ssh.id,
    aws_security_group.postgres.id,
  ]
  key_name                    = aws_key_pair.timescaledb.key_name
  associate_public_ip_address = true

  tags = {
    &quot;Name&quot; = &quot;${var.project}-${var.environment}-timescaledb&quot;
  }
}

output &quot;ec2_timescaledb_public_ip&quot; {
  value       = aws_instance.timescaledb.public_ip
  description = &quot;The public IP address of the EC2 Timescale DB Machine&quot;
}

output &quot;ssh_connect&quot; {
  value       = &quot;ssh -i ${path.module}/timescaledb.pem -o IdentitiesOnly=yes ubuntu@${aws_instance.timescaledb.public_ip}&quot;
  description = &quot;SSH command to connect to the Timescale DB EC2 instance&quot;
}
"><code>data <span class="hljs-string">"aws_ami"</span> <span class="hljs-string">"timescaledb"</span> {
  most_recent <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>

  filter {
    name   <span class="hljs-operator">=</span> <span class="hljs-string">"block-device-mapping.delete-on-termination"</span>
    values <span class="hljs-operator">=</span> [<span class="hljs-string">"true"</span>]
  }

  filter {
    name   <span class="hljs-operator">=</span> <span class="hljs-string">"is-public"</span>
    values <span class="hljs-operator">=</span> [<span class="hljs-string">"true"</span>]
  }


  filter {
    name   <span class="hljs-operator">=</span> <span class="hljs-string">"name"</span>
    values <span class="hljs-operator">=</span> [<span class="hljs-string">"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server*"</span>]
  }

  filter {
    name   <span class="hljs-operator">=</span> <span class="hljs-string">"root-device-type"</span>
    values <span class="hljs-operator">=</span> [<span class="hljs-string">"ebs"</span>]
  }

  owners <span class="hljs-operator">=</span> [<span class="hljs-string">"099720109477"</span>] # Canonical
}

resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"timescaledb"</span> {
  ami               <span class="hljs-operator">=</span> data.aws_ami.timescaledb.id
  instance_type     <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.timescaledb_server_instance_type
  availability_zone <span class="hljs-operator">=</span> <span class="hljs-string">"${var.region}${var.timescaledb_server_availability_zone}"</span>
  subnet_id         <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.subnet_id
  vpc_security_group_ids <span class="hljs-operator">=</span> [
    data.aws_security_group.timescaledb_vpc_default_security_group.id,
    aws_security_group.ssh.id,
    aws_security_group.postgres.id,
  ]
  key_name                    <span class="hljs-operator">=</span> aws_key_pair.timescaledb.key_name
  associate_public_ip_address <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>

  tags <span class="hljs-operator">=</span> {
    <span class="hljs-string">"Name"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"${var.project}-${var.environment}-timescaledb"</span>
  }
}

output <span class="hljs-string">"ec2_timescaledb_public_ip"</span> {
  value       <span class="hljs-operator">=</span> aws_instance.timescaledb.public_ip
  description <span class="hljs-operator">=</span> <span class="hljs-string">"The public IP address of the EC2 Timescale DB Machine"</span>
}

output <span class="hljs-string">"ssh_connect"</span> {
  value       <span class="hljs-operator">=</span> <span class="hljs-string">"ssh -i ${path.module}/timescaledb.pem -o IdentitiesOnly=yes ubuntu@${aws_instance.timescaledb.public_ip}"</span>
  description <span class="hljs-operator">=</span> <span class="hljs-string">"SSH command to connect to the Timescale DB EC2 instance"</span>
}
</code></pre><p>The <code>data “aws_ami” “timescaledb” { … }</code> block is used to select the AMI that will be used to launch the EC2 instance described in the following <code>resource “aws_instance” “timescaledb” { … }</code></p><p>You can see how the <code>resource</code> block is referencing the <code>data</code> block:</p><pre data-type="codeBlock" text="...
ami               = data.aws_ami.timescaledb.id
...
"><code>...
ami               <span class="hljs-operator">=</span> data.aws_ami.timescaledb.id
...
</code></pre><p>Inside the <code>resource</code> block, I am setting the very minimum properties so that can I launch the EC2 instance as I want it.</p><p><strong>Outputs:</strong></p><p>I also make sure that I export two outputs:</p><ul><li><p><code>ec2_timescaledb_public_ip</code>: the public IP of the EC2 machine.</p></li><li><p><code>ssh_connect</code>: the command to use locally if I want to SSH to the EC2 machine.</p></li></ul><h3 id="h-values-to-terraform-input-variables" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Values to <code>terraform</code> Input Variables</h3><p>Most of the <code>terraform</code> commands that I will need in this project will require me to have a method to pass values to the input variables that they don’t have a default value.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform/language/values/variables#assigning-values-to-root-module-variables">There are plenty of ways you can pass values to Terraform input variables</a>. For this particular demo, I prefer the method of <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform/language/values/variables#environment-variables">environment variables</a>.</p><p>So, I make sure that the terminal shell that I use to execute the <code>terraform</code> commands is set with correct values for the environment variables that <code>terraform</code> needs.</p><p>The tool that is helping me do that is <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://direnv.net/">direnv</a>. I have created the <code>terraform/.envrc</code> file with the following contents:</p><p>See <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/.envrc.sample">terraform/.envrc.sample</a> and create your own <code>terraform/.envrc</code>.</p><pre data-type="codeBlock" text="export AWS_PROFILE=...&lt;put here the name of the AWS profile you have configured&gt;...
export TF_VAR_subnet_id=...&lt;put here the sunet id from your VPC&gt;...
export TF_VAR_vpc_id=...&lt;put here the id of your VPC&gt;...
"><code>export AWS_PROFILE<span class="hljs-operator">=</span>...&#x3C;put here the name of the AWS profile you have configured<span class="hljs-operator">></span>...
export TF_VAR_subnet_id<span class="hljs-operator">=</span>...&#x3C;put here the sunet id <span class="hljs-keyword">from</span> your VPC<span class="hljs-operator">></span>...
export TF_VAR_vpc_id<span class="hljs-operator">=</span>...&#x3C;put here the id of your VPC<span class="hljs-operator">></span>...
</code></pre><p>Note that <code>terraform/.envrc</code> is being git ignored and it is not checked-in. This is to avoid checking in sensitive secret information. I have the file <code>terraform/.envrc.sample</code> which is checked-in and is a template to tell the reader how the actual <code>terraform/.envrc</code> file should be structured.</p><h3 id="h-awsprofile" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0"><code>AWS_PROFILE</code></h3><p>The <code>aws</code> provider needs a way to connect to my AWS account. One way that I like very much to use is AWS CLI Profiles. And the <code>AWS_PROFILE</code> is holding the name of the profile that I have configured on my local machine and that I want to use for this project.</p><p>If you want to use this method, follow the instructions <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html">here</a> and <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html">here</a>.</p><h3 id="h-checkpoint-1" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Checkpoint 1</h3><p>I will now double-check that everything works OK up until this point.</p><p>I run</p><pre data-type="codeBlock" text="terraform &gt; terraform validate
"><code>terraform <span class="hljs-operator">></span> terraform validate
</code></pre><p>It needs to print</p><blockquote><p>Success! The configuration is valid.</p></blockquote><p>And then I will actually create the resources.</p><p><strong>Important!</strong> This is the point where costs will be incurred in your AWS Account.</p><pre data-type="codeBlock" text="terraform &gt; terraform apply
"><code>terraform <span class="hljs-operator">></span> terraform apply
</code></pre><p>I am being presented with the plan and prompt to reply with <code>yes</code>, if I want to apply the plan:</p><pre data-type="codeBlock" text="Terraform will perform the following actions:

  # aws_instance.timescaledb will be created
  + resource &quot;aws_instance&quot; &quot;timescaledb&quot; {
      ...
    }

  # aws_key_pair.timescaledb will be created
  + resource &quot;aws_key_pair&quot; &quot;timescaledb&quot; {
      ...
    }

  # aws_security_group.postgres will be created
  + resource &quot;aws_security_group&quot; &quot;postgres&quot; {
      ...
    }

  # aws_security_group.ssh will be created
  + resource &quot;aws_security_group&quot; &quot;ssh&quot; {
      ...
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + ec2_timescaledb_public_ip = (known after apply)
  + ssh_connect               = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only &apos;yes&apos; will be accepted to approve.

  Enter a value: 
"><code>Terraform will perform the following actions:

  # aws_instance.timescaledb will be created
  <span class="hljs-operator">+</span> resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"timescaledb"</span> {
      ...
    }

  # aws_key_pair.timescaledb will be created
  <span class="hljs-operator">+</span> resource <span class="hljs-string">"aws_key_pair"</span> <span class="hljs-string">"timescaledb"</span> {
      ...
    }

  # aws_security_group.postgres will be created
  <span class="hljs-operator">+</span> resource <span class="hljs-string">"aws_security_group"</span> <span class="hljs-string">"postgres"</span> {
      ...
    }

  # aws_security_group.ssh will be created
  <span class="hljs-operator">+</span> resource <span class="hljs-string">"aws_security_group"</span> <span class="hljs-string">"ssh"</span> {
      ...
    }

Plan: <span class="hljs-number">4</span> to add, <span class="hljs-number">0</span> to change, <span class="hljs-number">0</span> to destroy.

Changes to Outputs:
  <span class="hljs-operator">+</span> ec2_timescaledb_public_ip <span class="hljs-operator">=</span> (known after apply)
  <span class="hljs-operator">+</span> ssh_connect               <span class="hljs-operator">=</span> (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span class="hljs-string">'yes'</span> will be accepted to approve.

  Enter a <span class="hljs-built_in">value</span>: 
</code></pre><p>I reply with <code>yes</code> and it is taking about 2 minutes for everything to be ready.</p><p>Then I use the AWS Management Console to check upon the state of the new EC2 Instance. I am waiting for it to become <code>Running</code>:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/17cb50ba215d0a5c905d254082fd95930d654b5686d9be3e5e47f63b3844a80d.png" alt="Instance state Running" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Instance state Running</figcaption></figure><p>Then I use the <code>ssh_connect</code> output to connect to the instance using <code>ssh</code> command. From the <code>terraform</code> folder again, I execute the command: (The shell first executes the command <code>terraform output ssh_connect</code> and then takes its output and executes again).</p><pre data-type="codeBlock" text="terraform &gt; $(terraform output -raw ssh_connect)
"><code>terraform <span class="hljs-operator">></span> $(terraform output <span class="hljs-operator">-</span>raw ssh_connect)
</code></pre><p>After I reply with <code>yes</code> to save the remote key to known hosts, I am inside the EC2 machine with an ssh shell:</p><pre data-type="codeBlock" text="Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-1030-aws x86_64)

 ...

To run a command as administrator (user &quot;root&quot;), use &quot;sudo &lt;command&gt;&quot;.
See &quot;man sudo_root&quot; for details.

ubuntu@ip-172-31-25-169:~$
"><code>Welcome to Ubuntu <span class="hljs-number">24.04</span><span class="hljs-number">.2</span> LTS (GNU<span class="hljs-operator">/</span>Linux <span class="hljs-number">6.8</span><span class="hljs-number">.0</span><span class="hljs-number">-1030</span><span class="hljs-operator">-</span>aws x86_64)

 ...

To run a command <span class="hljs-keyword">as</span> administrator (user <span class="hljs-string">"root"</span>), use <span class="hljs-string">"sudo &#x3C;command>"</span>.
See <span class="hljs-string">"man sudo_root"</span> <span class="hljs-keyword">for</span> details.

ubuntu@ip<span class="hljs-number">-172</span><span class="hljs-number">-31</span><span class="hljs-number">-25</span><span class="hljs-number">-169</span>:<span class="hljs-operator">~</span>$
</code></pre><h3 id="h-ebs-volume-data-storage" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">EBS Volume - Data Storage</h3><p>So far, so good.</p><p>But the EC2 instance doesn’t have TimescaleDB installed.</p><p>Also, it doesn’t have a specific instance-independent storage to save the actual database data.</p><p>I want:</p><ol><li><p>Every time the EC2 instance is created (or recreated) to have the TimescaleDB installed AND</p></li><li><p>Make sure that, if the EC2 instance is created (or recreated), the TimescaleDB data created from previous instance runs, it survives and continues to exist; i.e. I want make sure that I don’t lose my data every time the EC2 instance, for any reason, it is destroyed and then created from the beginning.</p></li></ol><p><strong>Outline Of Steps</strong></p><p>In order to satisfy these requirements:</p><ul><li><p>I will create the AWS EBS volume (using the resource <code>”aws_ebs_volume”</code>), then</p></li><li><p>I will attach it to the EC2 instance (using the resource <code>”aws_volume_attachment“</code>, then</p></li><li><p>I will automatically connect to the EC2 instance and</p><ul><li><p>create a file system device to represent the attached EBS volume</p></li><li><p>create the <code>/data</code> directory and mount it to the device created</p></li><li><p>install Postgres and configure TimescaleDB extension</p></li><li><p>configure Postgres to use the <code>/data</code> folder for storing data.</p></li></ul></li></ul><p><strong>AWS EBS Volume Resource</strong></p><p>This is the resource configuration for the AWS EBS volume:</p><p>file: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_volume.tf">terraform/ec2_volume.tf</a></p><pre data-type="codeBlock" text="resource &quot;aws_ebs_volume&quot; &quot;timescaledb_volume&quot; {
  availability_zone = &quot;${var.region}${var.timescaledb_server_availability_zone}&quot;
  size              = 64
  type              = &quot;gp3&quot;
  encrypted         = false
  final_snapshot    = false

  tags = {
    Name = &quot;${var.project}-${var.environment}-timescaledb-volume&quot;
  }
}
"><code>resource "aws_ebs_volume" "timescaledb_volume" {
  <span class="hljs-attr">availability_zone</span> = <span class="hljs-string">"${var.region}${var.timescaledb_server_availability_zone}"</span>
  <span class="hljs-attr">size</span>              = <span class="hljs-number">64</span>
  <span class="hljs-attr">type</span>              = <span class="hljs-string">"gp3"</span>
  <span class="hljs-attr">encrypted</span>         = <span class="hljs-literal">false</span>
  <span class="hljs-attr">final_snapshot</span>    = <span class="hljs-literal">false</span>

  <span class="hljs-attr">tags</span> = {
    <span class="hljs-attr">Name</span> = <span class="hljs-string">"${var.project}-${var.environment}-timescaledb-volume"</span>
  }
}
</code></pre><p>Note: the <code>final_snapshot</code> has been set to <code>false</code>. You may want to turn this to <code>true</code> to allow for AWS to create a snapshot when you destroy this volume, just in case you want to recover old data. But setting to <code>true</code> will incur costs.</p><p>This is quite minimum and many values are hard-coded. For example, the <code>size</code> is set to <code>64</code> GiB. And the type is set to <code>gp3</code>. Apparently, go ahead and set your own values.</p><p>If I <code>terraform apply</code>, Terraform is going to create an AWS EBS Volume which will be standing alone, not attached to any EC2 instance.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ad7cbc191f5c1bbb82c82af8998207ee92c7f1b51ea5021faeb02b63a31447a7.png" alt="AWS EBS Volume Is Not Attached" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">AWS EBS Volume Is Not Attached</figcaption></figure><p><strong>Attach New Volume to EC2 Instance</strong></p><p>This is how I am attaching the new volume to the EC2 instance:</p><p>file: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/ec2_volume.tf">terraform/ec2_volume.tf</a></p><pre data-type="codeBlock" text="resource &quot;aws_volume_attachment&quot; &quot;timescaledb_volume_attachment&quot; {
  device_name = &quot;/dev/sdd&quot;
  volume_id   = aws_ebs_volume.timescaledb_volume.id
  instance_id = aws_instance.timescaledb.id
}
"><code>resource <span class="hljs-string">"aws_volume_attachment"</span> <span class="hljs-string">"timescaledb_volume_attachment"</span> {
  device_name <span class="hljs-operator">=</span> <span class="hljs-string">"/dev/sdd"</span>
  volume_id   <span class="hljs-operator">=</span> aws_ebs_volume.timescaledb_volume.id
  instance_id <span class="hljs-operator">=</span> aws_instance.timescaledb.id
}
</code></pre><p>It’s quite simple, isn’t it.</p><p>But then, one might ask, how did I come up with the value <code>”/dev/sdd”</code> for the <code>device_name</code>?</p><p><strong>Explain</strong> <code>/dev/sdd</code></p><p>If I look at the details of the AMI that I use, I get this picture with regards to block devices that come with it:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/679cb7f9ed9c48ac2d171e83a776a2ece6222f990852cefb72c423447265f84a.png" alt="AMI Block Devices" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">AMI Block Devices</figcaption></figure><p>There are 3 devices that AMI offers to EC2 instances launched with this AMI. Only one of the 3 devices, the one with name <code>/dev/sda1</code> , is used as the 8GiB root device. This is where the EC2 instance is boot from. The devices <code>/dev/sdb</code> and <code>/dev/sdc</code> are <em>ephemeral</em> virtual devices which are not yet used by the EC2 Instance.</p><p>This is another picture (taken from AWS documentation) of what’s going on with storage in this EC2 instance:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/29a71a8a14056e4c912868a4bc73342b1758ab50946a4e37e1a629b450dd1c1f.png" alt="EBS-backed Instances" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">EBS-backed Instances</figcaption></figure><p>And if I look at the “Storage” tab on the instance details, I will see this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/43b23b723ba6835f3d263ec6e064cdf57c3ffaec27b33f2ebe1b1a1c64014b8b.png" alt="EC2 Instance Is Using Only the /dev/sda1 device" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">EC2 Instance Is Using Only the /dev/sda1 device</figcaption></figure><p>So, the EC2 instance uses the <code>/dev/sda1</code> device.</p><p>But how do I finally choose the name <code>/dev/sdd</code> for the new volume that I am attaching? The <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html">AWS documentation on device naming</a> has the details.</p><p>According to the documentation and the AMI virtualization type I am here:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c554c8782e53ae97c23aaede302b5b40748b2af90c9557658981552e1fa77e12.png" alt="The AWS Recommendation For Naming My Device" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">The AWS Recommendation For Naming My Device</figcaption></figure><p>Since <code>/dev/sdb</code> and <code>/dev/sdc</code> are already <em>taken</em> , I am going with <code>/dev/sdd</code>.</p><p>Note that I am not interested in mounting the <code>/dev/sdb</code> and <code>/dev/sdc</code> ephemeral devices. So, I just leave them unmounted.</p><p>Hence, <strong>after I</strong> <code>terraform apply</code> the new resource <code>timescaledb_volume_attachment</code>, the “Storage” tab on the instance picture will be:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/08b9f78561303f24cc58b3962546cc9fc0f6b5489171ce1723dd61a0c844fa4e.png" alt="64GiB volume attached" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">64GiB volume attached</figcaption></figure><p>I can see the new volume attached using the device name <code>/dev/sdd</code>.</p><p><strong>Prepare To Store Data</strong></p><p>But even if the EBS volume is attached to the EC2 machine, this doesn’t mean that it is ready to write data on it.</p><p>If I ssh to the new EC2 instance and do the following command:</p><pre data-type="codeBlock" text="$ lsblk
"><code><span class="hljs-variable">$ </span>lsblk
</code></pre><p>I will see something like this:</p><pre data-type="codeBlock" text="NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0 27.2M  1 loop /snap/amazon-ssm-agent/11320
loop1          7:1    0 73.9M  1 loop /snap/core22/2010
loop2          7:2    0 50.9M  1 loop /snap/snapd/24718
nvme0n1      259:0    0    8G  0 disk 
├─nvme0n1p1  259:1    0    7G  0 part /
├─nvme0n1p14 259:2    0    4M  0 part 
├─nvme0n1p15 259:3    0  106M  0 part /boot/efi
└─nvme0n1p16 259:4    0  913M  0 part /boot
nvme1n1      259:5    0   64G  0 disk 
"><code>NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          <span class="hljs-number">7</span>:<span class="hljs-number">0</span>    <span class="hljs-number">0</span> <span class="hljs-number">27</span>.2M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>amazon<span class="hljs-operator">-</span>ssm<span class="hljs-operator">-</span>agent<span class="hljs-operator">/</span><span class="hljs-number">11320</span>
loop1          <span class="hljs-number">7</span>:<span class="hljs-number">1</span>    <span class="hljs-number">0</span> <span class="hljs-number">73</span>.9M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>core22<span class="hljs-operator">/</span><span class="hljs-number">2010</span>
loop2          <span class="hljs-number">7</span>:<span class="hljs-number">2</span>    <span class="hljs-number">0</span> <span class="hljs-number">50</span>.9M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>snapd<span class="hljs-operator">/</span><span class="hljs-number">24718</span>
nvme0n1      <span class="hljs-number">259</span>:<span class="hljs-number">0</span>    <span class="hljs-number">0</span>    8G  <span class="hljs-number">0</span> disk 
├─nvme0n1p1  <span class="hljs-number">259</span>:<span class="hljs-number">1</span>    <span class="hljs-number">0</span>    7G  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>
├─nvme0n1p14 <span class="hljs-number">259</span>:<span class="hljs-number">2</span>    <span class="hljs-number">0</span>    4M  <span class="hljs-number">0</span> part 
├─nvme0n1p15 <span class="hljs-number">259</span>:<span class="hljs-number">3</span>    <span class="hljs-number">0</span>  106M  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>boot<span class="hljs-operator">/</span>efi
└─nvme0n1p16 <span class="hljs-number">259</span>:<span class="hljs-number">4</span>    <span class="hljs-number">0</span>  913M  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>boot
nvme1n1      <span class="hljs-number">259</span>:<span class="hljs-number">5</span>    <span class="hljs-number">0</span>   64G  <span class="hljs-number">0</span> disk 
</code></pre><p>The last entry <code>nvme1n1</code> of size <code>64G</code> corresponds to the newly attached AWS EBS volume. But, it doesn’t have any partition (like the <code>nvme0n1</code> does for example).</p><p>I need to format and create the logical partitions. Then we will have to mount a directory on it.</p><p>I have several ways to do that with Terraform. I chose to go with <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://developer.hashicorp.com/terraform/language/resources/terraform-data">terraform_data</a> resource.</p><p>I create the file <code>prepare_ebs_volume.tf</code> with the following content:</p><p>file: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/prepare_ebs_volume.tf">terraform/prepare_ebs_volume.tf</a></p><pre data-type="codeBlock" text="resource &quot;terraform_data&quot; &quot;prepare_ebs_volume_for_writing&quot; {
  depends_on = [aws_volume_attachment.timescaledb_volume_attachment]

  triggers_replace = {
    volume_attachment = aws_volume_attachment.timescaledb_volume_attachment.id
  }

  connection {
    type        = &quot;ssh&quot;
    user        = &quot;ubuntu&quot;
    host        = aws_instance.timescaledb.public_ip
    private_key = file(&quot;${path.module}/timescaledb.pem&quot;)
  }

  provisioner &quot;remote-exec&quot; {
    inline = concat(
      [
        &quot;sudo file -s /dev/nvme1n1 | grep -q &apos;/dev/nvme1n1: data$&apos; &amp;&amp; sudo mkfs -t xfs /dev/nvme1n1&quot;,
        &quot;sudo mkdir /data&quot;,
        &quot;sudo mount /dev/nvme1n1 /data&quot;,
        &quot;sudo cp /etc/fstab /etc/fstab.bak&quot;,
        &quot;echo \&quot;UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1) /data xfs defaults,nofail 0 2\&quot; | sudo tee -a /etc/fstab&quot;
      ]
    )
  }
}
"><code>resource <span class="hljs-string">"terraform_data"</span> <span class="hljs-string">"prepare_ebs_volume_for_writing"</span> {
  depends_on <span class="hljs-operator">=</span> [aws_volume_attachment.timescaledb_volume_attachment]

  triggers_replace <span class="hljs-operator">=</span> {
    volume_attachment <span class="hljs-operator">=</span> aws_volume_attachment.timescaledb_volume_attachment.id
  }

  connection {
    <span class="hljs-keyword">type</span>        <span class="hljs-operator">=</span> <span class="hljs-string">"ssh"</span>
    user        <span class="hljs-operator">=</span> <span class="hljs-string">"ubuntu"</span>
    host        <span class="hljs-operator">=</span> aws_instance.timescaledb.public_ip
    private_key <span class="hljs-operator">=</span> file(<span class="hljs-string">"${path.module}/timescaledb.pem"</span>)
  }

  provisioner <span class="hljs-string">"remote-exec"</span> {
    inline <span class="hljs-operator">=</span> concat(
      [
        <span class="hljs-string">"sudo file -s /dev/nvme1n1 | grep -q '/dev/nvme1n1: data$' &#x26;&#x26; sudo mkfs -t xfs /dev/nvme1n1"</span>,
        <span class="hljs-string">"sudo mkdir /data"</span>,
        <span class="hljs-string">"sudo mount /dev/nvme1n1 /data"</span>,
        <span class="hljs-string">"sudo cp /etc/fstab /etc/fstab.bak"</span>,
        <span class="hljs-string">"echo \"UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1) /data xfs defaults,nofail 0 2\" | sudo tee -a /etc/fstab"</span>
      ]
    )
  }
}
</code></pre><ol><li><p>the <code>depends_on</code> is critical in order to make sure that resources are created in correct order, but</p></li><li><p>the <code>triggers_replace</code> is critical to trigger reconstructions/recreations when the resource already exists. And for this particular case, if the EBS volume is reattached (see for example later on the case in which EC2 instance is destroyed and recreated again), we want this provisioner to fire again.</p></li><li><p><code>mkfs</code> creates a files system of type <code>xfs</code> on the new volume. Note, that I call <code>mkfs</code> <strong>only if there is not file system already on the volume</strong>, in order to avoid overwriting any existing data.</p></li><li><p><code>mkdir</code> creates the <code>/data</code> directory. This is where the Postgres/TimescaleDB will be writing data. See later on.</p></li><li><p><code>mount</code> mounts the volume to the directory <code>/data</code>.</p></li><li><p>the <code>echo….</code> updates the <code>/etc/fstab</code> file so that when the instance is ever rebooted to have the volume mounted to <code>/data</code> automatically.</p></li></ol><p><em>Note:</em> If you want to read more about this process read the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.aws.amazon.com/ebs/latest/userguide/ebs-using-volumes.html">official AWS documentation here</a>.</p><p>I do <code>terraform apply</code> and then I ssh to the EC2 instance.</p><p>The command <code>lsblk</code> now prints:</p><pre data-type="codeBlock" text="NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0 27.2M  1 loop /snap/amazon-ssm-agent/11320
loop1          7:1    0 73.9M  1 loop /snap/core22/2010
loop2          7:2    0 50.9M  1 loop /snap/snapd/24718
nvme0n1      259:0    0    8G  0 disk 
├─nvme0n1p1  259:1    0    7G  0 part /
├─nvme0n1p14 259:2    0    4M  0 part 
├─nvme0n1p15 259:3    0  106M  0 part /boot/efi
└─nvme0n1p16 259:4    0  913M  0 part /boot
nvme1n1      259:5    0   64G  0 disk /data
"><code>NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          <span class="hljs-number">7</span>:<span class="hljs-number">0</span>    <span class="hljs-number">0</span> <span class="hljs-number">27</span>.2M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>amazon<span class="hljs-operator">-</span>ssm<span class="hljs-operator">-</span>agent<span class="hljs-operator">/</span><span class="hljs-number">11320</span>
loop1          <span class="hljs-number">7</span>:<span class="hljs-number">1</span>    <span class="hljs-number">0</span> <span class="hljs-number">73</span>.9M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>core22<span class="hljs-operator">/</span><span class="hljs-number">2010</span>
loop2          <span class="hljs-number">7</span>:<span class="hljs-number">2</span>    <span class="hljs-number">0</span> <span class="hljs-number">50</span>.9M  <span class="hljs-number">1</span> loop <span class="hljs-operator">/</span>snap<span class="hljs-operator">/</span>snapd<span class="hljs-operator">/</span><span class="hljs-number">24718</span>
nvme0n1      <span class="hljs-number">259</span>:<span class="hljs-number">0</span>    <span class="hljs-number">0</span>    8G  <span class="hljs-number">0</span> disk 
├─nvme0n1p1  <span class="hljs-number">259</span>:<span class="hljs-number">1</span>    <span class="hljs-number">0</span>    7G  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>
├─nvme0n1p14 <span class="hljs-number">259</span>:<span class="hljs-number">2</span>    <span class="hljs-number">0</span>    4M  <span class="hljs-number">0</span> part 
├─nvme0n1p15 <span class="hljs-number">259</span>:<span class="hljs-number">3</span>    <span class="hljs-number">0</span>  106M  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>boot<span class="hljs-operator">/</span>efi
└─nvme0n1p16 <span class="hljs-number">259</span>:<span class="hljs-number">4</span>    <span class="hljs-number">0</span>  913M  <span class="hljs-number">0</span> part <span class="hljs-operator">/</span>boot
nvme1n1      <span class="hljs-number">259</span>:<span class="hljs-number">5</span>    <span class="hljs-number">0</span>   64G  <span class="hljs-number">0</span> disk <span class="hljs-operator">/</span>data
</code></pre><p>Look how the <code>nvme1n1</code>. Instead of just <code>disk</code> it shows <code>/data</code>.</p><p>Bingo! If you go back and read the <em>Outline of Steps</em> above, you will see that I have taken these two steps already:</p><ul><li><p>create a file system device to represent the attached EBS volume</p></li><li><p>create the <code>/data</code> directory and mount it to the device created</p></li></ul><p>Next two steps:</p><p><strong>Install Postgres and TimescaleDB extension</strong></p><p>I will now add two more <code>terraform_data</code> blocks that will take care of installing Postgres and TimescaleDB extension and configuring it to store data into the <code>/data</code> folder (which now resides in the new EBS volume).</p><p>I name the file <code>install_and_setup_timescaledb.tf</code>.</p><p>file: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/install_and_setup_timescaledb.tf">terraform/install_and_setup_timescaledb.tf</a>.</p><pre data-type="codeBlock" text="locals {
  db_name                   = &quot;events_server_${var.environment}&quot;
  path_to_postgres_data_dir = &quot;/data/postgresql/${var.postgresql_version}/main&quot;
}

resource &quot;terraform_data&quot; &quot;install_and_setup_timescaledb&quot; {
  depends_on = [terraform_data.prepare_ebs_volume_for_writing]

  triggers_replace = {
    volume_attachment  = aws_volume_attachment.timescaledb_volume_attachment.id
    postgresql_version = var.postgresql_version
  }

  connection {
    type        = &quot;ssh&quot;
    user        = &quot;ubuntu&quot;
    host        = aws_instance.timescaledb.public_ip
    private_key = file(&quot;${path.module}/timescaledb.pem&quot;)
  }

  provisioner &quot;file&quot; {
    source      = &quot;${path.module}/install_postgres.sh&quot;
    destination = &quot;/home/ubuntu/install_postgres.sh&quot;
  }

  provisioner &quot;file&quot; {
    source      = &quot;${path.module}/install_timescaledb.sh&quot;
    destination = &quot;/home/ubuntu/install_timescaledb.sh&quot;
  }

  provisioner &quot;remote-exec&quot; {
    inline = concat(
      [
        &quot;sudo chmod u+x /home/ubuntu/install_postgres.sh&quot;,
        &quot;sudo ./install_postgres.sh ${var.postgresql_version} ${var.timescaledb_server_port}  ${local.db_name} ${var.timescaledb_version}&quot;,

        &quot;sudo chmod u+x /home/ubuntu/install_timescaledb.sh&quot;,
        &quot;sudo ./install_timescaledb.sh ${var.postgresql_version} ${var.timescaledb_version} ${local.db_name}&quot;
      ]
    )
  }
}

resource &quot;terraform_data&quot; &quot;postgres_password&quot; {
  depends_on = [terraform_data.install_and_setup_timescaledb]

  triggers_replace = {
    volume_attachment  = aws_volume_attachment.timescaledb_volume_attachment.id
    postgresql_version = var.postgresql_version
  }

  connection {
    type        = &quot;ssh&quot;
    user        = &quot;ubuntu&quot;
    host        = aws_instance.timescaledb.public_ip
    private_key = file(&quot;${path.module}/timescaledb.pem&quot;)
  }

  provisioner &quot;remote-exec&quot; {
    inline = concat(
      [
        &quot;echo &apos;**************** remote-exec: Setting the postgres user password...&apos;&quot;,
        &quot;sudo -u postgres psql -c \&quot;ALTER USER postgres WITH PASSWORD &apos;${var.timescaledb_server_postgres_password}&apos;;\&quot;&quot;
      ]
    )
  }
}

output &quot;psql_connect&quot; {
  value       = &quot;psql -h ${aws_instance.timescaledb.public_ip} -U postgres -d ${local.db_name} -p ${var.timescaledb_server_port}&quot;
  description = &quot;Command to connect to the PostgreSQL database using psql.&quot;
}
"><code>locals {
  db_name                   <span class="hljs-operator">=</span> <span class="hljs-string">"events_server_${var.environment}"</span>
  path_to_postgres_data_dir <span class="hljs-operator">=</span> <span class="hljs-string">"/data/postgresql/${var.postgresql_version}/main"</span>
}

resource <span class="hljs-string">"terraform_data"</span> <span class="hljs-string">"install_and_setup_timescaledb"</span> {
  depends_on <span class="hljs-operator">=</span> [terraform_data.prepare_ebs_volume_for_writing]

  triggers_replace <span class="hljs-operator">=</span> {
    volume_attachment  <span class="hljs-operator">=</span> aws_volume_attachment.timescaledb_volume_attachment.id
    postgresql_version <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.postgresql_version
  }

  connection {
    <span class="hljs-keyword">type</span>        <span class="hljs-operator">=</span> <span class="hljs-string">"ssh"</span>
    user        <span class="hljs-operator">=</span> <span class="hljs-string">"ubuntu"</span>
    host        <span class="hljs-operator">=</span> aws_instance.timescaledb.public_ip
    private_key <span class="hljs-operator">=</span> file(<span class="hljs-string">"${path.module}/timescaledb.pem"</span>)
  }

  provisioner <span class="hljs-string">"file"</span> {
    source      <span class="hljs-operator">=</span> <span class="hljs-string">"${path.module}/install_postgres.sh"</span>
    destination <span class="hljs-operator">=</span> <span class="hljs-string">"/home/ubuntu/install_postgres.sh"</span>
  }

  provisioner <span class="hljs-string">"file"</span> {
    source      <span class="hljs-operator">=</span> <span class="hljs-string">"${path.module}/install_timescaledb.sh"</span>
    destination <span class="hljs-operator">=</span> <span class="hljs-string">"/home/ubuntu/install_timescaledb.sh"</span>
  }

  provisioner <span class="hljs-string">"remote-exec"</span> {
    inline <span class="hljs-operator">=</span> concat(
      [
        <span class="hljs-string">"sudo chmod u+x /home/ubuntu/install_postgres.sh"</span>,
        <span class="hljs-string">"sudo ./install_postgres.sh ${var.postgresql_version} ${var.timescaledb_server_port}  ${local.db_name} ${var.timescaledb_version}"</span>,

        <span class="hljs-string">"sudo chmod u+x /home/ubuntu/install_timescaledb.sh"</span>,
        <span class="hljs-string">"sudo ./install_timescaledb.sh ${var.postgresql_version} ${var.timescaledb_version} ${local.db_name}"</span>
      ]
    )
  }
}

resource <span class="hljs-string">"terraform_data"</span> <span class="hljs-string">"postgres_password"</span> {
  depends_on <span class="hljs-operator">=</span> [terraform_data.install_and_setup_timescaledb]

  triggers_replace <span class="hljs-operator">=</span> {
    volume_attachment  <span class="hljs-operator">=</span> aws_volume_attachment.timescaledb_volume_attachment.id
    postgresql_version <span class="hljs-operator">=</span> <span class="hljs-keyword">var</span>.postgresql_version
  }

  connection {
    <span class="hljs-keyword">type</span>        <span class="hljs-operator">=</span> <span class="hljs-string">"ssh"</span>
    user        <span class="hljs-operator">=</span> <span class="hljs-string">"ubuntu"</span>
    host        <span class="hljs-operator">=</span> aws_instance.timescaledb.public_ip
    private_key <span class="hljs-operator">=</span> file(<span class="hljs-string">"${path.module}/timescaledb.pem"</span>)
  }

  provisioner <span class="hljs-string">"remote-exec"</span> {
    inline <span class="hljs-operator">=</span> concat(
      [
        <span class="hljs-string">"echo '**************** remote-exec: Setting the postgres user password...'"</span>,
        <span class="hljs-string">"sudo -u postgres psql -c \"ALTER USER postgres WITH PASSWORD '${var.timescaledb_server_postgres_password}';\""</span>
      ]
    )
  }
}

output <span class="hljs-string">"psql_connect"</span> {
  value       <span class="hljs-operator">=</span> <span class="hljs-string">"psql -h ${aws_instance.timescaledb.public_ip} -U postgres -d ${local.db_name} -p ${var.timescaledb_server_port}"</span>
  description <span class="hljs-operator">=</span> <span class="hljs-string">"Command to connect to the PostgreSQL database using psql."</span>
}
</code></pre><p><strong>Dependency:</strong> I put a dependency of the first resource to the previous one, i.e. of the <code>install_and_setup_timescaledb</code> to the <code>terraform_data.prepare_ebs_volume_for_writing</code>. Hence, the <code>remote-exec</code> calls are going to be executed after that block’s commands do.</p><p><strong>Triggers Replace:</strong> However, besides the order of creation which is specified with the <code>dependends_on</code> block, I also use the <code>triggers_replace</code> block to specify that I want this block to be recreated under these two conditions:</p><ul><li><p>Every time the attachment of the EBS volume is recreated.</p></li><li><p>Every time the version of the Postgres server changes.</p></li></ul><p><strong>Two bash Scripts</strong></p><p>I use two bash scripts:</p><ul><li><p>Bash script to install Postgres: <code>install_postgres.sh</code>.</p></li><li><p>Bash script to install TimescaleDB: <code>install_timescaledb.sh</code>.</p></li></ul><p>Note that both scripts are also called when upgrading Postgres version.</p><p><strong>Installing Postgres</strong></p><p>The file <code>install_postgres.sh</code> has the following contents:</p><p>file <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/install_postgres.sh">terraform/install_postgres.sh</a>.</p><pre data-type="codeBlock" text="#!/bin/bash

set -e # e: exit if any command has a non-zero exit status
set -x # x: all executed commands are printed to the terminal
set -u # u: all references to variables that have not been previously defined cause an error

NEW_VERSION=$1
PORT=$2
DB_NAME=$3
TIMESCALEDB_VERSION=&quot;${4}*&quot;

# Create the PostgreSQL data directory
sudo mkdir -p /data/postgresql

# Update the package list
sudo DEBIAN_FRONTEND=noninteractive apt update -y

# Install postgres common tools
sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-common apt-transport-https net-tools

# Enable the PostgreSQL APT repository
sudo DEBIAN_FRONTEND=noninteractive /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y

# Let&apos;s now install on top of any existing
CURRENT_CLUSTER=$(sudo pg_lsclusters -h 2&gt;/dev/null | grep &quot;${PORT}&quot; | awk &apos;{print $1&quot;-&quot;$2}&apos; || true)

INSTALL=&quot;yes&quot;
OLD_VERSION=&quot;&quot;
OLD_NAME=&quot;main&quot;
if [ -n &quot;$CURRENT_CLUSTER&quot; ]; then
  echo &quot;Found existing PostgreSQL cluster:&quot;
  echo &quot;$CURRENT_CLUSTER&quot;
  echo

  OLD_VERSION=$(echo $CURRENT_CLUSTER | cut -d&apos;-&apos; -f1)
  OLD_NAME=$(echo $CURRENT_CLUSTER | cut -d&apos;-&apos; -f2)

  # Skip if this is the same version and name we&apos;re about to install
  if [ &quot;$OLD_VERSION&quot; = &quot;$NEW_VERSION&quot; ] &amp;&amp; [ &quot;$OLD_NAME&quot; = &quot;main&quot; ]; then
      echo &quot;Skipping cluster $OLD_VERSION/$OLD_NAME as it matches the target version and name&quot;
      INSTALL=&quot;no&quot;
      continue
  fi

  echo &quot;************************ stopping and disabling $OLD_VERSION/$OLD_NAME ************************&quot;
  sudo systemctl stop postgresql@$OLD_VERSION-$OLD_NAME || true
  sudo systemctl status postgresql@$OLD_VERSION-$OLD_NAME --no-pager || true
  sudo systemctl disable postgresql@$OLD_VERSION-$OLD_NAME || true
  sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME || true
  sudo pg_lsclusters -h
else
  echo &quot;No existing PostgreSQL clusters found on port ${PORT}. Proceeding with installation.&quot;
  echo

  INSTALL=&quot;yes&quot;
fi

echo &quot;****************** INSTALL: $INSTALL&quot;
if [ &quot;$INSTALL&quot; = &quot;yes&quot; ]; then
  sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-$NEW_VERSION postgresql-client-$NEW_VERSION postgresql-contrib-$NEW_VERSION
  sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-server-dev-$NEW_VERSION
  # When I install postgresql for the first time, the cluster is already created.
  # But when I install a new version while another already exists, the cluster is not created.
  if sudo pg_lsclusters -h 2&gt;/dev/null | grep -q &quot;^${NEW_VERSION}[[:space:]]\+main[[:space:]]&quot;; then
    echo &quot;Cluster $NEW_VERSION/main already exists&quot;
  else
    echo &quot;Creating cluster $NEW_VERSION/main&quot;
    sudo pg_createcluster $NEW_VERSION main
    sudo pg_ctlcluster start $NEW_VERSION main
    sudo pg_lsclusters -h
  fi
  sudo systemctl start postgresql@${NEW_VERSION}-main
  sudo systemctl enable postgresql@$NEW_VERSION-main
else
  echo &quot;Skipping installation of PostgreSQL ${NEW_VERSION} as it is already installed.&quot;
fi

# Show final status

echo &quot;Current PostgreSQL clusters:&quot;
sudo pg_lsclusters -h

sudo systemctl status postgresql@${NEW_VERSION}-main --no-pager

if [ -n &quot;$OLD_VERSION&quot; ]; then
  echo &quot;Stopping and disabling old PostgreSQL cluster $OLD_VERSION/$OLD_NAME&quot;
  sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME || true
else
  echo &quot;No old PostgreSQL cluster to stop.&quot;
fi

# Stop postgres from running
sudo systemctl stop postgresql@${NEW_VERSION}-main

# Change where PostgreSQL stores its data
ORIGINAL_DATA_DIR=&quot;/var/lib/postgresql/${NEW_VERSION}/main&quot;

if [ -d &quot;${ORIGINAL_DATA_DIR}&quot; ];then
  if [ -d &quot;/var/lib/postgresql/${NEW_VERSION}/main.bak&quot; ]; then
    rm -f -R /var/lib/postgresql/${NEW_VERSION}/main.bak
  fi
  sudo mv ${ORIGINAL_DATA_DIR} /var/lib/postgresql/${NEW_VERSION}/main.bak
fi

NEW_PATH_TO_POSTGRES_DATA_DIR=&quot;/data/postgresql/${NEW_VERSION}/main&quot;

sudo mkdir -p ${NEW_PATH_TO_POSTGRES_DATA_DIR}
sudo chown -R postgres:postgres /data/postgresql
if [ -n &quot;$(sudo ls -A ${NEW_PATH_TO_POSTGRES_DATA_DIR} 2&gt;/dev/null)&quot; ]; then
  echo &quot;The new PostgreSQL data directory is not empty. We will not initialize it.&quot;
else
  echo &quot;Initializing new PostgreSQL data directory at ${NEW_PATH_TO_POSTGRES_DATA_DIR}&quot;
  sudo -u postgres /usr/lib/postgresql/${NEW_VERSION}/bin/initdb -D ${NEW_PATH_TO_POSTGRES_DATA_DIR}
fi
sudo sed -i &quot;s|data_directory = &apos;${ORIGINAL_DATA_DIR}&apos;|data_directory = &apos;${NEW_PATH_TO_POSTGRES_DATA_DIR}&apos;|g&quot; /etc/postgresql/${NEW_VERSION}/main/postgresql.conf

# Set the port to whatever we specify as port in the terraform variables
sudo sed -i &quot;s|port = .*|port = ${PORT}|g&quot; /etc/postgresql/${NEW_VERSION}/main/postgresql.conf

# Allow remote connections
sudo sed -i &quot;s|#listen_addresses = &apos;localhost&apos;|listen_addresses = &apos;*&apos;|g&quot; /etc/postgresql/${NEW_VERSION}/main/postgresql.conf
sudo sed -i &quot;s|host    all             all             127.0.0.1/32            scram-sha-256|host    all             all             0.0.0.0/0            scram-sha-256|g&quot; /etc/postgresql/${NEW_VERSION}/main/pg_hba.conf

# upgrade from previous version if needed
LAST_CLUSTER=$(echo &quot;$CURRENT_CLUSTER&quot; | tail -n 1)
if [ -n &quot;$LAST_CLUSTER&quot; ]; then
    OLD_VERSION=$(echo $LAST_CLUSTER | cut -d&apos;-&apos; -f1)
    OLD_NAME=$(echo $LAST_CLUSTER | cut -d&apos;-&apos; -f2)
    if [ &quot;$OLD_VERSION&quot; = &quot;$NEW_VERSION&quot; ] &amp;&amp; [ &quot;$OLD_NAME&quot; = &quot;main&quot; ]; then
      echo &quot;...no need to upgrade data, we are on the same cluster version and name&quot;
    else
      echo &quot;We need to upgrade the data of the last cluster ${OLD_VERSION}-${OLD_NAME}&quot;
      # We will need to install timescale db for the new version, otherwise the pg_upgrade will fail
      # ---------------------------------------------------------------------------------------------
      sudo apt install timescaledb-2-postgresql-${NEW_VERSION}=&quot;${TIMESCALEDB_VERSION}&quot; timescaledb-2-loader-postgresql-${NEW_VERSION}=&quot;${TIMESCALEDB_VERSION}&quot; timescaledb-toolkit-postgresql-${NEW_VERSION} -y

      sudo sed -i &quot;s|#shared_preload_libraries = &apos;&apos;|shared_preload_libraries = &apos;timescaledb&apos;|g&quot; /data/postgresql/${NEW_VERSION}/main/postgresql.conf

      # Tune TimescaleDB
      sudo timescaledb-tune --yes
      # --------------- end of installing timescale db for the new version -----------------------------

      (cd /tmp &amp;&amp; sudo -u postgres /usr/lib/postgresql/${NEW_VERSION}/bin/pg_upgrade \
        --old-datadir=/data/postgresql/$OLD_VERSION/$OLD_NAME \
        --new-datadir=/data/postgresql/$NEW_VERSION/main \
        --old-bindir=/usr/lib/postgresql/$OLD_VERSION/bin \
        --new-bindir=/usr/lib/postgresql/$NEW_VERSION/bin)
    fi
fi

# Restart PostgreSQL to apply changes
sudo systemctl restart postgresql@${NEW_VERSION}-main

# Wait for PostgreSQL to start
sleep 10

# Create the database
sudo -u postgres psql -c &quot;create database ${DB_NAME};&quot; || echo &quot;Database ${DB_NAME} already exists, skipping creation.&quot;
"><code>#<span class="hljs-operator">!</span><span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>bash

set <span class="hljs-operator">-</span>e # e: exit <span class="hljs-keyword">if</span> any command has a non<span class="hljs-operator">-</span>zero exit status
set <span class="hljs-operator">-</span>x # x: all executed commands are printed to the terminal
set <span class="hljs-operator">-</span>u # u: all references to variables that have not been previously defined cause an <span class="hljs-function"><span class="hljs-keyword">error</span>

<span class="hljs-title">NEW_VERSION</span>=<span class="hljs-title">$1</span>
<span class="hljs-title">PORT</span>=<span class="hljs-title">$2</span>
<span class="hljs-title">DB_NAME</span>=<span class="hljs-title">$3</span>
<span class="hljs-title">TIMESCALEDB_VERSION</span>="<span class="hljs-title">$</span></span>{<span class="hljs-number">4</span>}<span class="hljs-operator">*</span><span class="hljs-string">"

# Create the PostgreSQL data directory
sudo mkdir -p /data/postgresql

# Update the package list
sudo DEBIAN_FRONTEND=noninteractive apt update -y

# Install postgres common tools
sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-common apt-transport-https net-tools

# Enable the PostgreSQL APT repository
sudo DEBIAN_FRONTEND=noninteractive /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y

# Let's now install on top of any existing
CURRENT_CLUSTER=$(sudo pg_lsclusters -h 2>/dev/null | grep "</span>${PORT}<span class="hljs-string">" | awk '{print $1"</span><span class="hljs-operator">-</span><span class="hljs-string">"$2}' || true)

INSTALL="</span>yes<span class="hljs-string">"
OLD_VERSION="</span><span class="hljs-string">"
OLD_NAME="</span>main<span class="hljs-string">"
if [ -n "</span>$CURRENT_CLUSTER<span class="hljs-string">" ]; then
  echo "</span>Found existing PostgreSQL cluster:<span class="hljs-string">"
  echo "</span>$CURRENT_CLUSTER<span class="hljs-string">"
  echo

  OLD_VERSION=$(echo $CURRENT_CLUSTER | cut -d'-' -f1)
  OLD_NAME=$(echo $CURRENT_CLUSTER | cut -d'-' -f2)

  # Skip if this is the same version and name we're about to install
  if [ "</span>$OLD_VERSION<span class="hljs-string">" = "</span>$NEW_VERSION<span class="hljs-string">" ] &#x26;&#x26; [ "</span>$OLD_NAME<span class="hljs-string">" = "</span>main<span class="hljs-string">" ]; then
      echo "</span>Skipping cluster $OLD_VERSION<span class="hljs-operator">/</span>$OLD_NAME <span class="hljs-keyword">as</span> it matches the target version and name<span class="hljs-string">"
      INSTALL="</span>no<span class="hljs-string">"
      continue
  fi

  echo "</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span> stopping and disabling $OLD_VERSION<span class="hljs-operator">/</span>$OLD_NAME <span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-string">"
  sudo systemctl stop postgresql@$OLD_VERSION-$OLD_NAME || true
  sudo systemctl status postgresql@$OLD_VERSION-$OLD_NAME --no-pager || true
  sudo systemctl disable postgresql@$OLD_VERSION-$OLD_NAME || true
  sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME || true
  sudo pg_lsclusters -h
else
  echo "</span>No existing PostgreSQL clusters found on port ${PORT}. Proceeding with installation."
  echo

  INSTALL<span class="hljs-operator">=</span><span class="hljs-string">"yes"</span>
fi

echo <span class="hljs-string">"****************** INSTALL: $INSTALL"</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">"$INSTALL"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"yes"</span> ]; then
  sudo DEBIAN_FRONTEND<span class="hljs-operator">=</span>noninteractive apt install <span class="hljs-operator">-</span>y postgresql<span class="hljs-operator">-</span>$NEW_VERSION postgresql<span class="hljs-operator">-</span>client<span class="hljs-operator">-</span>$NEW_VERSION postgresql<span class="hljs-operator">-</span>contrib<span class="hljs-operator">-</span>$NEW_VERSION
  sudo DEBIAN_FRONTEND<span class="hljs-operator">=</span>noninteractive apt install <span class="hljs-operator">-</span>y postgresql<span class="hljs-operator">-</span>server<span class="hljs-operator">-</span>dev<span class="hljs-operator">-</span>$NEW_VERSION
  # When I install postgresql <span class="hljs-keyword">for</span> the first time, the cluster <span class="hljs-keyword">is</span> already created.
  # But when I install a <span class="hljs-keyword">new</span> version <span class="hljs-keyword">while</span> another already exists, the cluster <span class="hljs-keyword">is</span> not created.
  if sudo pg_lsclusters <span class="hljs-operator">-</span>h <span class="hljs-number">2</span><span class="hljs-operator">></span><span class="hljs-operator">/</span>dev<span class="hljs-operator">/</span>null <span class="hljs-operator">|</span> grep <span class="hljs-operator">-</span>q <span class="hljs-string">"^${NEW_VERSION}[[:space:]]\+main[[:space:]]"</span>; then
    echo <span class="hljs-string">"Cluster $NEW_VERSION/main already exists"</span>
  <span class="hljs-keyword">else</span>
    echo <span class="hljs-string">"Creating cluster $NEW_VERSION/main"</span>
    sudo pg_createcluster $NEW_VERSION main
    sudo pg_ctlcluster start $NEW_VERSION main
    sudo pg_lsclusters <span class="hljs-operator">-</span>h
  fi
  sudo systemctl start postgresql@${NEW_VERSION}<span class="hljs-operator">-</span>main
  sudo systemctl enable postgresql@$NEW_VERSION<span class="hljs-operator">-</span>main
<span class="hljs-keyword">else</span>
  echo <span class="hljs-string">"Skipping installation of PostgreSQL ${NEW_VERSION} as it is already installed."</span>
fi

# Show final status

echo <span class="hljs-string">"Current PostgreSQL clusters:"</span>
sudo pg_lsclusters <span class="hljs-operator">-</span>h

sudo systemctl status postgresql@${NEW_VERSION}<span class="hljs-operator">-</span>main <span class="hljs-operator">-</span><span class="hljs-operator">-</span>no<span class="hljs-operator">-</span>pager

<span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>n <span class="hljs-string">"$OLD_VERSION"</span> ]; then
  echo <span class="hljs-string">"Stopping and disabling old PostgreSQL cluster $OLD_VERSION/$OLD_NAME"</span>
  sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-literal">true</span>
<span class="hljs-keyword">else</span>
  echo <span class="hljs-string">"No old PostgreSQL cluster to stop."</span>
fi

# Stop postgres <span class="hljs-keyword">from</span> running
sudo systemctl stop postgresql@${NEW_VERSION}<span class="hljs-operator">-</span>main

# Change where PostgreSQL stores its data
ORIGINAL_DATA_DIR<span class="hljs-operator">=</span><span class="hljs-string">"/var/lib/postgresql/${NEW_VERSION}/main"</span>

<span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>d <span class="hljs-string">"${ORIGINAL_DATA_DIR}"</span> ];then
  <span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>d <span class="hljs-string">"/var/lib/postgresql/${NEW_VERSION}/main.bak"</span> ]; then
    rm <span class="hljs-operator">-</span>f <span class="hljs-operator">-</span>R <span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main.bak
  fi
  sudo mv ${ORIGINAL_DATA_DIR} <span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main.bak
fi

NEW_PATH_TO_POSTGRES_DATA_DIR<span class="hljs-operator">=</span><span class="hljs-string">"/data/postgresql/${NEW_VERSION}/main"</span>

sudo mkdir <span class="hljs-operator">-</span>p ${NEW_PATH_TO_POSTGRES_DATA_DIR}
sudo chown <span class="hljs-operator">-</span>R postgres:postgres <span class="hljs-operator">/</span>data<span class="hljs-operator">/</span>postgresql
<span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>n <span class="hljs-string">"$(sudo ls -A ${NEW_PATH_TO_POSTGRES_DATA_DIR} 2>/dev/null)"</span> ]; then
  echo <span class="hljs-string">"The new PostgreSQL data directory is not empty. We will not initialize it."</span>
<span class="hljs-keyword">else</span>
  echo <span class="hljs-string">"Initializing new PostgreSQL data directory at ${NEW_PATH_TO_POSTGRES_DATA_DIR}"</span>
  sudo <span class="hljs-operator">-</span>u postgres <span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>initdb <span class="hljs-operator">-</span>D ${NEW_PATH_TO_POSTGRES_DATA_DIR}
fi
sudo sed <span class="hljs-operator">-</span>i <span class="hljs-string">"s|data_directory = '${ORIGINAL_DATA_DIR}'|data_directory = '${NEW_PATH_TO_POSTGRES_DATA_DIR}'|g"</span> <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>postgresql.conf

# Set the port to whatever we specify <span class="hljs-keyword">as</span> port in the terraform variables
sudo sed <span class="hljs-operator">-</span>i <span class="hljs-string">"s|port = .*|port = ${PORT}|g"</span> <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>postgresql.conf

# Allow remote connections
sudo sed <span class="hljs-operator">-</span>i <span class="hljs-string">"s|#listen_addresses = 'localhost'|listen_addresses = '*'|g"</span> <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>postgresql.conf
sudo sed <span class="hljs-operator">-</span>i <span class="hljs-string">"s|host    all             all             127.0.0.1/32            scram-sha-256|host    all             all             0.0.0.0/0            scram-sha-256|g"</span> <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>pg_hba.conf

# upgrade <span class="hljs-keyword">from</span> previous version <span class="hljs-keyword">if</span> needed
LAST_CLUSTER<span class="hljs-operator">=</span>$(echo <span class="hljs-string">"$CURRENT_CLUSTER"</span> <span class="hljs-operator">|</span> tail <span class="hljs-operator">-</span>n <span class="hljs-number">1</span>)
<span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>n <span class="hljs-string">"$LAST_CLUSTER"</span> ]; then
    OLD_VERSION<span class="hljs-operator">=</span>$(echo $LAST_CLUSTER <span class="hljs-operator">|</span> cut <span class="hljs-operator">-</span>d<span class="hljs-string">'-'</span> <span class="hljs-operator">-</span>f1)
    OLD_NAME<span class="hljs-operator">=</span>$(echo $LAST_CLUSTER <span class="hljs-operator">|</span> cut <span class="hljs-operator">-</span>d<span class="hljs-string">'-'</span> <span class="hljs-operator">-</span>f2)
    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"$OLD_VERSION"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"$NEW_VERSION"</span> ] <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> [ <span class="hljs-string">"$OLD_NAME"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"main"</span> ]; then
      echo <span class="hljs-string">"...no need to upgrade data, we are on the same cluster version and name"</span>
    <span class="hljs-keyword">else</span>
      echo <span class="hljs-string">"We need to upgrade the data of the last cluster ${OLD_VERSION}-${OLD_NAME}"</span>
      # We will need to install timescale db <span class="hljs-keyword">for</span> the <span class="hljs-keyword">new</span> version, otherwise the pg_upgrade will fail
      # <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
      sudo apt install timescaledb<span class="hljs-number">-2</span><span class="hljs-operator">-</span>postgresql<span class="hljs-operator">-</span>${NEW_VERSION}<span class="hljs-operator">=</span><span class="hljs-string">"${TIMESCALEDB_VERSION}"</span> timescaledb<span class="hljs-number">-2</span><span class="hljs-operator">-</span>loader<span class="hljs-operator">-</span>postgresql<span class="hljs-operator">-</span>${NEW_VERSION}<span class="hljs-operator">=</span><span class="hljs-string">"${TIMESCALEDB_VERSION}"</span> timescaledb<span class="hljs-operator">-</span>toolkit<span class="hljs-operator">-</span>postgresql<span class="hljs-operator">-</span>${NEW_VERSION} <span class="hljs-operator">-</span>y

      sudo sed <span class="hljs-operator">-</span>i <span class="hljs-string">"s|#shared_preload_libraries = ''|shared_preload_libraries = 'timescaledb'|g"</span> <span class="hljs-operator">/</span>data<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>postgresql.conf

      # Tune TimescaleDB
      sudo timescaledb<span class="hljs-operator">-</span>tune <span class="hljs-operator">-</span><span class="hljs-operator">-</span>yes
      # <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span> end of installing timescale db <span class="hljs-keyword">for</span> the <span class="hljs-keyword">new</span> version <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>

      (cd <span class="hljs-operator">/</span>tmp <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> sudo <span class="hljs-operator">-</span>u postgres <span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>${NEW_VERSION}<span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>pg_upgrade \
        <span class="hljs-operator">-</span><span class="hljs-operator">-</span>old<span class="hljs-operator">-</span>datadir<span class="hljs-operator">=</span><span class="hljs-operator">/</span>data<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>$OLD_VERSION<span class="hljs-operator">/</span>$OLD_NAME \
        <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-keyword">new</span><span class="hljs-operator">-</span>datadir<span class="hljs-operator">=</span><span class="hljs-operator">/</span>data<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>$NEW_VERSION<span class="hljs-operator">/</span>main \
        <span class="hljs-operator">-</span><span class="hljs-operator">-</span>old<span class="hljs-operator">-</span>bindir<span class="hljs-operator">=</span><span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>$OLD_VERSION<span class="hljs-operator">/</span>bin \
        <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-keyword">new</span><span class="hljs-operator">-</span>bindir<span class="hljs-operator">=</span><span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>postgresql<span class="hljs-operator">/</span>$NEW_VERSION<span class="hljs-operator">/</span>bin)
    fi
fi

# Restart PostgreSQL to apply changes
sudo systemctl restart postgresql@${NEW_VERSION}<span class="hljs-operator">-</span>main

# Wait <span class="hljs-keyword">for</span> PostgreSQL to start
sleep <span class="hljs-number">10</span>

# Create the database
sudo <span class="hljs-operator">-</span>u postgres psql <span class="hljs-operator">-</span>c <span class="hljs-string">"create database ${DB_NAME};"</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> echo <span class="hljs-string">"Database ${DB_NAME} already exists, skipping creation."</span>
</code></pre><p>I believe that the comments in-between the lines of bash script statements are enough to understand what is going on.</p><p>Note that the script makes sure that:</p><ul><li><p>it can be used to upgrade from one Postgres version to another. <strong>Downgrades</strong> don’t work though.</p></li><li><p>It makes sure that it upgrades the data too.</p></li></ul><p><strong>Installing TimescaleDB</strong></p><p>The file <code>install_timescaledb.sh</code> has the following contents:</p><p>file: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws/blob/main/terraform/install_timescaledb.sh">terraform/install_timescaledb.sh</a>.</p><pre data-type="codeBlock" text="#!/bin/bash

set -e # e: exit if any command has a non-zero exit status
set -x # x: all executed commands are printed to the terminal
set -u # u: all references to variables that have not been previously defined cause an error

POSTGRESQL_VERSION=$1
TIMESCALEDB_VERSION=&quot;${2}*&quot;
DB_NAME=$3
# Set TimescaleDB repository at system repositories so that we can install TimescaleDB
echo &quot;deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -c -s) main&quot; | sudo tee /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/timescaledb.gpg
sudo DEBIAN_FRONTEND=noninteractive apt update -y

sudo systemctl stop postgresql@${POSTGRESQL_VERSION}-main || true

# Install TimescaleDB and TimescaleDB Toolkit all TimescaleDB postgres related packages
sudo apt install timescaledb-2-postgresql-${POSTGRESQL_VERSION}=&quot;${TIMESCALEDB_VERSION}&quot; timescaledb-2-loader-postgresql-${POSTGRESQL_VERSION}=&quot;${TIMESCALEDB_VERSION}&quot; timescaledb-toolkit-postgresql-${POSTGRESQL_VERSION} -y

sudo sed -i &quot;s|#shared_preload_libraries = &apos;&apos;|shared_preload_libraries = &apos;timescaledb&apos;|g&quot; /data/postgresql/${POSTGRESQL_VERSION}/main/postgresql.conf

# Tune TimescaleDB
sudo timescaledb-tune --yes

sudo systemctl restart postgresql@${POSTGRESQL_VERSION}-main

# Create the extensions for TimescaleDB and TimescaleDB Toolkit
sudo -u postgres psql -d ${DB_NAME} -c &quot;CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;&quot;
sudo -u postgres psql -d ${DB_NAME} -c &quot;CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit CASCADE;&quot;
"><code><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -e <span class="hljs-comment"># e: exit if any command has a non-zero exit status</span>
<span class="hljs-built_in">set</span> -x <span class="hljs-comment"># x: all executed commands are printed to the terminal</span>
<span class="hljs-built_in">set</span> -u <span class="hljs-comment"># u: all references to variables that have not been previously defined cause an error</span>

POSTGRESQL_VERSION=<span class="hljs-variable">$1</span>
TIMESCALEDB_VERSION=<span class="hljs-string">"<span class="hljs-variable">${2}</span>*"</span>
DB_NAME=<span class="hljs-variable">$3</span>
<span class="hljs-comment"># Set TimescaleDB repository at system repositories so that we can install TimescaleDB</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb https://packagecloud.io/timescale/timescaledb/ubuntu/ <span class="hljs-subst">$(lsb_release -c -s)</span> main"</span> | sudo <span class="hljs-built_in">tee</span> /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo gpg --dearmor --<span class="hljs-built_in">yes</span> -o /etc/apt/trusted.gpg.d/timescaledb.gpg
sudo DEBIAN_FRONTEND=noninteractive apt update -y

sudo systemctl stop postgresql@<span class="hljs-variable">${POSTGRESQL_VERSION}</span>-main || <span class="hljs-literal">true</span>

<span class="hljs-comment"># Install TimescaleDB and TimescaleDB Toolkit all TimescaleDB postgres related packages</span>
sudo apt install timescaledb-2-postgresql-<span class="hljs-variable">${POSTGRESQL_VERSION}</span>=<span class="hljs-string">"<span class="hljs-variable">${TIMESCALEDB_VERSION}</span>"</span> timescaledb-2-loader-postgresql-<span class="hljs-variable">${POSTGRESQL_VERSION}</span>=<span class="hljs-string">"<span class="hljs-variable">${TIMESCALEDB_VERSION}</span>"</span> timescaledb-toolkit-postgresql-<span class="hljs-variable">${POSTGRESQL_VERSION}</span> -y

sudo sed -i <span class="hljs-string">"s|#shared_preload_libraries = ''|shared_preload_libraries = 'timescaledb'|g"</span> /data/postgresql/<span class="hljs-variable">${POSTGRESQL_VERSION}</span>/main/postgresql.conf

<span class="hljs-comment"># Tune TimescaleDB</span>
sudo timescaledb-tune --<span class="hljs-built_in">yes</span>

sudo systemctl restart postgresql@<span class="hljs-variable">${POSTGRESQL_VERSION}</span>-main

<span class="hljs-comment"># Create the extensions for TimescaleDB and TimescaleDB Toolkit</span>
sudo -u postgres psql -d <span class="hljs-variable">${DB_NAME}</span> -c <span class="hljs-string">"CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"</span>
sudo -u postgres psql -d <span class="hljs-variable">${DB_NAME}</span> -c <span class="hljs-string">"CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit CASCADE;"</span>
</code></pre><p>Again, this script is idempotent, and allows for upgrades of the timescale db.</p><p>It installs the TimescaleDB extension and the TimescaleDB Toolkit.</p><p>Also, it sets some common default values for TimescaleDB configuration.</p><p><strong>Setting the Password</strong></p><p>The second <code>terraform_data</code> resource is about setting the password for <code>postgres</code> user. I have it in a separate block, because it is using sensitive data and if I put it on the first block, Terraform would hide all output from that block (Postgres installation and setup commands) which I wouldn’t want.</p><p><strong>Important!</strong> This scripts introduces more variables. My <code>variables.tf</code> needs to have these too:</p><pre data-type="codeBlock" text="variable &quot;postgresql_version&quot; {
  description = &quot;The PostgreSQL version to install on the TimescaleDB server.&quot;
  type        = string
  default     = &quot;16&quot;
}

variable &quot;timescaledb_version&quot; {
  description = &quot;The TimescaleDB version to install on the TimescaleDB server.&quot;
  type        = string
  default     = &quot;2.19.3&quot;
}

variable &quot;timescaledb_server_port&quot; {
  description = &quot;Port for the TimescaleDB server.&quot;
  type        = number
  default     = 5432
}

variable &quot;timescaledb_server_postgres_password&quot; {
  description = &quot;The password for the PostgreSQL user on the TimescaleDB server.&quot;
  type        = string
  sensitive   = true
}
"><code>variable "postgresql_version" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The PostgreSQL version to install on the TimescaleDB server."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">default</span>     = <span class="hljs-string">"16"</span>
}

variable "timescaledb_version" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The TimescaleDB version to install on the TimescaleDB server."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">default</span>     = <span class="hljs-string">"2.19.3"</span>
}

variable "timescaledb_server_port" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"Port for the TimescaleDB server."</span>
  <span class="hljs-attr">type</span>        = number
  <span class="hljs-attr">default</span>     = <span class="hljs-number">5432</span>
}

variable "timescaledb_server_postgres_password" {
  <span class="hljs-attr">description</span> = <span class="hljs-string">"The password for the PostgreSQL user on the TimescaleDB server."</span>
  <span class="hljs-attr">type</span>        = string
  <span class="hljs-attr">sensitive</span>   = <span class="hljs-literal">true</span>
}
</code></pre><p><strong>Note:</strong> At the time of this writing, the latest Postgres version was <code>17</code>, but, in the file above I choose to install version <code>16</code>, to start with. Later in the post below, you will see how I choose to upgrade to <code>17</code>, in order to demonstrate the Postgres upgrade process.</p><p><strong>Important!</strong> Look also at the <code>.envrc</code> file (or other file that you keep your environment variables that set values to the Terraform input variables), you need to add the <code>TF_VAR_timescaledb_server_postgres_password=…</code></p><pre data-type="codeBlock" text="...
export TF_VAR_timescaledb_server_postgres_password=&apos;...&lt;put your postgres password here&gt;...&apos;
"><code>...
export TF_VAR_timescaledb_server_postgres_password<span class="hljs-operator">=</span><span class="hljs-string">'...&#x3C;put your postgres password here>...'</span>
</code></pre><p>If I do <code>terraform apply</code>, at the end, I have my TimescaleDB ready.</p><p>I use this to connect (from <code>terraform</code> folder):</p><pre data-type="codeBlock" text="$ (terraform output -raw psql_connect)
"><code>$ (terraform <span class="hljs-built_in">output</span> -raw psql_connect)
</code></pre><p>And then I key in the <code>postgres</code> user password and I am on a <code>psql</code> terminal.</p><p>Look at the output of some commands.</p><p><strong>Check that Extensions are Installed</strong></p><p>I connect to the <code>events_server_development</code> database and I issue the command to get the extensions installed:</p><pre data-type="codeBlock" text="postgres=# \c events_server_development 
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are now connected to database &quot;events_server_development&quot; as user &quot;postgres&quot;.
events_server_development=# select * from pg_extension;
  oid  |       extname       | extowner | extnamespace | extrelocatable | extversion |                                                                                 extconfig                                                                                 |                                                              extcondition                                                               
-------+---------------------+----------+--------------+----------------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------
 13610 | plpgsql             |       10 |           11 | f              | 1.0        |                                                                                                                                                                           | 
 17140 | timescaledb_toolkit |       10 |         2200 | f              | 1.21.0     |                                                                                                                                                                           | 
 16385 | timescaledb         |       10 |         2200 | f              | 2.19.3     | {16406,16407,16429,16443,16442,16462,16461,16477,16476,16502,16518,16519,16536,16535,16555,16556,16611,16624,16651,16664,16674,16684,16688,16704,16715,16731,16740,16739} | {&quot;&quot;,&quot;WHERE id &gt;= 1&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;WHERE id &gt;= 1000&quot;,&quot; WHERE key &lt;&gt; &apos;uuid&apos; &quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;&quot;}
(3 rows)
"><code>postgres<span class="hljs-operator">=</span># \c events_server_development 
SSL connection (protocol: TLSv1<span class="hljs-number">.3</span>, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are <span class="hljs-built_in">now</span> connected to database <span class="hljs-string">"events_server_development"</span> <span class="hljs-keyword">as</span> user <span class="hljs-string">"postgres"</span>.
events_server_development=# select <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> pg_extension;
  oid  <span class="hljs-operator">|</span>       extname       <span class="hljs-operator">|</span> extowner <span class="hljs-operator">|</span> extnamespace <span class="hljs-operator">|</span> extrelocatable <span class="hljs-operator">|</span> extversion <span class="hljs-operator">|</span>                                                                                 extconfig                                                                                 <span class="hljs-operator">|</span>                                                              extcondition                                                               
<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
 <span class="hljs-number">13610</span> <span class="hljs-operator">|</span> plpgsql             <span class="hljs-operator">|</span>       <span class="hljs-number">10</span> <span class="hljs-operator">|</span>           <span class="hljs-number">11</span> <span class="hljs-operator">|</span> f              <span class="hljs-operator">|</span> <span class="hljs-number">1.0</span>        <span class="hljs-operator">|</span>                                                                                                                                                                           <span class="hljs-operator">|</span> 
 <span class="hljs-number">17140</span> <span class="hljs-operator">|</span> timescaledb_toolkit <span class="hljs-operator">|</span>       <span class="hljs-number">10</span> <span class="hljs-operator">|</span>         <span class="hljs-number">2200</span> <span class="hljs-operator">|</span> f              <span class="hljs-operator">|</span> <span class="hljs-number">1.21</span><span class="hljs-number">.0</span>     <span class="hljs-operator">|</span>                                                                                                                                                                           <span class="hljs-operator">|</span> 
 <span class="hljs-number">16385</span> <span class="hljs-operator">|</span> timescaledb         <span class="hljs-operator">|</span>       <span class="hljs-number">10</span> <span class="hljs-operator">|</span>         <span class="hljs-number">2200</span> <span class="hljs-operator">|</span> f              <span class="hljs-operator">|</span> <span class="hljs-number">2.19</span><span class="hljs-number">.3</span>     <span class="hljs-operator">|</span> {<span class="hljs-number">16406</span>,<span class="hljs-number">16407</span>,<span class="hljs-number">16429</span>,<span class="hljs-number">16443</span>,<span class="hljs-number">16442</span>,<span class="hljs-number">16462</span>,<span class="hljs-number">16461</span>,<span class="hljs-number">16477</span>,<span class="hljs-number">16476</span>,<span class="hljs-number">16502</span>,<span class="hljs-number">16518</span>,<span class="hljs-number">16519</span>,<span class="hljs-number">16536</span>,<span class="hljs-number">16535</span>,<span class="hljs-number">16555</span>,<span class="hljs-number">16556</span>,<span class="hljs-number">16611</span>,<span class="hljs-number">16624</span>,<span class="hljs-number">16651</span>,<span class="hljs-number">16664</span>,<span class="hljs-number">16674</span>,<span class="hljs-number">16684</span>,<span class="hljs-number">16688</span>,<span class="hljs-number">16704</span>,<span class="hljs-number">16715</span>,<span class="hljs-number">16731</span>,<span class="hljs-number">16740</span>,<span class="hljs-number">16739</span>} <span class="hljs-operator">|</span> {<span class="hljs-string">""</span>,<span class="hljs-string">"WHERE id >= 1"</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">"WHERE id >= 1000"</span>,<span class="hljs-string">" WHERE key &#x3C;> 'uuid' "</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>,<span class="hljs-string">""</span>}
(<span class="hljs-number">3</span> rows)
</code></pre><p><strong>Creating a Table and A Row</strong></p><p>I am creating a table and inserting a row.</p><pre data-type="codeBlock" text="postgres=# \c events_server_development 
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.04.1), server 17.5 (Ubuntu 17.5-1.pgdg24.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are now connected to database &quot;events_server_development&quot; as user &quot;postgres&quot;.
events_server_development=# create table events (id bigint not null, type varchar not null, created_at timestamp default curren
t_timestamp not null, primary key(id));
CREATE TABLE
Time: 37,602 ms
events_server_development=# insert into events (id, type) values (1, &apos;product_sold&apos;);
INSERT 0 1
Time: 31,945 ms
events_server_development=# select * from e  vents;
 id |     type     |         created_at         
----+--------------+----------------------------
  1 | product_sold | 2025-07-06 12:37:03.418051
(1 row)

Time: 31,053 ms
"><code>postgres<span class="hljs-operator">=</span># \c events_server_development 
psql (<span class="hljs-number">16.9</span> (Ubuntu <span class="hljs-number">16.9</span><span class="hljs-operator">-</span>0ubuntu0<span class="hljs-number">.24</span><span class="hljs-number">.04</span><span class="hljs-number">.1</span>), server <span class="hljs-number">17.5</span> (Ubuntu <span class="hljs-number">17.5</span><span class="hljs-number">-1</span>.pgdg24.04+<span class="hljs-number">1</span>))
SSL connection (protocol: TLSv1<span class="hljs-number">.3</span>, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are <span class="hljs-built_in">now</span> connected to database <span class="hljs-string">"events_server_development"</span> <span class="hljs-keyword">as</span> user <span class="hljs-string">"postgres"</span>.
events_server_development=# create table events (id bigint not null, <span class="hljs-keyword">type</span> varchar not null, created_at timestamp default curren
t_timestamp not null, primary key(id));
CREATE TABLE
Time: <span class="hljs-number">37</span>,<span class="hljs-number">602</span> ms
events_server_development<span class="hljs-operator">=</span># insert into events (id, <span class="hljs-keyword">type</span>) values (<span class="hljs-number">1</span>, <span class="hljs-string">'product_sold'</span>);
INSERT <span class="hljs-number">0</span> <span class="hljs-number">1</span>
Time: <span class="hljs-number">31</span>,<span class="hljs-number">945</span> ms
events_server_development<span class="hljs-operator">=</span># select <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> e  vents;
 id <span class="hljs-operator">|</span>     <span class="hljs-keyword">type</span>     <span class="hljs-operator">|</span>         created_at         
<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">+</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
  <span class="hljs-number">1</span> <span class="hljs-operator">|</span> product_sold <span class="hljs-operator">|</span> <span class="hljs-number">2025</span><span class="hljs-operator">-</span>07<span class="hljs-operator">-</span>06 <span class="hljs-number">12</span>:<span class="hljs-number">37</span>:03<span class="hljs-number">.418051</span>
(<span class="hljs-number">1</span> row)

Time: <span class="hljs-number">31</span>,053 ms
</code></pre><p>So, I have created the table <code>events</code> and I have added 1 row into it.</p><p>Let’s now see some scenarios under which we would like to keep our data intact.</p><h3 id="h-what-if-ec2-instance-is-upgraded" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What if EC2 Instance is Upgraded?</h3><p>The current EC2 instance type is <code>t3.xlarge</code>. What If I want to scale up to <code>t3.2xlarge</code> for example.</p><p>I update the default value of the variable <code>timescaledb_server_instance_type</code> to <code>t3.2xlarge</code> and I do <code>terraform apply</code>.</p><p>The plan presented is very simple:</p><pre data-type="codeBlock" text="  # aws_instance.timescaledb will be updated in-place
  ~ resource &quot;aws_instance&quot; &quot;timescaledb&quot; {
        id                                   = &quot;i-069be2f31daf52118&quot;
      ~ instance_type                        = &quot;t3.xlarge&quot; -&gt; &quot;t3.2xlarge&quot;
      ~ public_dns                           = &quot;ec2-63-178-242-62.eu-central-1.compute.amazonaws.com&quot; -&gt; (known after apply)
      ~ public_ip                            = &quot;63.178.242.62&quot; -&gt; (known after apply)
        tags                                 = {
            &quot;Name&quot; = &quot;setting_up_timescaledb_on_aws-development-timescaledb&quot;
        }
        # (35 unchanged attributes hidden)

        # (9 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
"><code>  # aws_instance.timescaledb will be updated in<span class="hljs-operator">-</span>place
  <span class="hljs-operator">~</span> resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"timescaledb"</span> {
        id                                   <span class="hljs-operator">=</span> <span class="hljs-string">"i-069be2f31daf52118"</span>
      <span class="hljs-operator">~</span> instance_type                        <span class="hljs-operator">=</span> <span class="hljs-string">"t3.xlarge"</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-string">"t3.2xlarge"</span>
      <span class="hljs-operator">~</span> public_dns                           <span class="hljs-operator">=</span> <span class="hljs-string">"ec2-63-178-242-62.eu-central-1.compute.amazonaws.com"</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> (known after apply)
      <span class="hljs-operator">~</span> public_ip                            <span class="hljs-operator">=</span> <span class="hljs-string">"63.178.242.62"</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> (known after apply)
        tags                                 <span class="hljs-operator">=</span> {
            <span class="hljs-string">"Name"</span> <span class="hljs-operator">=</span> <span class="hljs-string">"setting_up_timescaledb_on_aws-development-timescaledb"</span>
        }
        # (<span class="hljs-number">35</span> unchanged attributes hidden)

        # (<span class="hljs-number">9</span> unchanged blocks hidden)
    }

Plan: <span class="hljs-number">0</span> to add, <span class="hljs-number">1</span> to change, <span class="hljs-number">0</span> to destroy.
</code></pre><p>Only a <em>change</em> of the instance type.</p><p>I <em>apply</em> and then I connect to TimescaleDB with <code>psql</code>. It works. My data are still there.</p><h3 id="h-what-if-ec2-instance-is-replaced" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What if EC2 Instance is Replaced?</h3><p>I do <code>terraform taint aws_instance.timescaledb</code> to mark the EC2 instance as something that needs to be destroyed and created again by Terraform.</p><p>Then I do <code>terraform apply</code>. I see that I am being presented with a plan to</p><ul><li><p>destroy and create the EC2 instance</p></li><li><p>destroy and recreate the EBS volume attachment.</p></li><li><p>destroy and recreate the <code>terraform_data</code> <code>prepare_ebs_volume_for_writing</code>.</p></li><li><p>destroy and recreate the <code>terraform_data</code> <code>install_and_setup_timescaledb</code>.</p></li></ul><p>It seems that this is exactly what I want. New EC2 instance and the EBS volume to be detached from the old and attached to the new. Plus, installation of postgres on the new machine.</p><p>I proceed.</p><p>Then I test that I can connect with <code>psql</code> and that I can find my data in place.</p><p>Boom! Data there! All good!</p><h3 id="h-what-if-the-ebs-volume-is-scaled-up" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What if the EBS Volume is Scaled Up?</h3><p>I go to the <code>ec2_volume.tf</code> file and I increase the storage size from <code>64</code> to <code>128</code>.</p><p>Then I do <code>terraform apply</code>.</p><p>I am being presented with a simple plan that the EBS volume will be updated in-place.</p><p>I then connect to the database and I can still see my data intact.</p><h3 id="h-what-if-postgres-version-is-upgraded" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What If Postgres Version is Upgraded?</h3><p>I change the default value of the <code>postgresql_version</code> variable from <code>16</code> to <code>17</code>. Thanks to the <code>triggers_replace</code> of the resources <code>install_and_setup_timescaledb</code> and <code>postgres_password</code>, these resources will be recreated/replaced.</p><p>After I do <code>terraform apply</code> I then connect to the Postgres server. I can still see my <code>events</code> table and <code>events</code> record.</p><h3 id="h-what-if-timescaledb-version-is-upgraded" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What If TimescaleDB Version is Upgraded?</h3><p>This <strong>is not supported</strong> by the current script. Upgrading the TimescaleDB minor and/or major version is a quite involved process which is described here: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.tigerdata.com/self-hosted/latest/upgrades/">Upgrade TimescaleDB</a>.</p><h3 id="h-what-could-be-improved" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What Could Be Improved?</h3><p>This setup is good to start, but it has plenty of room for improvement in terms of production readiness</p><ul><li><p>One can implement a read-replica of the main database. The read-replica could be used to scale the server horizontally, by serving the read requests.</p></li><li><p>Automated backups and quick restore from backups should be set up.</p></li><li><p>Cloudwatch alarms should be implemented on</p><ul><li><p>CPU</p></li><li><p>Memory</p></li><li><p>Free Storage</p></li></ul></li><li><p>Allow direct access only from within VPC.</p></li><li><p>Implement a Multi-AZ deployment and automatic switch over from master to stand-by server on another zone.</p></li><li><p>Use of AWS Systems Manager to install Postgres and TimescaleDB and manage upgrades.</p></li><li><p>Have a Terraform provider that would allow setting TimescaleDB specific parameters.</p></li><li><p>Attach the same Elastic IP, in case one wants to expose the same public IP.</p></li></ul><h3 id="h-closing-thoughts" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Closing Thoughts</h3><p>Running your own, self-hosted TimescaleDB on AWS is quite possible. The above work demonstrates that. However, it needs more work if this was to become a real production environment.</p><hr><p>gm! I am Panos Matsinopoulos, humble software reader, writer, classical music and V8 engine lover. I work for <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com/">Talent Protocol</a>, where we make builders get the recognition they deserve.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/ddec42a2a65c131f6e8ea5a1ad05737d22cda4085854bb6bec88d8d68211d60f.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Querying a Daily Stats Table - Postgres]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/querying-a-daily-stats-table-postgres</link>
            <guid>TfFw0C4VnkY6vyL7ulSj</guid>
            <pubDate>Sun, 04 May 2025 08:25:58 GMT</pubDate>
            <description><![CDATA[Hero Image by Panos Matsinopoulos on ZoraThe TableI have a table named daily_stats, which keeps track of some daily metrics. Here it is:Example rows from table \`daily_stats\`The structure of the table:-- CREATE TABLE "daily_stats" ---------------------------------- CREATE TABLE "public"."daily_stats" ( "id" BigInt DEFAULT nextval('daily_stats_id_seq'::regclass) NOT NULL, "metric" Character Varying NOT NULL, "date" Date NOT NULL, "count" BigInt DEFAULT 0 NOT NULL, "lock_version" BigInt DEFAUL...]]></description>
            <content:encoded><![CDATA[<p>Hero Image by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://zora.co/@panosm">Panos Matsinopoulos</a> on <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://zora.co/coin/base:0xf003651e8e450db99b3fac9d950580710c5cb0e5?referrer=0x324e9e13dd19528d0f390201923d17c4b7e94462">Zora</a></p><h2 id="h-the-table" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The Table</h2><p>I have a table named <code>daily_stats</code>, which keeps track of some daily metrics. Here it is:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/24b6ea9fc7cf7740cef9b1fb5c8b04a3d3b5c621d0054a9ebc11fff5bbf6a9ad.png" alt="Example rows from table \`daily_stats\`" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Example rows from table \`daily_stats\`</figcaption></figure><p>The structure of the table:</p><pre data-type="codeBlock" text="-- CREATE TABLE &quot;daily_stats&quot; ----------------------------------
CREATE TABLE &quot;public&quot;.&quot;daily_stats&quot; ( 
  &quot;id&quot; BigInt DEFAULT nextval(&apos;daily_stats_id_seq&apos;::regclass) NOT NULL,
  &quot;metric&quot; Character Varying NOT NULL,
  &quot;date&quot; Date NOT NULL,
  &quot;count&quot; BigInt DEFAULT 0 NOT NULL,
  &quot;lock_version&quot; BigInt DEFAULT 0 NOT NULL,
  &quot;created_at&quot; Timestamp( 6 ) Without Time Zone NOT NULL,
  &quot;updated_at&quot; Timestamp( 6 ) Without Time Zone NOT NULL,
  PRIMARY KEY ( &quot;id&quot; ) );
 ;
-- -------------------------------------------------------------

-- CREATE INDEX &quot;daily_stats_metric_date_uidx&quot; -----------------
CREATE UNIQUE INDEX &quot;daily_stats_metric_date_uidx&quot; ON &quot;public&quot;.&quot;daily_stats&quot; USING btree( &quot;metric&quot; Asc NULLS Last, &quot;date&quot; Asc NULLS Last );
-- -------------------------------------------------------------
"><code><span class="hljs-operator">-</span><span class="hljs-operator">-</span> CREATE TABLE <span class="hljs-string">"daily_stats"</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
CREATE TABLE <span class="hljs-string">"public"</span>."daily_stats<span class="hljs-string">" ( 
  "</span>id<span class="hljs-string">" BigInt DEFAULT nextval('daily_stats_id_seq'::regclass) NOT NULL,
  "</span>metric<span class="hljs-string">" Character Varying NOT NULL,
  "</span>date<span class="hljs-string">" Date NOT NULL,
  "</span>count<span class="hljs-string">" BigInt DEFAULT 0 NOT NULL,
  "</span>lock_version<span class="hljs-string">" BigInt DEFAULT 0 NOT NULL,
  "</span>created_at<span class="hljs-string">" Timestamp( 6 ) Without Time Zone NOT NULL,
  "</span>updated_at<span class="hljs-string">" Timestamp( 6 ) Without Time Zone NOT NULL,
  PRIMARY KEY ( "</span>id<span class="hljs-string">" ) );
 ;
-- -------------------------------------------------------------

-- CREATE INDEX "</span>daily_stats_metric_date_uidx<span class="hljs-string">" -----------------
CREATE UNIQUE INDEX "</span>daily_stats_metric_date_uidx<span class="hljs-string">" ON "</span><span class="hljs-keyword">public</span><span class="hljs-string">"."</span>daily_stats<span class="hljs-string">" USING btree( "</span>metric<span class="hljs-string">" Asc NULLS Last, "</span>date<span class="hljs-string">" Asc NULLS Last );
-- -------------------------------------------------------------
</span></code></pre><p>I have 3 columns, mainly:</p><ul><li><p><code>metric</code> which identifies the metric I keep counts for</p></li><li><p><code>date</code> which identifies the date I keep counts for</p></li><li><p><code>count</code> which is the actual count for the given <code>metric</code> and <code>date</code></p></li></ul><p>And, as you can see from the <code>CREATE INDEX…</code> above, the (<code>metric</code>,<code>date</code>) pair has a unique index, which means that I can’t have the same <code>metric</code> for the same <code>date</code> more than once.</p><p>I will not tell you how I populate this table. It is out of scope of this blog post. But I will tell you how I built some useful queries on top of it.</p><h2 id="h-querying-by-a-date-range" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Querying by a Date Range</h2><p>I want to get a result set for a given date range, <code>date_from</code> .. <code>date_to</code>.</p><p>I can just simply do:</p><p>(assuming that <code>date_from = ‘2025-04-29’</code> and <code>date_to = ‘2025-05-03’</code>)</p><pre data-type="codeBlock" text="select * from &quot;daily_stats&quot; 
where date between &apos;2025-04-29&apos; and &apos;2025-05-03&apos;
order by date desc
"><code><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> "daily_stats" 
<span class="hljs-keyword">where</span> <span class="hljs-type">date</span> <span class="hljs-keyword">between</span> <span class="hljs-string">'2025-04-29'</span> <span class="hljs-keyword">and</span> <span class="hljs-string">'2025-05-03'</span>
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">desc</span>
</code></pre><p>That is good and easy!</p><p>But <strong>what if a metric doesn’t have a count entry</strong> for a given date within the range?</p><p>Is this a problem? Generally not, but if you want to send the results to the front-end to draw a line chart with the date series from the first date to the last, there will be gaps.</p><h2 id="h-querying-by-a-date-range-filling-in-the-missing-dates-with-count-0" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Querying by a Date Range - Filling in the Missing Dates With Count <code>0</code></h2><p>Look at the result set which the previous, simple query, might return:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d04723c96a97ba1abf2b0fc94ff2fb106e41d23a794afa788ed4384b6a5e4571.png" alt="\`base_basename\` metric does not have count for 2025-05-02" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">\`base_basename\` metric does not have count for 2025-05-02</figcaption></figure><p>How can I return <code>0</code> for that date for that <code>metric</code>?</p><h3 id="h-step-1-build-a-result-set-with-all-dates-within-date-range" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 1 - Build a Result Set with All Dates Within Date Range</h3><p>First, I need to build a set will all the dates within date range.</p><p>I want something like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/67ec7667849b75ef314ba7afba82034817fb375212c9118915e7ef843ca9b3d6.png" alt="Date Series from &apos;2025-04-25&apos; to &apos;2025-05-03&apos;" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Date Series from &apos;2025-04-25&apos; to &apos;2025-05-03&apos;</figcaption></figure><p>This is quite easy in Postgres. The select I need is this:</p><pre data-type="codeBlock" text=" select generate_series(
    date &apos;2025-04-25&apos;,
    date &apos;2025-05-03&apos;,
    interval &apos;1 day&apos;
)::date as date
"><code> <span class="hljs-keyword">select</span> generate_series(
    <span class="hljs-type">date</span> <span class="hljs-string">'2025-04-25'</span>,
    <span class="hljs-type">date</span> <span class="hljs-string">'2025-05-03'</span>,
    <span class="hljs-type">interval</span> <span class="hljs-string">'1 day'</span>
)::<span class="hljs-type">date</span> <span class="hljs-keyword">as</span> <span class="hljs-type">date</span>
</code></pre><p>One could say: Let’s just do <code>left join</code> with the <code>daily_stats</code> on the <code>date</code> column. So, for each <code>date</code> that does not exist in <code>daily_stats</code> it will just give <code>NULL</code> which can be <code>coalesce</code> to <code>0</code>. But this is not correct. Because we want a <code>0</code> entry for each <code>metric</code> that does not have counter for the particular date.</p><h3 id="h-step-2-all-metrics" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 2 - All Metrics</h3><p>Lets first get all the metrics.</p><pre data-type="codeBlock" text="select distinct metric from daily_stats;
"><code><span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> metric <span class="hljs-keyword">from</span> daily_stats;
</code></pre><p>This returns something like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f8eabd6b4c5cee310e3eb8659bd391a5d76c01a01e0de42ab9ad6344dac228ce.png" alt="All metrics" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">All metrics</figcaption></figure><h3 id="h-step-3-dates-and-metrics-combinations" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 3 - Dates and Metrics Combinations</h3><p>Now, I want all the dates with all the metrics combinations.</p><p>The key here is <code>cross join</code>:</p><p>I will use the following select which also incorporates <code>with</code> i.e. the <code>CTE</code> in Postgres:</p><blockquote><p>Want to read more about CTEs? <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.postgresql.org/docs/current/queries-with.html">Read this</a>.</p></blockquote><pre data-type="codeBlock" text="with all_dates as (
  select generate_series(
     date &apos;2025-04-25&apos;,
     date &apos;2025-05-03&apos;,
     interval &apos;1 day&apos;
   )::date as date
),

all_metrics as (
  select distinct metric from daily_stats
)
 
select * from all_dates
cross join all_metrics
order by all_dates.date desc, all_metrics.metric asc
;
"><code><span class="hljs-keyword">with</span> all_dates <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> generate_series(
     <span class="hljs-type">date</span> <span class="hljs-comment">'2025-04-25',</span>
     <span class="hljs-type">date</span> <span class="hljs-comment">'2025-05-03',</span>
     interval <span class="hljs-comment">'1 day'</span>
   )::<span class="hljs-type">date</span> <span class="hljs-keyword">as</span> <span class="hljs-type">date</span>
),

all_metrics <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> metric <span class="hljs-keyword">from</span> daily_stats
)
 
<span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> all_dates
cross <span class="hljs-keyword">join</span> all_metrics
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> all_dates.<span class="hljs-type">date</span> desc, all_metrics.metric asc
;
</code></pre><p>When I run this, it gives me a result set like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7c8de03c74a96dc4218c927e2b3f4bccb38af433ee115c0c6a81688303a03d6b.png" alt="Dates Metrics Combinations" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Dates Metrics Combinations</figcaption></figure><h3 id="h-step-4-final-left-join-with-daily-stats" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 4 - Final: Left Join With Daily Stats</h3><p>Now that I have all the combinations of dates and metrics, I can <code>left join</code> with the <code>daily_stats</code> and for the cases which I don’t have a count for, I will just display a <code>0</code>.</p><pre data-type="codeBlock" text="with all_dates as (
  select generate_series(
     date &apos;2025-04-25&apos;,
     date &apos;2025-05-03&apos;,
     interval &apos;1 day&apos;
   )::date as date
),

all_metrics as (
  select distinct metric from daily_stats
),

all_dates_and_metrics as (
  select * from all_dates
  cross join all_metrics
)

select
  adm.date,
  adm.metric,
  coalesce(ds.count, 0) as count
from all_dates_and_metrics adm
left join daily_stats ds on ds.date = adm.date and ds.metric = adm.metric
order by adm.date desc, adm.metric asc
;
"><code>with all_dates <span class="hljs-keyword">as</span> (
  select generate_series(
     date <span class="hljs-string">'2025-04-25'</span>,
     date <span class="hljs-string">'2025-05-03'</span>,
     interval <span class="hljs-string">'1 day'</span>
   )::date <span class="hljs-keyword">as</span> date
),

all_metrics <span class="hljs-keyword">as</span> (
  select distinct metric <span class="hljs-keyword">from</span> daily_stats
),

all_dates_and_metrics <span class="hljs-keyword">as</span> (
  select <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> all_dates
  cross join all_metrics
)

select
  adm.date,
  adm.metric,
  coalesce(ds.count, <span class="hljs-number">0</span>) <span class="hljs-keyword">as</span> count
<span class="hljs-keyword">from</span> all_dates_and_metrics adm
left join daily_stats ds on ds.date <span class="hljs-operator">=</span> adm.date and ds.metric <span class="hljs-operator">=</span> adm.metric
order by adm.date desc, adm.metric asc
;
</code></pre><p>This returns the results with the missing dates filled with <code>0</code>s:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fba22b039cf645bc4f5241b4eb946152d8aaed187ec224926f4442f4218d69e5.png" alt="Missing dates filled with \`0\`s" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Missing dates filled with \`0\`s</figcaption></figure><p>Which allows me to draw nice line charts for all the metrics:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3e07f0f869475ee6dbf660a3fbc5e2ba7c7b912c23b66ead207098331e85d56b.png" alt="Line chart with all the metrics" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Line chart with all the metrics</figcaption></figure><h2 id="h-getting-cumulative-counts" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Getting Cumulative Counts</h2><p><code>daily_stats#count</code> gives me the count for a given metric for a given date.</p><p>What if I want to get the <code>cumulative count</code>. I.e. the accumulated count up until a date, still with a date range?</p><h3 id="h-step-1-cumulative-counts-on-daily-stats" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 1 - Cumulative Counts on Daily Stats</h3><p>Getting the cumulative counts from the daily stats is easy:</p><pre data-type="codeBlock" text="select
  date,
  metric,
  sum(count) over (
    partition by metric
    order by date asc
    rows between unbounded preceding and current row
) as cumulative_count
from daily_stats
order by date desc, metric asc
"><code><span class="hljs-keyword">select</span>
  <span class="hljs-type">date</span>,
  metric,
  <span class="hljs-built_in">sum</span>(count) <span class="hljs-keyword">over</span> (
    <span class="hljs-keyword">partition</span> <span class="hljs-keyword">by</span> metric
    <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">asc</span>
    <span class="hljs-keyword">rows</span> <span class="hljs-keyword">between</span> unbounded preceding <span class="hljs-keyword">and</span> <span class="hljs-keyword">current</span> <span class="hljs-type">row</span>
) <span class="hljs-keyword">as</span> cumulative_count
<span class="hljs-keyword">from</span> daily_stats
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">desc</span>, metric <span class="hljs-keyword">asc</span>
</code></pre><p>We use the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.postgresql.org/docs/current/tutorial-window.html">window function</a> <code>sum(count) over (…)</code> The <code>partition by metric</code> would have been enough if I wanted sums by metric only. But here we want to partition our data by <code>metric</code>, but for each partition, to order them by <code>date</code> and sum all the counts up to the <code>current row</code>. This is what the window function does.</p><p>The above will return data like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/acc70148056af4cdffca785a638dafff70669435393a2294ce1d47ec2ed028bd.png" alt="Accumulation works, But Dates Are Missing" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Accumulation works, But Dates Are Missing</figcaption></figure><p>Although accumulation works, the dates for which a metric does not have a <code>count</code> are missing.</p><blockquote><p><strong>Important:</strong> This result set, on the first date of the date range, i.e. on <code>2025-04-25</code>, it has the correct accumulated values up until that date:</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7956ab6c97848320cc91841e33c915d8f2e6daf528af32c4d2c3d20f79b127fd.png" alt="Correct values on the first date of the range" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Correct values on the first date of the range</figcaption></figure><p>Can we just generate the date series and <code>left join</code> as we did with the previous case?</p><p>Not exactly, because we don’t want to fill in the missing rows with <code>0</code>. For example, the <code>base_basename</code> case for <code>2025-05-02</code> should be <code>660,101</code> which is the value the metric had on the <code>2025-04-30</code>, the latest date before the <code>2025-05-02</code> that it had its value accumulated.</p><h3 id="h-step-2-date-series-dates-metrics-combinations-cumulative-left-join-lateral" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Step 2 - Date Series + Dates Metrics Combinations + Cumulative + <code>Left Join</code>+ <code>lateral</code></h3><p>The solution is to use <code>left join</code> and <code>lateral</code> sub-query.</p><p>But, before we see the actual solution, let’s see what is NOT the solution:</p><p><strong>Don’t do this. This is not the solution:</strong></p><p>One might say: let’s use the window function on the result set of the previous case:</p><pre data-type="codeBlock" text="with all_dates as (
  select generate_series(
     date &apos;2025-04-25&apos;,
     date &apos;2025-05-03&apos;,
     interval &apos;1 day&apos;
   )::date as date
),

all_metrics as (
  select distinct metric from daily_stats
),

all_dates_and_metrics as (
  select * from all_dates
  cross join all_metrics
),

with_all_counts as (
  select
    adm.date,
    adm.metric,
    coalesce(ds.count, 0) as count
  from all_dates_and_metrics adm
  left join daily_stats ds on ds.date = adm.date and ds.metric = adm.metric
)

select
  date,
  metric,
  sum(count) over (
    partition by metric
    order by date asc
    rows between unbounded preceding and current row
  ) as cumulative_count
from with_all_counts
order by date desc, metric asc
;
"><code><span class="hljs-keyword">with</span> all_dates <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> generate_series(
     <span class="hljs-type">date</span> <span class="hljs-string">'2025-04-25'</span>,
     <span class="hljs-type">date</span> <span class="hljs-string">'2025-05-03'</span>,
     <span class="hljs-type">interval</span> <span class="hljs-string">'1 day'</span>
   )::<span class="hljs-type">date</span> <span class="hljs-keyword">as</span> <span class="hljs-type">date</span>
),

all_metrics <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> metric <span class="hljs-keyword">from</span> daily_stats
),

all_dates_and_metrics <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> all_dates
  <span class="hljs-keyword">cross</span> <span class="hljs-keyword">join</span> all_metrics
),

with_all_counts <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span>
    adm.date,
    adm.metric,
    <span class="hljs-built_in">coalesce</span>(ds.count, <span class="hljs-number">0</span>) <span class="hljs-keyword">as</span> count
  <span class="hljs-keyword">from</span> all_dates_and_metrics adm
  <span class="hljs-keyword">left</span> <span class="hljs-keyword">join</span> daily_stats ds <span class="hljs-keyword">on</span> ds.date <span class="hljs-operator">=</span> adm.date <span class="hljs-keyword">and</span> ds.metric <span class="hljs-operator">=</span> adm.metric
)

<span class="hljs-keyword">select</span>
  <span class="hljs-type">date</span>,
  metric,
  <span class="hljs-built_in">sum</span>(count) <span class="hljs-keyword">over</span> (
    <span class="hljs-keyword">partition</span> <span class="hljs-keyword">by</span> metric
    <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">asc</span>
    <span class="hljs-keyword">rows</span> <span class="hljs-keyword">between</span> unbounded preceding <span class="hljs-keyword">and</span> <span class="hljs-keyword">current</span> <span class="hljs-type">row</span>
  ) <span class="hljs-keyword">as</span> cumulative_count
<span class="hljs-keyword">from</span> with_all_counts
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">desc</span>, metric <span class="hljs-keyword">asc</span>
;
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/74aef5996a8db7a3ae682b2812a65cd9092aebcbce4798d719679bf1325b2cee.png" alt="Window Function on Previous Query" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Window Function on Previous Query</figcaption></figure><p>This is not correct, because it accumulates from the first date of the range specified, i.e. the <code>2025-04-25</code> and not from the first date of the whole set in the <code>daily_stats</code> whichever this might be. In other words, the <code>count</code> of a metric for the <code>2025-04-25</code> <strong>is NOT accumulated value</strong>.</p><p>If you run this, you will see that the value for <code>base_basename</code> on <code>2025-04-25</code> is <code>460</code> which is not the accumulated value for this metric up to that date. This, of course, makes the whole result set being wrong, as it accumulates values on top of the value counted on <code>2025-04-25</code>.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c2e08be2a70c9cc942f2904d70049153b75adde57e4f6610ec7dc198a9c40f96.png" alt="Wrong Accumulated Value for \`2025-04-25\`" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Wrong Accumulated Value for \`2025-04-25\`</figcaption></figure><p>Hence, the accumulated values need to be calculated on the whole <code>daily_stats</code> and not limited to the date range I want the graph/chart for.</p><p>In other words, I first need to accumulate and then limit by date range.</p><p><strong>The correct SQL is the following:</strong></p><pre data-type="codeBlock" text="with all_dates as (
  select generate_series(
     date &apos;2025-04-25&apos;,
     date &apos;2025-05-03&apos;,
     interval &apos;1 day&apos;
   )::date as date
),

all_metrics as (
  select distinct metric from daily_stats
),

all_dates_and_metrics as (
  select * from all_dates
  cross join all_metrics
),

cumulative_counts as (
  select
    date,
    metric,
    sum(count) over (
      partition by metric
      order by date asc
      rows between unbounded preceding and current row
    ) as cumulative_count
  from daily_stats
)

select
  adm.date,
  adm.metric,
  coalesce(cc_top.cumulative_count::int, 0) as count
  from
    all_dates_and_metrics adm
  left join lateral (
    select
      cumulative_count
    from cumulative_counts cc
    where cc.metric = adm.metric and cc.date &lt;= adm.date
    order by
      cc.date desc limit 1
  ) cc_top on true
  
order by date desc, metric asc
;
"><code><span class="hljs-keyword">with</span> all_dates <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> generate_series(
     <span class="hljs-type">date</span> <span class="hljs-string">'2025-04-25'</span>,
     <span class="hljs-type">date</span> <span class="hljs-string">'2025-05-03'</span>,
     <span class="hljs-type">interval</span> <span class="hljs-string">'1 day'</span>
   )::<span class="hljs-type">date</span> <span class="hljs-keyword">as</span> <span class="hljs-type">date</span>
),

all_metrics <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> metric <span class="hljs-keyword">from</span> daily_stats
),

all_dates_and_metrics <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> all_dates
  <span class="hljs-keyword">cross</span> <span class="hljs-keyword">join</span> all_metrics
),

cumulative_counts <span class="hljs-keyword">as</span> (
  <span class="hljs-keyword">select</span>
    <span class="hljs-type">date</span>,
    metric,
    <span class="hljs-built_in">sum</span>(count) <span class="hljs-keyword">over</span> (
      <span class="hljs-keyword">partition</span> <span class="hljs-keyword">by</span> metric
      <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">asc</span>
      <span class="hljs-keyword">rows</span> <span class="hljs-keyword">between</span> unbounded preceding <span class="hljs-keyword">and</span> <span class="hljs-keyword">current</span> <span class="hljs-type">row</span>
    ) <span class="hljs-keyword">as</span> cumulative_count
  <span class="hljs-keyword">from</span> daily_stats
)

<span class="hljs-keyword">select</span>
  adm.date,
  adm.metric,
  <span class="hljs-built_in">coalesce</span>(cc_top.cumulative_count::<span class="hljs-type">int</span>, <span class="hljs-number">0</span>) <span class="hljs-keyword">as</span> count
  <span class="hljs-keyword">from</span>
    all_dates_and_metrics adm
  <span class="hljs-keyword">left</span> <span class="hljs-keyword">join</span> <span class="hljs-keyword">lateral</span> (
    <span class="hljs-keyword">select</span>
      cumulative_count
    <span class="hljs-keyword">from</span> cumulative_counts cc
    <span class="hljs-keyword">where</span> cc.metric <span class="hljs-operator">=</span> adm.metric <span class="hljs-keyword">and</span> cc.date <span class="hljs-operator">&#x3C;=</span> adm.date
    <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span>
      cc.date <span class="hljs-keyword">desc</span> limit <span class="hljs-number">1</span>
  ) cc_top <span class="hljs-keyword">on</span> <span class="hljs-literal">true</span>
  
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-type">date</span> <span class="hljs-keyword">desc</span>, metric <span class="hljs-keyword">asc</span>
;
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/aff76547f1392b325c301b6b9e7f9c8625f790d70a769f53a1260805de838d6c.png" alt="First Accumulate, then left join literal" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">First Accumulate, then left join literal</figcaption></figure><p>As you can see above, the accumulation takes place on the whole <code>daily_stats</code> set, and then we join.</p><p>But we don’t just do <code>left join</code> because this would have given us <code>0</code> instead of accumulated values. We use <code>left join</code> and <code>lateral subquery</code>. Read about the <code>lateral</code> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-LATERAL">here</a>.</p><p>The sub-query:</p><pre data-type="codeBlock" text="select
  cumulative_count
from cumulative_counts cc
where cc.metric = adm.metric and cc.date &lt;= adm.date
order by cc.date desc limit 1
"><code>select
  cumulative_count
<span class="hljs-keyword">from</span> cumulative_counts cc
where cc.metric <span class="hljs-operator">=</span> adm.metric and cc.date <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> adm.date
order by cc.date desc limit <span class="hljs-number">1</span>
</code></pre><p>is using a reference to the <code>date</code> and <code>metric</code> columns of the preceding <code>from</code> item, i.e. of the <code>from all_dates_and_metrics adm</code>. This is possible thanks to <code>lateral</code>.</p><p>So, we <code>left join</code> the <code>all_dates_and_metrics</code> with the result of the sub-query. The result of the sub-query is always a single row (thanks to <code>limit 1</code>) and it is the latest row we have accumulation value for the metric and date at hand. Note that the join is <strong>unconditional</strong> thanks to the <code>on true</code>.</p><p>Here is the result set (first and last parts shown only) returned by this query:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/afb399a0e23f748423c6f2d6be0440fb11e1bccb287a7fd12c94957e78fdfd99.png" alt="First part" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">First part</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/082330839e5bdd8c5cd3973d022fbb8ba9a1885c8a58f1667a51905e0b4952b7.png" alt="Last part" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Last part</figcaption></figure><p>It looks that it works! 🎉</p><hr><p>gm! I am Panos Matsinopoulos, humble software reader, writer, classical music and V8 engine lover. I work for <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com">Talent Protocol</a>, where we make builders get the recognition they deserve.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/280b4b4304800b867a44df3c3b76037660490a9b084e5b090742fc7a67cc23d8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Singleton, Unique Jobs, Concurrency - Ruby on Rails]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/singleton-unique-jobs-concurrency-ruby-on-rails</link>
            <guid>BuIteci7JkuFbGW8wSH6</guid>
            <pubDate>Sun, 13 Apr 2025 06:12:02 GMT</pubDate>
            <description><![CDATA[Hero Image by Ahmad Bello from PixabayIntroductionIn a Ruby on Rails project, creating a job to run in the background is as simple asDefining the job class / execution codeSending a job instance into a queueHave a worker execute that instanceBut all things that can go wrong when multiple users are trying to access the same shared state, a.k.a. concurrency issues, the same wrong things can happen when multiple processes / threads (let’s call them workers) are trying to execute a background tas...]]></description>
            <content:encoded><![CDATA[<p>Hero Image by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com/users/gpop_yef-26469207/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=7832779">Ahmad Bello</a> from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=7832779">Pixabay</a></p><h2 id="h-introduction" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Introduction</h2><p>In a Ruby on Rails project, creating a job to run in the background is as simple as</p><ol><li><p>Defining the job class / execution code</p></li><li><p>Sending a job instance into a queue</p></li><li><p>Have a worker execute that instance</p></li></ol><p>But all things that can go wrong when multiple <em>users</em> are trying to access the same shared state, a.k.a. <strong>concurrency issues</strong>, the same wrong things can happen when multiple processes / threads (let’s call them workers) are trying to execute a background task.</p><p>Also, there might be reasons when you want a background task to be executed only once. How do you prevent it from being enqueued many times?</p><p>Let’s see various scenarios of concurrent access to shared state and possible solutions.</p><p>This blog post is long, but still not exhaustive of the subject.</p><h2 id="h-tools-suggested-as-solutions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Tools Suggested As Solutions</h2><p>Before I delve into details, here is the list of tools suggested as possible solutions to various problems:</p><ul><li><p>Synchronize access to parts of the code using a <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ruby-doc.org/core-3.1.1/Thread/Mutex.html">Mutex</a>.</p></li><li><p>Use <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/Persistence.html#method-i-increment-21">ActiveRecord#increment!</a> to update an integer value atomically.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html">Optimistic Locking</a>.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html">Pessimistic Locking - ActiveRecord API</a>.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS">Postgres Advisory Locks</a>.</p></li><li><p>Enqueued Jobs table.</p></li><li><p><code>Sidekiq::Shutdown</code> Exception.</p></li><li><p>Other Solutions like <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sidekiq/sidekiq/wiki/Ent-Unique-Jobs">Sidekiq Enterprise</a>, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/mhenrixon/sidekiq-unique-jobs">Sidekiq Unique Jobs</a>, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/fatkodima/sidekiq-iteration">Sidekiq Iteration</a>.</p></li></ul><h2 id="h-context" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Context:</h2><h3 id="h-ruby-on-rails-application" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Ruby on Rails application</h3><p>A Ruby on Rails application. <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/singleton_sidekiq_jobs">Repository here</a>.</p><h3 id="h-model" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Model</h3><p>A simple data model, <code>User</code> , stored in <code>users</code> table, and <code>UserBalance</code> stored in table <code>user_balances</code>.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/199d62a10c99bb31728e303467dc1819d8452971d4ea112afa2ee460d7bb3322.png" alt="Simple Data Model" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Simple Data Model</figcaption></figure><p>The <code>User</code>:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/7a0df745fde6fe8b2b367faaadbd26a5">https://gist.github.com/pmatsinopoulos/7a0df745fde6fe8b2b367faaadbd26a5</a></p><p>The <code>UserBalance</code>:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/20dbde10b987886c0d5edfb43b8ace4a">https://gist.github.com/pmatsinopoulos/20dbde10b987886c0d5edfb43b8ace4a</a></p><p>I keep track of a user balance.</p><h3 id="h-background-task" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Background Task</h3><p>I have a <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sidekiq/sidekiq">Sidekiq</a> job in my Ruby on Rails project.</p><p>You can imagine any background task in your project.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/e10f9bc615ef733ee3a0400a97e4a696">https://gist.github.com/pmatsinopoulos/e10f9bc615ef733ee3a0400a97e4a696</a></p><h2 id="h-problem" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Problem:</h2><p>This job is <strong>not thread-and-process safe</strong>.</p><p>We can prove it by running this code in a rails console. Assuming that we have created a User who has balance <code>0</code>, running the following code should increase it to 5.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/5d2f69cdf14f19f9931dc0fd070ea0e6">https://gist.github.com/pmatsinopoulos/5d2f69cdf14f19f9931dc0fd070ea0e6</a></p><p>This is an instance of running this script:</p><pre data-type="codeBlock" text="be rails runner script/threads_increase_balance.rb 
12780 -&gt; Calculating balance for user: panagiotis@matsinopoulos.gr
12860 -&gt; Calculating balance for user: panagiotis@matsinopoulos.gr
12840 -&gt; Calculating balance for user: panagiotis@matsinopoulos.gr
12800 -&gt; Calculating balance for user: panagiotis@matsinopoulos.gr
12820 -&gt; Calculating balance for user: panagiotis@matsinopoulos.gr
user_balance: 1
"><code>be rails runner script<span class="hljs-operator">/</span>threads_increase_balance.rb 
<span class="hljs-number">12780</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> Calculating balance <span class="hljs-keyword">for</span> user: panagiotis@matsinopoulos.gr
<span class="hljs-number">12860</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> Calculating balance <span class="hljs-keyword">for</span> user: panagiotis@matsinopoulos.gr
<span class="hljs-number">12840</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> Calculating balance <span class="hljs-keyword">for</span> user: panagiotis@matsinopoulos.gr
<span class="hljs-number">12800</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> Calculating balance <span class="hljs-keyword">for</span> user: panagiotis@matsinopoulos.gr
<span class="hljs-number">12820</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> Calculating balance <span class="hljs-keyword">for</span> user: panagiotis@matsinopoulos.gr
user_balance: <span class="hljs-number">1</span>
</code></pre><p>This is because all threads are accessing the balance concurrently. They are not serialized one behind the other.</p><h2 id="h-solutions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solutions?</h2><p>There are, but not all of them are good.</p><h3 id="h-solution-1-mutex-i-dont-like-it" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Solution 1 - Mutex (I don’t like it)</h3><p>I can make the job use a Mutex to access the shared balance resource.</p><p>The <code>#perform()</code> needs to take an extra argument, the <code>mutex</code>. And the script to fire the 5 jobs should be something like this:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/a19c20b63c996f7a66652e947d4d181a">https://gist.github.com/pmatsinopoulos/a19c20b63c996f7a66652e947d4d181a</a></p><p><strong>Bad!</strong></p><p>This is bad. I don’t like it. I would have to do the same for all my background jobs. Also, the most important thing is that <strong>it is not process-safe.</strong> I can’t share the same mutex across different processes. And the <code>#perform</code> taking as input the <code>mutex</code> is not an option. It’s not something that can be serialized to be stored into a queue, like Redis.</p><h3 id="h-solution-2-use-increment-i-dont-like-it" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Solution 2 - Use <code>#increment!</code> (I don’t like it)</h3><p>I can use the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/Persistence.html#method-i-increment-21">ActiveRecord#increment!</a> method, which is atomic. That’s not bad initially.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/0eafae7b6c6eefbbbc48235002fdf81e">https://gist.github.com/pmatsinopoulos/0eafae7b6c6eefbbbc48235002fdf81e</a></p><p>It works as expected with the script that spawns threads to update the balance in parallel.</p><p>And it works well if we put these jobs in the background. I use this script to do it: (I make sure that I have <code>sidekiq</code> running)</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/e40e2265cbfa65f4b20c577310c604a6">https://gist.github.com/pmatsinopoulos/e40e2265cbfa65f4b20c577310c604a6</a></p><p>However, I still don’t like that.</p><p>First reason? <code>#increment!()</code> <strong>does not call the</strong> <code>ActiveRecord</code> <strong>callbacks</strong>.</p><p>Second and most important reason? <strong>Doesn’t work with job business logic which is a little bit more elaborate than just an increment.</strong></p><p>I generalize my job to do some complex logic to calculate the balance. Imagine that it reads other models, possibly updates others, and finally comes up with a new balance to set the balance with a new value, and <strong>it is not just an increment</strong>. It is a value which does not depend on the previous / existing balance.</p><p>So, I am on a new problem now.</p><h2 id="h-more-complex-problem" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">More Complex Problem</h2><p>This is another, more complex problem, job:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/4b42cba1efbbbf7e3b9b57471396cbbf">https://gist.github.com/pmatsinopoulos/4b42cba1efbbbf7e3b9b57471396cbbf</a></p><p>Here, the job sets the <code>balance</code> to a business-dependent value.</p><p>And because there are 5 jobs running in parallel, we don’t know which one has set the <em>correct</em> value, whatever <em>correct</em> means in the business that we run.</p><p>BTW, the script prints a log of the jobs and what balance each job calculated.</p><p>The business logic inside the <code>CalculateUserBalanceJob</code> might be long and complex, as I said, and I can’t set two (or more) instances of this job run at the same time.</p><p><strong>I should NOT allow two (or more) instances of this job to run at the same time.</strong></p><p>How can I solve this problem now?</p><h2 id="h-solutions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solutions</h2><h3 id="h-solution-optimistic-locking-is-not-enough-i-dont-like-it-as-the-single-solution" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Solution - Optimistic Locking (Is not Enough - I don’t like It as The Single Solution)</h3><p>I am going to assume that this job is going to run as a singleton, i.e at most one instance of it at any point in time. I am optimism. But, if for any reason, this is violated, then I need a way to know and make sure that I don’t suffer any serious implication.</p><p>The tool I use is called optimistic locking.</p><p>I will add the column <code>:lock_version</code> to the <code>UserBalance</code> model. This will make sure that only the first process that loads a <code>UserBalance</code> instance will succeed in updating that instance. All others will fail with <code>ActiveRecord::StaleObjectError</code> exception.</p><p>Without doing any modification to my Job code itself, if I try the script that fires 5 background jobs, 4 of them will fail.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4cad9419473ec560a903fde10c0a11e019c4837df64b09c06290bbfeae54c1e8.png" alt="4 Jobs will fail with ActiveRecord::StaleObjectError" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">4 Jobs will fail with ActiveRecord::StaleObjectError</figcaption></figure><p>And <code>Sidekiq</code> will keep on retrying them, until they succeed. And finally they will succeed since on every retry, one will be succeeding and the rest will be failing until all succeed.</p><p>So, this looks good. But</p><ul><li><p>I don’t like the fact that my jobs fail. <code>Sidekiq</code> will retry them unto to 25 times, but with exponential wait time in between retries.</p></li><li><p>Lot’s of exceptions and noise.</p></li></ul><p>Is there anything better I can do?</p><h3 id="h-solution-optimistic-locking-retry-in-the-job-i-dont-like-it-either" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Solution - Optimistic Locking - Retry in the Job (I don’t like it either)</h3><p>I can rescue the <code>ActiveRecord::StaleObjectError</code> and reload the object and retry again, maybe after waiting for some time.</p><p>So, I could do the job that <code>Sidekiq</code> does for me with <em>Retries</em>.</p><p>Nah! Very dirty code and raises questions like:</p><ul><li><p>How many retries?</p></li><li><p>How long to wait between retries?</p></li></ul><p>There has to be something better.</p><h3 id="h-solution-pessimistic-locking-lock-on-user-keep-optimistic-locking-good-when-updating-the-record-at-hand" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Solution - Pessimistic Locking (Lock on User), Keep Optimistic Locking (Good When Updating The Record At Hand)</h3><p>I will keep the optimistic locking with the <code>lock_version</code>. I really like it in general. It keeps me from doing updates on objects that might be accessed by more than one process. In a multi-user Web application for example, the majority of objects would benefit from optimistic locking.</p><p>Hence, I am leaving optimistic locking in, but I also introduce <strong>pessimistic locking.</strong> This will lock all processes out except one. The other processes will wait behind a database enforced lock mechanism.</p><p>Ruby on rails allows me to do record-level locking and will work well for my case.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/e2fee3d4ce74be9614f152196668ec9e">https://gist.github.com/pmatsinopoulos/e2fee3d4ce74be9614f152196668ec9e</a></p><p>That’s almost perfect. Isn’t it?</p><p>When we run the script, in the <code>Sidekiq</code> console, the <code>”****** &lt;job-id&gt; Calculating balance for user:…”</code> does not appear for all 5 jobs at once. Only after a job finishes, then next message appears. And the jobs do not fail with the <code>ActiveRecord::StaleObjectError</code>. So, the jobs have been serialized behind the <code>User.lock.find(user_id)</code> statement, which does a <code>select * from users where user_id = ? for update</code>.</p><p><strong>Good!</strong> That is a very good solution for locking processes out when <strong>updating</strong> a record instance, like a <code>User</code> with a specific id.</p><p><strong>Not Good For:</strong> But what happens if I don’t want to update a record, but I want to create a record?</p><h2 id="h-problem-create-record" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Problem: Create Record</h2><p>Let’s see another problem. I have a job that creates a <code>User</code>. But I want to make sure that it runs, again, as singleton, unique job at a given point in time.</p><p>This is an example:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/a11a2ab5887629a1a600be54c1e75c36">https://gist.github.com/pmatsinopoulos/a11a2ab5887629a1a600be54c1e75c36</a></p><p>If I just span 5 such jobs in <code>Sidekiq</code>, simulating a bad, but possible to happen, situation, 4 of them will fail, because the email is unique. They will retry and will retry until they find that another process has created the user.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3cb09290035106d2a21566644ca297f9e1dedf7a13e4ccde03eb0bbe57a17626.png" alt="Failing Jobs Because Only One Can Create the User With Given Email" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Failing Jobs Because Only One Can Create the User With Given Email</figcaption></figure><p><strong>Note:</strong> One might say, “Hey you! Why don’t you use <code>User.find_or_create_by!(email: user_email)</code>”. My answer to this comment is that I would do it if it were that simple. If I didn’t have any complex business logic in creating a User. The point is to find a solution to a more complex problem. A job/task that does a lot of things before committing/creating one or more things into the database. So, the example of creating a User by email might not be representative, but this is why I am not using <code>find_or_create_by()</code>. To simulate a more complex scenario.</p><p>How do I lock a process out when another one is being executed in this <em>creation</em> case? There is no way I can do <code>select for update</code>, because there are not any records to lock. I am not doing any update.</p><h2 id="h-solution-pessimistic-locking-using-advisory-locks-i-like-this-very-much" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solution - Pessimistic Locking Using Advisory Locks (I like this very much)</h2><p>I am going to use <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.postgresql.org/docs/17/explicit-locking.html#ADVISORY-LOCKS">Postgres Advisory Locks</a>.</p><hr><p><strong>Side note to those who use MySQL or MS SQL Server:</strong> I asked an LLM to tell me whether MySQL and MS SQL Server have equivalent to Advisory Locks tools. Here is what it replied to me:</p><blockquote><ul><li><p><strong>MySQL</strong>: MySQL doesn&apos;t have a direct equivalent to Advisory Locks, but you can achieve similar functionality using <strong>GET_LOCK()</strong> and <strong>RELEASE_LOCK()</strong> functions. These allow you to create named locks that can be used across sessions, much like Advisory Locks in PostgreSQL.</p></li><li><p><strong>SQL Server</strong>: SQL Server doesn&apos;t have an exact match either, but you can use <strong>sp_getapplock</strong> and <strong>sp_releaseapplock</strong> to create application-level locks. These functions allow you to define locks that aren&apos;t tied to specific database objects, making them somewhat comparable to Advisory Locks.</p></li></ul></blockquote><hr><p>Here is how I can use it in the <code>CreateUserJob</code>:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/6edb671bdabdc6e4daac8c5c75246b16">https://gist.github.com/pmatsinopoulos/6edb671bdabdc6e4daac8c5c75246b16</a></p><p>The behavior of this process is exactly the same like when I used the Rails pessimistic locking with <code>User.lock.find()</code>. Each process is queued behind the other.</p><p>I repeat here the lines that do the locking:</p><pre data-type="codeBlock" text="# LOCKING &gt;
lock_namespace = ADVISORY_LOCK_NAMESPACES[:create_user_job]

lock_key = user_email

lock_comment = &quot;CreateUserJob: #{provider_job_id} - #{user_email}&quot;

lock_query = &lt;&lt;~SQL.squish
  SELECT pg_advisory_xact_lock(:lock_namespace, hashtext(:key)) /* #{lock_comment} */
SQL

lock_query_args = {
  lock_namespace: lock_namespace,
  key: lock_key
}

sql = ActiveRecord::Base.sanitize_sql_array([ lock_query, lock_query_args ])

ActiveRecord::Base.connection.execute(sql)
# &lt; LOCKING
"><code># LOCKING <span class="hljs-operator">></span>
lock_namespace <span class="hljs-operator">=</span> ADVISORY_LOCK_NAMESPACES[:create_user_job]

lock_key <span class="hljs-operator">=</span> user_email

lock_comment <span class="hljs-operator">=</span> <span class="hljs-string">"CreateUserJob: #{provider_job_id} - #{user_email}"</span>

lock_query <span class="hljs-operator">=</span> <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">&#x3C;</span><span class="hljs-operator">~</span>SQL.squish
  SELECT pg_advisory_xact_lock(:lock_namespace, hashtext(:key)) <span class="hljs-comment">/* #{lock_comment} */</span>
SQL

lock_query_args <span class="hljs-operator">=</span> {
  lock_namespace: lock_namespace,
  key: lock_key
}

sql <span class="hljs-operator">=</span> ActiveRecord::Base.sanitize_sql_array([ lock_query, lock_query_args ])

ActiveRecord::Base.connection.execute(sql)
# <span class="hljs-operator">&#x3C;</span> LOCKING
</code></pre><p>The juice of the story here is the <code>SELECT pg_advisory_xact_lock()</code>. I am not going to explain how it works. You can read the Postgres documentation.</p><p>What I am going to emphasize is what I did on top of it:</p><ol><li><p>I use the namespace to tell that I am locking out <code>CreateUserJob</code>(s) for the key <code>user_email</code>. Note that the <code>pg_advisory_xact_lock()</code> takes as arguments integers. That’s why I have to convert the namespace <code>:create_user_job</code> and the email to an integer. The first I do it with an application level hash and the second using the <code>hashtext()</code> function in Postgres.</p></li><li><p>I also annotate the SQL statement with an SQL level lock.</p></li></ol><hr><p><strong>Important Note:</strong> <code>hashtext()</code> is an internal function in Postgres. It is not publicly documented. This means that it might break in the future. Also, as a hashing function, it might fall into collisions. Now, I am looking at alternatives to generate a unique integer from a string. But, if you can provide an <strong>integer</strong> for the key, rather than a string which is converted with <code>hashtext()</code> to an integer, that would be ideal.</p><hr><p><strong>Suggestion:</strong> Factor out these lines of code into a reusable Ruby method. Like this:</p><pre data-type="codeBlock" text="module AdvisoryLock
  class &lt;&lt; self
    def lock(lock_namespace:, lock_key:, lock_comment:)
      lock_query = &lt;&lt;~SQL.squish
        SELECT pg_advisory_xact_lock(:lock_namespace, hashtext(:key)) /* #{lock_comment} */
      SQL
      lock_query_args = {
        lock_namespace: lock_namespace,
        key: lock_key
      }
      sql = ActiveRecord::Base.sanitize_sql_array([lock_query, lock_query_args])
      ActiveRecord::Base.connection.execute(sql)
    end
  end
end
"><code>module AdvisoryLock
  class <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">&#x3C;</span> <span class="hljs-built_in">self</span>
    def lock(lock_namespace:, lock_key:, lock_comment:)
      lock_query <span class="hljs-operator">=</span> <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">&#x3C;</span><span class="hljs-operator">~</span>SQL.squish
        SELECT pg_advisory_xact_lock(:lock_namespace, hashtext(:key)) <span class="hljs-comment">/* #{lock_comment} */</span>
      SQL
      lock_query_args <span class="hljs-operator">=</span> {
        lock_namespace: lock_namespace,
        key: lock_key
      }
      sql <span class="hljs-operator">=</span> ActiveRecord::Base.sanitize_sql_array([lock_query, lock_query_args])
      ActiveRecord::Base.connection.execute(sql)
    end
  end
end
</code></pre><p>Then it will only be a matter of calling this method: <code>AdvisoryLock.lock(…)</code> at the beginning of your transaction.</p><hr><p><strong>Disadvantages?:</strong> It doesn’t have any. As long as your server is configured to accept many advisory locks, then you will not hit any problem. Except this solution is not very good if the job is very-long-running with numerous updates in db. See next problem.</p><p><strong>Good!</strong> I even use it some times instead of the Ruby on Rails pessimistic locking. This is because Ruby on Rails pessimistic locking requires you to lock some rows in the database and prevents others from updating them, which might not be necessary.</p><hr><p><strong>Important Important Important!</strong> Exactly as it is mentioned on the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sidekiq/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional">Best Practices section in Sidekiq documentation</a>,</p><blockquote><p>Make your jobs idempotent and transactional</p></blockquote><p>This is exactly what I did with the <code>CreateUserJob</code>.</p><p>It is idempotent, because I am checking whether a user already exists or not. If it exists, I don’t do anything. And it runs inside a transaction.</p><p>Hey! This solution is not good for the next problem. What if my job is a long-running job? Assuming that it needs to run for 20 mins or for 40 mins or for 2 hours? What if it is a batch job?</p><h2 id="h-problem-long-running-batch-jobs" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Problem: Long Running Batch Jobs</h2><p><strong>Important!</strong> I don’t like long running batch jobs. If I have a long-running batch job, I try to break into smaller chunks, chunks that can finish in about 1 or 2 minutes maximum.</p><p>But, let’s suppose that we have a job that is long-running and cannot be broken into smaller jobs.</p><p>Keeping the advisory lock and the transaction open for 1 hour or 2 hours is not good for many reasons.</p><h2 id="h-solution-use-table-to-log-a-job-is-enqueued" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solution - Use Table To Log A Job Is Enqueued</h2><p>Before I enqueue a job with something like <code>LongRunningBatchJob.perform_later(batch_id)</code>, I check whether the job has already been enqueued.</p><p>So, I want something like this:</p><pre data-type="codeBlock" text="unless job_enqueued?(LongRunningBatchJob.name, batch_id)
  LongRunningBatchJob.perform_later(batch_id)
end
"><code>unless job_enqueued?(LongRunningBatchJob.<span class="hljs-built_in">name</span>, batch_id)
  LongRunningBatchJob.perform_later(batch_id)
end
</code></pre><p>But this ☝️, <strong>needs to be atomic</strong>. I.e. in between <code>job_enqueued?()</code> check and the <code>…perform_later()</code> the job should not be enqueued.</p><p>Hence, we need to find a way to <strong>lock the access to the queue</strong> when checking and then enqueuing.</p><p>Can I use <code>Sidekiq</code> API to do that. No, I can’t. <code>Sidekiq</code> uses Redis to store background tasks. And I don’t see <code>Sidekiq</code> API having any such facility as <code>LongRunningJob.perform_later_if_not_already_enqueued()</code>.</p><p>And it’s not only that we want to check whether it is already enqueued. It might be enqueued and have already started. In that case it is not literally enqueued in <code>Sidekiq</code> terms.</p><p>So, generally, I believe that <code>Sidekiq</code> is not very helpful here.</p><h3 id="h-enqueued-jobs-in-a-table" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Enqueued Jobs In a Table</h3><p>Hence, I am going to use a Postgres table to implement this feature.</p><p><code>enqueued_jobs</code> table:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6a137cbdc9b6f8f51dba5b2c3114f8e24681e7ca867c7c639448db74a1b202b0.png" alt="Enqueued Jobs" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Enqueued Jobs</figcaption></figure><p><code>EnqueuedJob</code> model:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/4d566b971a60a4cd67a1a432f3dc6afe">https://gist.github.com/pmatsinopoulos/4d566b971a60a4cd67a1a432f3dc6afe</a></p><p>This is a model that has a polymorphic association to units of work, other models that are called <code>queuables</code>. For example to a <code>User</code> , or to a <code>Batch</code>.</p><p>Each enqueued job is for</p><ul><li><p>a <code>queueable</code>, such as a <code>User</code>, a <code>Batch</code>, an <code>Order</code>. What we usually give as argument to the job, <code>#perform</code>.</p></li><li><p>a <code>job_class_name</code>, such as <code>CreateUserJob</code>, <code>LongRunningBatchJob</code>, e.t.c.</p></li></ul><h3 id="h-enqueuing-a-job-dont-do-like-this" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Enqueuing a Job - Don’t Do Like This</h3><p>This should not be done with just <code>LongRunningBatchJob.perform_later(batch_id)</code>. We should not call the <code>#perform_later()</code>on the Job class. If we do, we don’t take advantage of this <code>enqueued_jobs</code> table.</p><p>We should call the <code>enqueue()</code> method on the model at hand, on the <code>queueable</code>.</p><p>For example: <code>batch.enqueue(…)</code> or <code>user.enqueue(…)</code>.</p><p>In order to support this interface, we introduce the <code>Queuable</code> module <code>concern</code>.</p><h3 id="h-enqueuing-a-job-do-like-this-queuable-api" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Enqueuing a Job - Do Like This - <code>Queuable</code> API.</h3><p>I have developed the <code>module Queueable</code>:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/b84115e5966d3b64c7be28f0a666e024">https://gist.github.com/pmatsinopoulos/b84115e5966d3b64c7be28f0a666e024</a></p><p>The main method is the <code>enqueue</code>. If I include this module into the <code>User</code> or <code>Batch</code> or other model, then I can call it like this:</p><pre data-type="codeBlock" text="user = User.find_by(email: “panagiotis@matsinopoulos.gr”)
user.enqueue(job_class_name: LongRunningBatchJob.name)
"><code><span class="hljs-selector-tag">user</span> = <span class="hljs-selector-tag">User</span><span class="hljs-selector-class">.find_by</span>(<span class="hljs-attribute">email</span>: “panagiotis<span class="hljs-variable">@matsinopoulos</span>.gr”)
<span class="hljs-selector-tag">user</span><span class="hljs-selector-class">.enqueue</span>(<span class="hljs-attribute">job_class_name</span>: LongRunningBatchJob.name)
</code></pre><p>This will use the Postgres advisory lock to create an entry in the <code>enqueued_jobs</code> only if there is none. If it creates the entry it also do a <code>perform_later()</code>. Otherwise, it doesn’t.</p><hr><p>The <code>Queueable</code> API supports enqueuing jobs in the future with the <code>scheduling_options</code> too.</p><hr><p>I will not go into the details of the implementation, but they should be fairly clear if you have read the whole blog post from the beginning.</p><h3 id="h-what-about-the-job-itself" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">What About The Job Itself?</h3><p>But how do I implement a job to work together with the <code>enqueued_jobs</code>?</p><p>Here it is how:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/9908d5d43ba95994d72d8cb918765929">https://gist.github.com/pmatsinopoulos/9908d5d43ba95994d72d8cb918765929</a></p><p>First, I include a concern called <code>EnqueueableJob</code></p><p>Then, I wrap the actual work inside a call such as:</p><pre data-type="codeBlock" text="process_enqueued_job(queueable: batch) do
  ...
end
"><code><span class="hljs-built_in">process_enqueued_job</span>(queueable: batch) <span class="hljs-keyword">do</span>
  ...
end
</code></pre><p>As simple as that. The interaction wit the <code>enqueued_jobs</code> happens inside the <code>process_enqueued_job</code> which is implemented in the <code>EnqueueableJob</code> concern as follows:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gist.github.com/pmatsinopoulos/3edd981a26b85a58fdd3e4f406cf933e">https://gist.github.com/pmatsinopoulos/3edd981a26b85a58fdd3e4f406cf933e</a></p><p>This is very simple.</p><p>I run the job using <code>yield</code> and then I delete it from the <code>enqueued_jobs</code>.</p><p>In summary, for the long running jobs:</p><ol><li><p>I synchronize the access to the <code>enqueued_jobs</code> and the subsequent call to <code>#perform_later()</code>.</p></li><li><p>The job itself, runs only if it still exists in the <code>enqueued_jobs</code> and removes itself after finishing.</p></li></ol><h2 id="h-problem-jobs-need-to-restart-from-where-they-were-stopped" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Problem: Jobs Need To Restart From Where They Were Stopped</h2><p>This is a very interesting problem. When I have a long running job that usually takes 1 or 2 hours, for example, to run, <code>Sidekiq</code> might kill it as part of a restart. For example, every time I do a deployment.</p><p>My job needs to be able to continue from the point it was killed. Not do the whole job from the beginning. Otherwise, in the case of frequent deployments, more frequent than the duration of a single-run of the job, this job will never finish.</p><h2 id="h-solution-rescue-sidekiqshutdown-save-state-raise" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solution: Rescue <code>Sidekiq::Shutdown</code>, Save State, Raise</h2><p>Inside the job, one can rescue the <code>Sidekiq::Shutdown</code> exception which signals the killing of a job by <code>Sidekiq</code> .</p><p>Then, inside the rescue block, job will save its state so that the next time it starts it will continue from the point it was stopped.</p><p>Then it will <code>raise</code> the exception because this is the proper thing to do as a <code>Sidekiq</code> job.</p><h2 id="h-solutions-other" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solutions: Other</h2><p>There are some gems that are trying to solve the above problems, but I have to be honest, I have not tried them out. Maybe in another post I will do:</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sidekiq/sidekiq/wiki/Ent-Unique-Jobs">Unique Jobs by Sidekiq Enterprise</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/mhenrixon/sidekiq-unique-jobs">Sidekiq Unique Jobs</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/fatkodima/sidekiq-iteration">Sidekiq Iteration</a></p></li><li><br></li></ul><hr><p>gm! I am Panos Matsinopoulos, humble software reader, writer, classical music and V8 engine lover. I work for <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com">Talent Protocol</a>, where we make builders get the recognition they deserve.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c78948dc7cfd234994c0d3212edc401c4c61aa1acba802df06fa1571bc7c5ca1.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[SQL Query Annotation]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/sql-query-annotation</link>
            <guid>l5vi9EY53YYxFn3OTb72</guid>
            <pubDate>Sat, 05 Apr 2025 14:09:23 GMT</pubDate>
            <description><![CDATA[Context:Ruby on RailsPostgresActiveRecordProblem:When I inspect the SQL queries stats in my Postgres database, I want to know where each one is coming from, in terms of application code. As precisely as possible. The pg_stat_activity , for example, has the column query which shows the SQL statement:- START_REPLICATION SLOT "..." 539/C0000000 TIMELINE 1 - COMMIT - SELECT "github_accounts".* FROM "github_accounts" WHERE "github_accounts"."name" = '...' ORDER BY "github_accounts"."account_create...]]></description>
            <content:encoded><![CDATA[<h2 id="h-context" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Context:</h2><ul><li><p>Ruby on Rails</p></li><li><p>Postgres</p></li><li><p>ActiveRecord</p></li></ul><h2 id="h-problem" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Problem:</h2><p>When I inspect the SQL queries stats in my Postgres database, I want to know where each one is coming from, in terms of application code. As precisely as possible.</p><p>The <code>pg_stat_activity</code> , for example, has the column <code>query</code> which shows the SQL statement:</p><pre data-type="codeBlock" text="- START_REPLICATION SLOT &quot;...&quot; 539/C0000000 TIMELINE 1
- COMMIT 
- SELECT &quot;github_accounts&quot;.* FROM &quot;github_accounts&quot; WHERE &quot;github_accounts&quot;.&quot;name&quot; = &apos;...&apos; ORDER BY &quot;github_accounts&quot;.&quot;account_created_at&quot; ASC LIMIT 1 
- SELECT value FROM rds_heartbeat2
- COMMIT
- COMMIT 
- COMMIT 
- COMMIT
"><code><span class="hljs-operator">-</span> START_REPLICATION SLOT "..." <span class="hljs-number">539</span><span class="hljs-operator">/</span>C0000000 TIMELINE <span class="hljs-number">1</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> 
<span class="hljs-operator">-</span> <span class="hljs-keyword">SELECT</span> "github_accounts".<span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> "github_accounts" <span class="hljs-keyword">WHERE</span> "github_accounts"."name" <span class="hljs-operator">=</span> <span class="hljs-string">'...'</span> <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> "github_accounts"."account_created_at" <span class="hljs-keyword">ASC</span> LIMIT <span class="hljs-number">1</span> 
<span class="hljs-operator">-</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">value</span> <span class="hljs-keyword">FROM</span> rds_heartbeat2
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> 
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> 
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span>
</code></pre><p>Or AWS RDS Performance Insights:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5649a7ed7b25180604e9e7d2b2bf2c882d221aba09d22df77a8b11fc06f679ad.png" alt="Possibly Long SQL Statements Identified by AWS RDS Performance Insights" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Possibly Long SQL Statements Identified by AWS RDS Performance Insights</figcaption></figure><p>But I can’t tell which point in the code is generating these queries.</p><p><strong>Knowing the exact line of code that generates these queries is very useful when I want to troubleshoot problematic queries such as slow ones.</strong></p><h2 id="h-solution" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solution:</h2><p>I enable the <code>ActiveRecord</code> <code>query_log_tags</code>. In my <code>config/application.rb</code> file I have the following configuration:</p><pre data-type="codeBlock" text="module TalentProtocol
  class Application &lt; Rails::Application
    ...
    config.active_record.query_log_tags_enabled = true
    config.active_record.query_log_tags |= [:namespaced_controller, :source_location]
    config.active_record.verbose_query_logs = true
  end
end
"><code>module TalentProtocol
  class Application <span class="hljs-operator">&#x3C;</span> Rails::Application
    ...
    config.active_record.query_log_tags_enabled <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
    config.active_record.query_log_tags <span class="hljs-operator">|</span><span class="hljs-operator">=</span> [:namespaced_controller, :source_location]
    config.active_record.verbose_query_logs <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
  end
end
</code></pre><p>Bingo!</p><p>Then <code>pg_stat_activity</code> includes SQL comments that have the precise file and line of code (from my Ruby on Rails application) that generates this particular SQL statement.</p><pre data-type="codeBlock" text="- COMMIT /*action:index,application:TalentProtocol,namespaced_controller:api/v2/passport_credentials,source_location:app/jobs/api/log_request_job.rb:21:in `perform&apos;*/
- SELECT &quot;github_accounts&quot;.* FROM &quot;github_accounts&quot; WHERE &quot;github_accounts&quot;.&quot;name&quot; = &apos;...&apos; ORDER BY &quot;github_accounts&quot;.&quot;account_created_at&quot; ASC LIMIT 1 /*application:TalentProtocol,job:BuilderGrants::CalculateUserScoreJob,source_location:app/services/builder_grants/calculation_methods/base_org_v1.
rb:202:in `calculate_github_score&apos;*/
- COMMIT /*action:show,application:TalentProtocol,namespaced_controller:api/v2/passports,source_location:app/jobs/api/log_request_job.rb:21:in `perform&apos;*/
- COMMIT /*action:index,application:TalentProtocol,namespaced_controller:api/v2/passport_credentials,source_location:app/jobs/api/log_request_job.rb:21:in `perform&apos;*/
- COMMIT /*action:refresh_token,application:TalentProtocol,namespaced_controller:api/v2/auth,source_location:app/jobs/api/log_request_job.rb:21:in `perform&apos;*/
"><code><span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> <span class="hljs-comment">/*action:index,application:TalentProtocol,namespaced_controller:api/v2/passport_credentials,source_location:app/jobs/api/log_request_job.rb:21:in `perform'*/</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">SELECT</span> "github_accounts".<span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> "github_accounts" <span class="hljs-keyword">WHERE</span> "github_accounts"."name" <span class="hljs-operator">=</span> <span class="hljs-string">'...'</span> <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> "github_accounts"."account_created_at" <span class="hljs-keyword">ASC</span> LIMIT <span class="hljs-number">1</span> <span class="hljs-comment">/*application:TalentProtocol,job:BuilderGrants::CalculateUserScoreJob,source_location:app/services/builder_grants/calculation_methods/base_org_v1.
rb:202:in `calculate_github_score'*/</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> <span class="hljs-comment">/*action:show,application:TalentProtocol,namespaced_controller:api/v2/passports,source_location:app/jobs/api/log_request_job.rb:21:in `perform'*/</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> <span class="hljs-comment">/*action:index,application:TalentProtocol,namespaced_controller:api/v2/passport_credentials,source_location:app/jobs/api/log_request_job.rb:21:in `perform'*/</span>
<span class="hljs-operator">-</span> <span class="hljs-keyword">COMMIT</span> <span class="hljs-comment">/*action:refresh_token,application:TalentProtocol,namespaced_controller:api/v2/auth,source_location:app/jobs/api/log_request_job.rb:21:in `perform'*/</span>
</code></pre><p>And in AWS RDS Performance Insights, I can see these comments too:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/737614d9b363685655ea69787218290a2dbafe9d16465ab066d67a63742e8cf7.png" alt="AWS RDS Performance Insights Shows Me the SQL Query Application Source Code Location" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">AWS RDS Performance Insights Shows Me the SQL Query Application Source Code Location</figcaption></figure><p>Extremely Useful!</p><hr><p>gm!</p><p>I am Panos M., humble, back-end software reader and writer at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com/">Talent Protocol</a>.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/9cfbb43980a17bbad2ace8a682d83d2506e5addebe0363dcefbbdd158bafaec4.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Find Long Ruby on Rails Requests]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/find-long-ruby-on-rails-requests</link>
            <guid>IzVNMLwB4G2hnRYXYEBB</guid>
            <pubDate>Thu, 20 Mar 2025 22:41:51 GMT</pubDate>
            <description><![CDATA[Image by G Poulsen from PixabayIntroductionAt Talent Protocol we use Ruby on Rails for our back-end server.Problem DefinitionHow do we know which HTTP requests take too long to be processed? With too long we mean more than 2 seconds.SolutionAWS CloudWatch LogsWe collect all Rails server logs into AWS CloudWatch. Hence, when we inspect the logs we can see lines like the following:Rails Log LinesCompleted OK - More than 2 SecondsSo, it is quite easy to select the lines individually. For example...]]></description>
            <content:encoded><![CDATA[<blockquote><p>Image by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com/users/gpoulsen-6673015/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=6964161">G Poulsen</a> from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=6964161">Pixabay</a></p></blockquote><h2 id="h-introduction" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Introduction</h2><p>At <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.talentprotocol.com">Talent Protocol</a> we use Ruby on Rails for our back-end server.</p><h3 id="h-problem-definition" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Problem Definition</h3><p>How do we know which HTTP requests take too long to be processed? With <em>too long</em> we mean more than 2 seconds.</p><h2 id="h-solution" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solution</h2><h3 id="h-aws-cloudwatch-logs" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">AWS CloudWatch Logs</h3><p>We collect all Rails server logs into AWS CloudWatch. Hence, when we inspect the logs we can see lines like the following:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/475f5285a774b111cded508b59caa8056cf723c291f7b23b2b27867251cb5dbe.png" alt="Rails Log Lines" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Rails Log Lines</figcaption></figure><h3 id="h-completed-ok-more-than-2-seconds" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Completed OK - More than 2 Seconds</h3><p>So, it is quite easy to select the lines individually. For example, the following <em>Logs Insights QL</em> statement gives me the successful HTTP requests that took more than 2 seconds:</p><pre data-type="codeBlock" text="filter @message like /Completed 200 OK in \d+ms/
| parse @message /Completed 200 OK\sin\s(?&lt;duration&gt;\d+)ms/
| filter duration &gt;= 2000
| display duration, @message
| sort duration desc
| limit 10000
"><code>filter @message like <span class="hljs-operator">/</span>Completed <span class="hljs-number">200</span> OK in \d<span class="hljs-operator">+</span>ms<span class="hljs-operator">/</span>
<span class="hljs-operator">|</span> parse @message <span class="hljs-operator">/</span>Completed <span class="hljs-number">200</span> OK\sin\s(?<span class="hljs-operator">&#x3C;</span>duration<span class="hljs-operator">></span>\d<span class="hljs-operator">+</span>)ms<span class="hljs-operator">/</span>
<span class="hljs-operator">|</span> filter duration <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">2000</span>
<span class="hljs-operator">|</span> display duration, @message
<span class="hljs-operator">|</span> sort duration desc
<span class="hljs-operator">|</span> limit <span class="hljs-number">10000</span>
</code></pre><p>Note: this also sorts these lines in descending order of the <code>duration</code>.</p><p>An example result set is this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3a33b3f4a99f190d470624732ac691d118f03873dc88f51bca414a7a7a05c3fc.png" alt="Example Long Requests" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Example Long Requests</figcaption></figure><p>The problem is that I don’t know which requests, in terms of <em>path</em> and <em>query</em> parameters are these. This is because this information is coming on another line, the one that contains the pattern <code>Started…</code>, i.e. the <em>1st</em> line that is logged for a particular request.</p><h3 id="h-working-with-opensearch-sql" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Working with OpenSearch SQL</h3><p>I understand that I need a way to combine, join, two lines by a common piece of information.</p><p>I know that this sounds like <em>SQL joins</em> and the common piece of information is the <em>request id</em>.</p><p>This is where I switched to the CloudWatch Logs Insights OpenSearch SQL tool.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a5508c4e17bbdef82b2a49e0ca865bd0c2a461ae310191d62538a9ab603d2636.png" alt="OpenSearch SQL tool" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">OpenSearch SQL tool</figcaption></figure><p>Let me work with this tool, step-by-step.</p><h3 id="h-get-the-completed-lines-with-duration-greater-2-seconds" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Get the <code>Completed</code> lines with duration &gt;= 2 seconds</h3><p>I will write this statement first:</p><pre data-type="codeBlock" text="SELECT 
  c.`@message` as message,
  REGEXP_EXTRACT(c.`@message`, &apos;INFO -- : \\[([0-9a-f-]+)\\] .+ Completed 200 OK&apos;, 1) as request_id,
  CAST(REGEXP_EXTRACT(c.`@message`, &apos;in ([0-9]+)ms&apos;, 1) as INT) as duration_in_ms
FROM `play-production-play-server` as c
WHERE c.`@message` like &apos;%Completed 200 OK%&apos; AND CAST(REGEXP_EXTRACT(c.`@message`, &apos;in ([0-9]+)ms&apos;, 1) as INT) &gt;= 2000
"><code>SELECT 
  c.`@message` <span class="hljs-keyword">as</span> message,
  REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'INFO -- : \\[([0-9a-f-]+)\\] .+ Completed 200 OK'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> request_id,
  CAST(REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'in ([0-9]+)ms'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> INT) <span class="hljs-keyword">as</span> duration_in_ms
FROM `play<span class="hljs-operator">-</span>production<span class="hljs-operator">-</span>play<span class="hljs-operator">-</span>server` <span class="hljs-keyword">as</span> c
WHERE c.`@message` like <span class="hljs-string">'%Completed 200 OK%'</span> AND CAST(REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'in ([0-9]+)ms'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> INT) <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">2000</span>
</code></pre><p>As you can see, this is an <code>SQL</code>-like language that I am using. <code>SELECT … FROM…WHERE…</code>.</p><p>Look how I use the <code>REGEXP_EXTRACT</code> to get the <code>request_id</code> from the <code>@message</code>. I use the same function to get the <code>duration</code> and cast it to integer so that I can compare it to 2000 (which is 2 seconds in milliseconds).</p><p>The CloudWatch group that I am querying is the <code>play-production-play-server</code>. This is the name of the CloudWatch group.</p><p>If I run this, I am getting results like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/eab97ce5246d7d99a68524309482cb2b72535483995f9f03adc665cd2389a8ec.png" alt="Getting the request_id and duration_in_ms from Completed lines" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Getting the request_id and duration_in_ms from Completed lines</figcaption></figure><h3 id="h-get-the-started-lines" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Get the <code>Started</code> lines</h3><p>I will use another statement to get the lines that have the pattern <code>Started</code>. Here it is:</p><pre data-type="codeBlock" text="SELECT 
    s.`@message` as message,
    REGEXP_EXTRACT(s.`@message`, &apos;Started GET .+\\?(.+)&quot; for&apos;, 1) as query,
    REGEXP_EXTRACT(s.`@message`, &apos;Started GET &quot;([a-zA-Z0-9-/]+).+ for&apos;, 1) as path,
    REGEXP_EXTRACT(s.`@message`, &apos;INFO -- : \\[([0-9a-f-]+)\\] .+ Started GET&apos;, 1) as request_id,
    REGEXP_EXTRACT(s.`@message`, &apos;Started GET .+ for ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) &apos;, 1) as ip
FROM `play-production-play-server` as s
WHERE s.`@message` LIKE &apos;%Started GET%&apos;
"><code>SELECT 
    s.`@message` <span class="hljs-keyword">as</span> message,
    REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET .+\\?(.+)" for'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> query,
    REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET "([a-zA-Z0-9-/]+).+ for'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> path,
    REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'INFO -- : \\[([0-9a-f-]+)\\] .+ Started GET'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> request_id,
    REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET .+ for ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) '</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> ip
FROM `play<span class="hljs-operator">-</span>production<span class="hljs-operator">-</span>play<span class="hljs-operator">-</span>server` <span class="hljs-keyword">as</span> s
WHERE s.`@message` LIKE <span class="hljs-string">'%Started GET%'</span>
</code></pre><p>Again, my tool is the <code>REGEXP_EXTRACT</code>. I can use it to extract the <code>query</code>, the <code>path</code>, the <code>request_id</code> and the <code>ip</code> of the request. Pretty useful information.</p><p>If I run this statement, I get results like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/98e7ccd569b602c3628304773c32af946ec641590fd6ae79155e07c6a863875b.png" alt="Extract Information from Started Lines" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Extract Information from Started Lines</figcaption></figure><h3 id="h-join-two-statements" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Join Two Statements</h3><p>Now that I have the two statements, I can join them on the <code>request_id</code>. This will give me the duration of each request, along side other useful information like its <code>path</code>, its <code>query</code> and its <code>ip</code>.</p><p>Here is the statement:</p><pre data-type="codeBlock" text="SELECT 
    ss.path as path,
    URL_DECODE(ss.query) as query,
    ss.ip as client_ip,
    ss.request_id as request_id,
    cc.duration_in_ms as duration_in_ms
FROM (
    SELECT 
      s.`@message` as message,
      REGEXP_EXTRACT(s.`@message`, &apos;Started GET .+\\?(.+)&quot; for&apos;, 1) as query,
      REGEXP_EXTRACT(s.`@message`, &apos;Started GET &quot;([a-zA-Z0-9-/]+).+ for&apos;, 1) as path,
      REGEXP_EXTRACT(s.`@message`, &apos;INFO -- : \\[([0-9a-f-]+)\\] .+ Started GET&apos;, 1) as request_id,
      REGEXP_EXTRACT(s.`@message`, &apos;Started GET .+ for ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) &apos;, 1) as ip
    FROM `play-production-play-server` as s
    WHERE s.`@message` like &apos;%Started GET%&apos;
) as ss
    inner join (
        SELECT c.`@message` as message,
           REGEXP_EXTRACT(c.`@message`, &apos;INFO -- : \\[([0-9a-f-]+)\\] .+ Completed 200 OK&apos;, 1) as request_id,
           CAST(REGEXP_EXTRACT(c.`@message`, &apos;in ([0-9]+)ms&apos;, 1) as INT) as duration_in_ms
        FROM `play-production-play-server` as c
        WHERE c.`@message` like &apos;%Completed 200 OK%&apos; and CAST(REGEXP_EXTRACT(c.`@message`, &apos;in ([0-9]+)ms&apos;, 1) as INT) &gt;= 2000
    ) as cc on cc.request_id = ss.request_id
ORDER BY duration_in_ms desc
LIMIT 100
"><code>SELECT 
    ss.path <span class="hljs-keyword">as</span> path,
    URL_DECODE(ss.query) <span class="hljs-keyword">as</span> query,
    ss.ip <span class="hljs-keyword">as</span> client_ip,
    ss.request_id <span class="hljs-keyword">as</span> request_id,
    cc.duration_in_ms <span class="hljs-keyword">as</span> duration_in_ms
FROM (
    SELECT 
      s.`@message` <span class="hljs-keyword">as</span> message,
      REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET .+\\?(.+)" for'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> query,
      REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET "([a-zA-Z0-9-/]+).+ for'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> path,
      REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'INFO -- : \\[([0-9a-f-]+)\\] .+ Started GET'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> request_id,
      REGEXP_EXTRACT(s.`@message`, <span class="hljs-string">'Started GET .+ for ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) '</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> ip
    FROM `play<span class="hljs-operator">-</span>production<span class="hljs-operator">-</span>play<span class="hljs-operator">-</span>server` <span class="hljs-keyword">as</span> s
    WHERE s.`@message` like <span class="hljs-string">'%Started GET%'</span>
) <span class="hljs-keyword">as</span> ss
    inner join (
        SELECT c.`@message` <span class="hljs-keyword">as</span> message,
           REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'INFO -- : \\[([0-9a-f-]+)\\] .+ Completed 200 OK'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> request_id,
           CAST(REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'in ([0-9]+)ms'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> INT) <span class="hljs-keyword">as</span> duration_in_ms
        FROM `play<span class="hljs-operator">-</span>production<span class="hljs-operator">-</span>play<span class="hljs-operator">-</span>server` <span class="hljs-keyword">as</span> c
        WHERE c.`@message` like <span class="hljs-string">'%Completed 200 OK%'</span> and CAST(REGEXP_EXTRACT(c.`@message`, <span class="hljs-string">'in ([0-9]+)ms'</span>, <span class="hljs-number">1</span>) <span class="hljs-keyword">as</span> INT) <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">2000</span>
    ) <span class="hljs-keyword">as</span> cc on cc.request_id <span class="hljs-operator">=</span> ss.request_id
ORDER BY duration_in_ms desc
LIMIT <span class="hljs-number">100</span>
</code></pre><p>Two SQL sub-queries, inner-joined on the <code>request_id</code>.</p><p>Look how I also use the function <code>URL_DECODE()</code> to decode the query in order to make it easier to read.</p><p>I now get results like these:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fd85fc39809680d58f5e540e8547bb1834542b8d9f39243a5b9b3813c764d1dc.png" alt="Combined Results" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Combined Results</figcaption></figure><p>Bingo! I now know which <code>path</code>, <code>query</code> and <code>client_ip</code> had these long running HTTP requests.</p><hr><p>gm!</p><p>I am Panos M., humble, back-end software reader and writer at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com/">Talent Protocol</a>.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f8057cf0230c8b7d21582258159e6ae5af4a5f5ed4e8d63182b9641da62130c5.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[[book review] The Developer Facing Startup, by Adam Frankl]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/book-review-the-developer-facing-startup-by-adam-frankl</link>
            <guid>25KHK58h7Tzbyb2h5Vy8</guid>
            <pubDate>Mon, 10 Mar 2025 13:01:39 GMT</pubDate>
            <description><![CDATA[TL;DR;Yes, I liked it very much! Yes, I recommend it to all founders of startups, especially those who target developers. It will help you build numerous TODO lists. They constitute a good action plan for your startup to succeed. So, it is very practical. But I found it useful to other types of startups too. Moreover, when I read such books I pick up lessons to apply to both my personal and professional life, because I like to read and abstract. And I like to generalize and find applications ...]]></description>
            <content:encoded><![CDATA[<h2 id="h-tldr" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><strong>TL;DR;</strong></h2><p>Yes, I liked it very much!</p><p>Yes, I recommend it to all founders of startups, especially those who target developers.</p><p>It will help you build numerous TODO lists. They constitute a good action plan for your startup to succeed.</p><p>So, it is <strong>very practical.</strong></p><p>But I found it useful to other types of startups too.</p><p>Moreover, when I read such books I pick up lessons to apply to both my personal and professional life, because I like to read and abstract. And I like to generalize and find applications of what I read to other aspects of life too.</p><p>And this book had a lot of lessons for me to learn.</p><p>Also, it had a different perspective on things, perspective that I didn’t expect one could have.</p><p>No Spoiler: I will not reveal the <em>secrets</em> of the book. It’s not fair to the book author.</p><p>Affiliate link: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://amzn.to/4bFI0UU">This is where you can buy the book from</a>.</p><h2 id="h-some-thinks-you-will-find-in-the-book" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Some Thinks You Will Find In the Book</h2><p>About what <em>Entrepreneurship and Startup</em> is.</p><p>How developers are different. This means they have to be approached and treated differently, in a special way. The author is explaining in low-level details and with an action plan on how to do it. It tells you which is the wrong and which is the correct approach.</p><p>Be the trusted authority, the subject-matter expert. And show that you are.</p><p>What is a TAB, Technical Advisory Board and how it can help you focus the right problem searching for the right solution. The book will teach you how to set up a TAB. From recruiting to management.</p><p>Very nice chapters on how to build a story and how a story is <em>boring without a villain</em>. Book comes with a lot of example on good and bad stories.</p><p>The importance of evidence. Book is suggesting what kind of evidence you should build upon. What to avoid and what to pursue.</p><p>What should be your focus when you approach devs. Is it the problem, the solution, their pain?</p><p>Mistake, mistakes and mistakes. Books comes with lots of acts and actions you need to avoid. They are mistakes. But they are common.</p><p>And when the story is ready, then you have to build your presence. A lot of advises on how you should do it effectively and in way that will build trust. And it’s not only the Web site presence.</p><p>What about content creation? Should you write blog posts? Should you write books?</p><p>Take advantage of your users and customers. In a good way of course. Book will tell you how. What about the old-school social proof?</p><p>Book gives useful advises on how you should package your product. How to price it? When to price it?</p><p>Build feedback loop.</p><p>The importance of a well-designed (in terms of information and easy of read) Web Site. It deserves its own chapter. Book has it.</p><p>Developers need to adopt your product. But this deserves a pipeline.</p><p>Do you need a Marketing Team? Book will tell you how to set this up and when. Who to hire and in what order.</p><p>Special advises come from the book for those startups what are looking for funding.</p><hr><p>gm!</p><p>I am Panos M., humble, backend software reader and writer at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://talentprotocol.com">Talent Protocol</a>.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/aca9cc3c57f200a80908354ef28902793fbe28600a474761329c11d1b6de6f24.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Gas Optimization - Yul/Assembly]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/gas-optimization-yul-assembly</link>
            <guid>t9cYR8sR1hU8nSwTk3nQ</guid>
            <pubDate>Sun, 22 Dec 2024 11:59:06 GMT</pubDate>
            <description><![CDATA[Cover Image by DESPOINA MATSINOPOULOU from PixabayIntroductionRecently, I followed the Advanced Solidity Bootcamp organized by Encode Club and delivered by Extropy. Among the things we learnt, it was about gas optimization. The two many areas we touched in gas optimization were:Storage optimizationConverting Solidity to Yul/Assembly.Then I decided to apply this knowledge to one Talent Protocol production smart contract. The TalentCommunitySale.sol published in this repository here. https://gi...]]></description>
            <content:encoded><![CDATA[<p><em>Cover Image by </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com/users/despoinamatsinopoulou-42753791/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=9134616"><em>DESPOINA MATSINOPOULOU</em></a><em> from </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=9134616"><em>Pixabay</em></a></p><h2 id="h-introduction" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Introduction</h2><p>Recently, I followed the Advanced Solidity Bootcamp organized by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.encode.club/">Encode Club</a> and delivered by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.extropy.io/">Extropy</a>.</p><p>Among the things we learnt, it was about gas optimization. The two many areas we touched in gas optimization were:</p><ul><li><p>Storage optimization</p></li><li><p>Converting Solidity to Yul/Assembly.</p></li></ul><p>Then I decided to apply this knowledge to one Talent Protocol production smart contract. The <code>TalentCommunitySale.sol</code> published in this repository here.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/talentprotocol/contracts/blob/master/contracts/talent/TalentCommunitySale.sol">https://github.com/talentprotocol/contracts/blob/master/contracts/talent/TalentCommunitySale.sol</a></p><p>Note: If you want to jump into the code, click <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/TalentCommunitySale/blob/main/src/TalentCommunitySale.sol">here</a>.</p><h2 id="h-business-logic" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Business Logic</h2><p>A quick note on the business logic of this contract.</p><p>Talent Protocol published this contract to allow for interested builders to pre-purchase $TALENT tokens by depositing an amount of $USDC.</p><p>There were 4 different tiers:</p><ul><li><p>Tier 1: 100 USDC</p></li><li><p>Tier 2: 250 USDC</p></li><li><p>Tier 3: 500 USDC</p></li><li><p>Tier 4: 1000 USDC</p></li></ul><p>The picture below tries to depict the main business flow:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e95736f7785e116ea92162c1ebb7e65a252c5f96e2a18100a046f5b08aea73a6.png" alt="Main TalentCommunitySale Business Flow" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Main TalentCommunitySale Business Flow</figcaption></figure><p>Assuming that Bonnie wants to deposit 100USDC, she would call <code>TalentCommunitySale#buyTier1()</code></p><p>There are requirements for this transaction to succeed:</p><ul><li><p>The Sale should be active. Note that the Sale ran for a specific period of time in July 2024 (you can see the public contract <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://basescan.org/address/0xcd2fbec25b07f065bc905051c801df66a5b48512">here</a>)</p></li><li><p>Bonnie should have enough $USDC, i.e. at least 100 for tier 1.</p></li><li><p>Bonnie should have approved <code>TalentCommunitySale</code> contract to <em>spend</em> as a <em>spender</em> 100USDC on her behalf.</p></li><li><p>Also, for each tier, Talent Protocol, had specified a maximum number of sales. This was registered in the contract itself. After this number, tier was considered sold out.</p></li><li><p>Finally, the same buyer was not allowed to purchase more than once, even if it were on different tiers.</p></li></ul><p>All these business rules were coded as part of each <code>buyTierX()</code> function in the original contract code. Here is the related snippet for <code>buyTier1()</code>:</p><pre data-type="codeBlock" text="require(saleActive, &quot;TalentCommunitySale: Sale is not active&quot;);
require(
    paymentToken.allowance(msg.sender, address(this)) &gt;= 100 * 10**tokenDecimals,
    &quot;TalentCommunitySale: Insufficient allowance&quot;
);
require(tier1Bought &lt; TIER1_MAX_BUYS, &quot;TalentCommunitySale: Tier 1 sold out&quot;);
require(!listOfBuyers[msg.sender], &quot;TalentCommunitySale: Address already bought&quot;);
require(paymentToken.transferFrom(msg.sender, receivingWallet, 100 * 10**tokenDecimals), &quot;Transfer failed&quot;);
"><code><span class="hljs-built_in">require</span>(saleActive, <span class="hljs-string">"TalentCommunitySale: Sale is not active"</span>);
<span class="hljs-built_in">require</span>(
    paymentToken.allowance(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">100</span> <span class="hljs-operator">*</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>tokenDecimals,
    <span class="hljs-string">"TalentCommunitySale: Insufficient allowance"</span>
);
<span class="hljs-built_in">require</span>(tier1Bought <span class="hljs-operator">&#x3C;</span> TIER1_MAX_BUYS, <span class="hljs-string">"TalentCommunitySale: Tier 1 sold out"</span>);
<span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>listOfBuyers[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>], <span class="hljs-string">"TalentCommunitySale: Address already bought"</span>);
<span class="hljs-built_in">require</span>(paymentToken.transferFrom(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, receivingWallet, <span class="hljs-number">100</span> <span class="hljs-operator">*</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>tokenDecimals), <span class="hljs-string">"Transfer failed"</span>);
</code></pre><h2 id="h-test-coverage" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Test Coverage</h2><p>Before trying to optimize a Smart Contract code, and as a good practice in refactoring any code in software engineering, one should have very good test coverage.</p><p>The original smart contract had some coverage with tests written in <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hardhat.org/">Hardhat</a> and <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.ethers.org/v5/">Ethers</a>.</p><p>I decided to go with <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://book.getfoundry.sh/">foundry</a>, mainly because that was the tool we were taught in the bootcamp.</p><h3 id="h-test-source-code" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Test Source Code</h3><p>The test source code can be found <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/TalentCommunitySale/blob/main/test/TalentCommunitySale.t.sol">here</a>.</p><h3 id="h-test-main-takeaways" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Test Main Takeaways</h3><p><strong>Dependency on an ERC20 Token</strong></p><p>When your contract depends on an ERC20 Token, you can use a Mock smart contract while testing. This is the purpose of the <code>ERC20Mock.sol</code> (and the <code>ERC20MockBad.sol</code> which is specially used for testing <code>ReentrancyGuard</code>)</p><p><strong>Test <em>3rd-Party</em> Inherited Contracts</strong></p><p>When a contract derives from <em>3rd-Party</em> inhered contracts, like <code>OpenZeppelin</code> <code>Ownable</code> and <code>ReentrancyGuard</code>, you have to write tests for all the public features/functions these contracts make your contract expose. This is because inheriting from these 3rd-party contracts is an <em>implementation detail,</em> which you might decide to change in the future, but still want to keep the functionality.</p><p>This is why you will see tests that test the <em>Ownership</em> features inherited from <code>Ownable</code> and a couple of tests testing <code>ReentrancyGuard</code>.</p><p><strong>Testing</strong> <code>ReentrancyGuard</code></p><p>This deserves its own paragraph in this post, because I had to use a special <code>ERC20MockBad.sol</code> contract to simulate the behavior of a bad contract whose function is trying to call back to the calling function on the calling contract.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/de3b770230cb8d64a28e9dc22099b7fc711799cd76f3dc88ea7fec6ba57e3b0b.png" alt="Calling " blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Calling</figcaption></figure><p>I implemented the <code>ERC20MockBad.sol</code> contract to call back to the <code>buyTier1()</code> function that is calling the bad’s contract <code>transferFrom()</code> function:</p><pre data-type="codeBlock" text="function transferFrom(address, address, uint256) public override returns (bool) {
    msg.sender.functionCall(abi.encodeWithSignature(&quot;buyTier1()&quot;));

    return true;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferFrom</span>(<span class="hljs-params"><span class="hljs-keyword">address</span>, <span class="hljs-keyword">address</span>, <span class="hljs-keyword">uint256</span></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
    <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.functionCall(abi.encodeWithSignature("buyTier1()<span class="hljs-string">"));

    return true;
}
</span></code></pre><p>Testing the re-entrancy had one glitch. The following code should have worked but it didn’t:</p><pre data-type="codeBlock" text="vm.prank(caller);
vm.expectRevert(abi.encodeWithSelector(TalentCommunitySale.ReentrancyGuardReentrantCall.selector));
talentCommunitySaleBad.buyTier1();
"><code>vm.prank(caller);
vm.expectRevert(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSelector</span>(TalentCommunitySale.ReentrancyGuardReentrantCall.<span class="hljs-built_in">selector</span>));
talentCommunitySaleBad.buyTier1();
</code></pre><p>The error that it threw was:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ecfa81ae705f67b874b57b235bd0a8bad5edfd8670985b607c6e1a8c6ffd2037.png" alt="Strange Error on Reentrancy Guard Test" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Strange Error on Reentrancy Guard Test</figcaption></figure><p>In order to sort this out, I had to write:</p><pre data-type="codeBlock" text="vm.prank(caller);
try talentCommunitySaleBad.buyTier1() {}
catch (bytes memory err) {
    assertEq(bytes4(err), TalentCommunitySale.ReentrancyGuardReentrantCall.selector);
}
"><code>vm.prank(caller);
<span class="hljs-keyword">try</span> talentCommunitySaleBad.buyTier1() {}
<span class="hljs-keyword">catch</span> (<span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> err) {
    assertEq(<span class="hljs-keyword">bytes4</span>(err), TalentCommunitySale.ReentrancyGuardReentrantCall.<span class="hljs-built_in">selector</span>);
}
</code></pre><p>Here, I catch the error and I check that the first 4 bytes of the error caught are equal to the selector for the custom error <code>TalentCommunitySale.ReentrancyGuardReentrantCall</code>.</p><p><strong>Test Coverage</strong></p><p>Foundry has a very good test coverage report:</p><p>One can run:</p><pre data-type="codeBlock" text="$ forge coverage
"><code><span class="hljs-variable">$ </span>forge coverage
</code></pre><p>and Foundry will run the tests and print the report. A report like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4c49521390b87c44d93c1f6f53ced493e34f1af57df617eeeff45f2b35e084e6.png" alt="TalentCommunitySale - Test Coverage Report" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">TalentCommunitySale - Test Coverage Report</figcaption></figure><p><strong>Printing Messages to Console</strong></p><p>Another very useful utility that helps debugging inside your tests is the <code>console</code> utility, which you can import from <code>forge-std/Test.sol</code>.</p><p><strong>USDC Dependency</strong></p><p>The <code>USDCTMock</code> is an ERC-20 compatible contract that I used in my tests, instead of the real <code>USDC</code> contract.</p><p><strong>Main Foundry Cheat Codes</strong></p><p>The main Foundry cheat codes that I used:</p><ul><li><p><code>vm.prank()</code> to set the <code>msg.sender</code> of the following transaction.</p></li><li><p><code>vm.expectRevert()</code> to set revert expectations.</p></li><li><p><code>vm.expectEmit()</code> to set emit event expectations.</p></li></ul><h2 id="h-storage-layout" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Storage Layout</h2><p>Initially, I did the exercise of reducing the number of slots the contract occupied.</p><p>I can get the storage layout and information about how state variables use the storage with the following Foundry command:</p><pre data-type="codeBlock" text="$ forge inspect --pretty TalentCommunitySale storageLayout
"><code>$ forge inspect <span class="hljs-operator">-</span><span class="hljs-operator">-</span>pretty TalentCommunitySale storageLayout
</code></pre><p>Initially, before I do any change, the storage layout was:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/187ddaf2c40157d339920fe6dddc1dc91f26f0586f703b104a02429c73a6bff6.png" alt="Initial Storage Layout" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Initial Storage Layout</figcaption></figure><p>As you can see, it occupied 8 slots.</p><p>At the end of the exercise, the storage layout became this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/16a56c2d07b34b21113360a09340e3f87eaee0b560a07082e9e8297544082633.png" alt="Storage Layout After" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Storage Layout After</figcaption></figure><p>As you can see, the optimized storage, uses 3 less slots. Instead of 8, it uses 5.</p><h2 id="h-code-optimizations" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Code optimizations</h2><p>With regards to code optimization, as I said at the beginning, I converted a lot of Solidity code to Yul/Assembly code.</p><p>The source code can be found <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pmatsinopoulos/TalentCommunitySale/blob/main/src/TalentCommunitySale.sol">here</a>. But the key takeaways I would like to mention in this blog post are:</p><h3 id="h-dont-use-magic-numbers-use-constants" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Don’t use Magic Numbers - Use Constants</h3><p>Don’t just throw magic numbers like <code>100</code> in the code. Use constants instead. Also, don’t just use state variables without making them <code>constant</code>, because you are wasting storage space.</p><p>This is a good example:</p><pre data-type="codeBlock" text="uint32 public constant TIER1_MAX_BUYS = 100;
"><code><span class="hljs-keyword">uint32</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> TIER1_MAX_BUYS <span class="hljs-operator">=</span> <span class="hljs-number">100</span>;
</code></pre><p>Just keep in mind though, that, unfortunately, constants you declare at the Solidity level, can’t be used at the assembly level. This means that you might need to assign the constant to a local variable before being able to access it inside an <code>assembly</code> block.</p><h3 id="h-immutable" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Immutable</h3><p>If something takes a value inside the constructor and never changes again, declare it as <code>immutable</code>. It saves storage space.</p><p>Example:</p><pre data-type="codeBlock" text="uint256 public immutable TIER1_AMOUNT;
"><code><span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">immutable</span> TIER1_AMOUNT;
</code></pre><h3 id="h-slot-numbers-offsets-and-sizes-as-private-constants" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Slot Numbers, Offsets and Sizes as Private Constants</h3><p>When I wrote Yul/Assembly I had to reference slot numbers, offsets and sizes of state variables. If you don’t use private constants to refer to them it will be difficult to update your code should you decide to further optimize the storage. Also, it makes it easier to read when you use constants. It is another case to avoid magic numbers.</p><p>Example:</p><pre data-type="codeBlock" text="uint8 private constant STORAGE_TIER1_BOUGHT_SLOT = 3;
uint8 private constant STORAGE_TIER1_BOUGHT_OFFSET = 20;
"><code><span class="hljs-keyword">uint8</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> STORAGE_TIER1_BOUGHT_SLOT <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;
<span class="hljs-keyword">uint8</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> STORAGE_TIER1_BOUGHT_OFFSET <span class="hljs-operator">=</span> <span class="hljs-number">20</span>;
</code></pre><h3 id="h-removed-dependencies-to-openzeppelin-ownable-and-reentrancyguard" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Removed Dependencies to OpenZeppelin <code>Ownable</code> and <code>ReentrancyGuard</code></h3><p>Implementation of ownership and reentrancy guard has been moved inside the contract code. This allowed us to write the functions inherited in our own assembly implementation alongside with other optimizations.</p><h3 id="h-removed-dependency-to-math" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Removed Dependency to <code>Math</code></h3><p>This library was not necessary. I removed it.</p><h3 id="h-removed-require-calls" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Removed <code>require</code> Calls</h3><p>I removed all the <code>require</code> calls. I replaced them with <code>revert</code> calls and custom errors.</p><h3 id="h-non-slot-aligned-variable-update-is-hard" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Non-Slot Aligned Variable Update Is Hard</h3><p>Reducing the slots in the storage has a disadvantage when you write assembly. It requires a lot of careful bit operations to change the value of a variable which does not have its own dedicated slot.</p><p>Take for example the <code>saleActive</code> <code>bool</code> which is in the middle of slot <code>5</code>. It occupies <code>1</code> byte at offset <code>20</code>.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4de9d3ca51fa42e43c1ed95aff242c52e64570eec8ef108275a5d74e777570ef.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>If I want to set it to <code>true</code>, I have to write this assembly code:</p><pre data-type="codeBlock" text="assembly {
    let slotSaleActiveValue := sload(STORAGE_SALE_ACTIVE_SLOT)
    let offsetBits := mul(STORAGE_SALE_ACTIVE_OFFSET, 8)
    let zeroMask := not(shl(offsetBits, 0xFF))
    let setMask := shl(offsetBits, 0x01)
    sstore(STORAGE_SALE_ACTIVE_SLOT, or(and(slotSaleActiveValue, zeroMask), setMask))
}
"><code><span class="hljs-keyword">assembly</span> {
    <span class="hljs-keyword">let</span> slotSaleActiveValue <span class="hljs-operator">:=</span> <span class="hljs-built_in">sload</span>(STORAGE_SALE_ACTIVE_SLOT)
    <span class="hljs-keyword">let</span> offsetBits <span class="hljs-operator">:=</span> <span class="hljs-built_in">mul</span>(STORAGE_SALE_ACTIVE_OFFSET, <span class="hljs-number">8</span>)
    <span class="hljs-keyword">let</span> zeroMask <span class="hljs-operator">:=</span> <span class="hljs-built_in">not</span>(<span class="hljs-built_in">shl</span>(offsetBits, <span class="hljs-number">0xFF</span>))
    <span class="hljs-keyword">let</span> setMask <span class="hljs-operator">:=</span> <span class="hljs-built_in">shl</span>(offsetBits, <span class="hljs-number">0x01</span>)
    <span class="hljs-built_in">sstore</span>(STORAGE_SALE_ACTIVE_SLOT, <span class="hljs-built_in">or</span>(<span class="hljs-built_in">and</span>(slotSaleActiveValue, zeroMask), setMask))
}
</code></pre><p>if <code>saleActive</code> occupied an entire slot, then it would have only be a case of calling just the <code>sstore()</code> statement. But with the <code>saleActive</code> being in between other variable values in slot <code>5</code>, I need to make sure that I only update the specific <code>byte</code> at offset <code>20</code> and leave the rest as it was.</p><h3 id="h-revert-with-custom-code-in-assembly" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Revert With Custom Code in Assembly</h3><p>When reverting with a custom code in <code>assembly</code> here is how to do it:</p><ol><li><p>save the free memory pointer to use it as the memory point to store revert information.</p></li></ol><pre data-type="codeBlock" text="let freeMemoryPointer := mload(0x40)
let initialFreeMemoryPointer := freeMemoryPointer
"><code><span class="hljs-built_in">let</span> freeMemoryPointer := mload(0x40)
<span class="hljs-built_in">let</span> initialFreeMemoryPointer := freeMemoryPointer
</code></pre><ol start="2"><li><p>Assuming that I want to revert with the custom error <code>OwnableUnauthorizedAccount(address)</code> , I have to store the error identifier:</p></li></ol><pre data-type="codeBlock" text="mstore(freeMemoryPointer, shl(mul(28, 8), 0x118cdaa7))
freeMemoryPointer := add(freeMemoryPointer, 4)
"><code><span class="hljs-built_in">mstore</span>(freeMemoryPointer, shl(mul(<span class="hljs-number">28</span>, <span class="hljs-number">8</span>), <span class="hljs-number">0</span>x118cdaa7))
freeMemoryPointer := <span class="hljs-built_in">add</span>(freeMemoryPointer, <span class="hljs-number">4</span>)
</code></pre><p>Note: How do I find the identifier? Various different methods. You can use the <code>cast</code> utility that is coming with foundry:</p><pre data-type="codeBlock" text="$ cast keccak &apos;OwnableUnauthorizedAccount(address)&apos;
0x118cdaa7a341953d1887a2245fd6665d741c67c8c50581daa59e1d03373fa188
"><code><span class="hljs-variable">$ </span>cast keccak <span class="hljs-string">'OwnableUnauthorizedAccount(address)'</span>
<span class="hljs-number">0x118cdaa7a341953d1887a2245fd6665d741c67c8c50581daa59e1d03373fa188</span>
</code></pre><p>… and take the first 4 bytes (<code>0x118cdaa7</code>).</p><p>The identifier, <code>0x118cdaa7</code> needs to be left aligned before being stored in the memory at the free memory pointer. That’s why I do <code>shl(mul(28, 8), 0x118cdaa7))</code>. I shift left by <code>28</code> bits (which is the result of <code>32</code>, the total size of bits of the memory slot, minus <code>4</code>, which is the side of the identifier).</p><p>Which makes the memory position looking something like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9c1bcf7c6bcf89cf9b23bda38aaaf098bff8085468da28e48cecefefe6a2e284.png" alt="Memory with 0x118cdaa7 stored left aligned" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Memory with 0x118cdaa7 stored left aligned</figcaption></figure><p>Note: After I set a value to the memory, I then update the free memory pointer value to point to the next position available for writing to memory. That’s the second command you see in the snippet above: <code>freeMemoryPointer := add(freeMemoryPointer, 4)</code>. I move the free memory pointer by <code>4</code> which is the number of bytes occupied by the identifier.</p><ol start="3"><li><p>Then I write the run-time argument values to the custom error. In our case, I only have one argument, the <code>msg.sender</code>, an argument of type <code>address</code>.</p></li></ol><p>In <code>assembly</code> the <code>msg.sender</code> is accessed with the Yul function <code>caller()</code>.</p><p>The <code>address</code> type in Ethereum is <code>20</code> bytes long, but, in Yul/assembly it is always right aligned in <code>32</code> bytes with leading <code>0</code>s. This is because the only available type in Yul/assembly is <code>u256</code> i.e. a 32 bytes long unsigned integer.</p><p>Hence, I am not going to write <code>20</code> bytes into the memory. I am going to write <code>32</code> bytes.</p><pre data-type="codeBlock" text="mstore(freeMemoryPointer, caller())
freeMemoryPointer := add(freeMemoryPointer, 32)
"><code><span class="hljs-built_in">mstore</span>(freeMemoryPointer, caller())
freeMemoryPointer := <span class="hljs-built_in">add</span>(freeMemoryPointer, <span class="hljs-number">32</span>)
</code></pre><p>This means that both identifier and caller address will be stored in memory like this:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d20a83e1f247f7d0f4556560f42a53c6048de44181dba703484b7e78244c515a.png" alt="Memory storing identifier and caller address" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Memory storing identifier and caller address</figcaption></figure><p>Then, as part of a good practice, I save the new free memory pointer value:</p><pre data-type="codeBlock" text="mstore(0x40, freeMemoryPointer)
"><code><span class="hljs-built_in">mstore</span>(<span class="hljs-number">0</span>x40, freeMemoryPointer)
</code></pre><ol start="4"><li><p>Reverting</p></li></ol><p>Finally, reverting is a matter of calling the <code>revert</code> function with the memory pointer holding the revert data, i.e. the identifier and the caller address:</p><pre data-type="codeBlock" text="revert(initialFreeMemoryPointer, 36)
"><code><span class="hljs-built_in">revert</span>(initialFreeMemoryPointer, <span class="hljs-number">36</span>)
</code></pre><p>Reverting with custom error that takes more than one run-time argument values follows the same technique and rules.</p><h3 id="h-emitting-events" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Emitting Events</h3><p>Emitting events in assembly, is a matter of using the correct <code>logX()</code> function.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/73129db6291405e26723dca62c844d2dc18400f95f865055f409f19771b9e2b8.png" alt="logX() functions" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">logX() functions</figcaption></figure><p>I will show you an example for the event <code>OwnershipTransferred(address,address)</code>.</p><p>This event is actually declared as:</p><pre data-type="codeBlock" text="event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
);
"><code><span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">OwnershipTransferred</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> previousOwner,
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> newOwner
</span>)</span>;
</code></pre><p>This is a 3 topics event, because the signature of the event is always the first topic</p><ul><li><p><code>OwnershipTransferred(address,address)</code></p></li><li><p><code>previousOwner</code></p></li><li><p><code>newOwner</code></p></li></ul><p>with no extra data.</p><p>Hence, I am using the <code>log3</code> function.</p><pre data-type="codeBlock" text="log3(
  0x00,
  0x00,
  0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0, 
  oldOwner,
  newOwner
)
"><code><span class="hljs-built_in">log3</span>(
  <span class="hljs-number">0</span>x00,
  <span class="hljs-number">0</span>x00,
  <span class="hljs-number">0</span>x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0, 
  oldOwner,
  newOwner
)
</code></pre><p>How do I get the signature of the event? Again I can use different methods. Like <code>cast</code> or <code>forge</code>. Let’s use <code>forge</code> here:</p><pre data-type="codeBlock" text="$ forge inspect --pretty TalentCommunitySale events
{
  &quot;OwnershipTransferred(address,address)&quot;: &quot;0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0&quot;,
  ...
}
"><code>$ forge inspect <span class="hljs-operator">-</span><span class="hljs-operator">-</span>pretty TalentCommunitySale events
{
  <span class="hljs-string">"OwnershipTransferred(address,address)"</span>: <span class="hljs-string">"0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0"</span>,
  ...
}
</code></pre><h3 id="h-accessing-dynamic-variables-mapping" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Accessing Dynamic Variables - <code>mapping</code></h3><p><code>mapping</code> type variables do not store their payload at the position in storage at which they are declared. They occupy 1 slot in storage and they set the value of it to be <code>0x0</code>.</p><p>The actual payload is stored in storage at positions which are dynamically determined.</p><p>I will explain this with the example of the variable:</p><pre data-type="codeBlock" text="mapping(address =&gt; bool) public listOfBuyers;  
"><code><span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">bool</span>) <span class="hljs-keyword">public</span> listOfBuyers;  
</code></pre><p>The storage layout shows that it is stored on slot number 2:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a72f325be64eb85361b4e3c2b6b910088c23e4f6b14330b68dc22e6c7f7ca8a1.png" alt="Only 1 slot for listOfBuyers" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Only 1 slot for listOfBuyers</figcaption></figure><p>But if I deploy the contract and check with <code>cast storage &lt;contract-address&gt; 2</code> I will see the value of this slot being <code>0x0</code>:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8bc1251e8f52c7b3bd6a10466797cee6343d4e8fa54471493ef47070f52b0120.png" alt="0x0 value for slot 2" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">0x0 value for slot 2</figcaption></figure><p>But, if I have stored the <code>true</code> for the address <code>0x324e9E13dd19528D0F390201923d17c4B7E94462</code>, then this is stored at the position:</p><p><code>keccak256(0x324e9E13dd19528D0F390201923d17c4B7E94462,2)</code></p><p>The <code>keccak256()</code> assembly function returns a 32 byte number which is the slot number in the storage for the given address (<code>0x324…</code>) and slot number <code>2</code>. To be precise, the arguments to the Yul/assembly function are pointing to the memory. Hence, I first have to store the address and slot number into the memory.</p><p>Here is the <code>assembly</code> code that accesses a mapping using this method. I am checking whether the <code>caller()</code> is in the <code>listOfBuyers</code> by accessing the <code>listOfBuyers[caller()]</code>.</p><pre data-type="codeBlock" text="let buyerAddress := caller()
let freeMemoryPointer := mload(0x40)
let initialFreeMemoryPointer := freeMemoryPointer

mstore(freeMemoryPointer, buyerAddress) // 32 bytes
freeMemoryPointer := add(freeMemoryPointer, 32)

mstore(freeMemoryPointer, STORAGE_LIST_OF_BUYERS_SLOT)
freeMemoryPointer := add(freeMemoryPointer, 32)
mstore(0x40, freeMemoryPointer)

let listOfBuyersSlotForBuyer := keccak256(initialFreeMemoryPointer, 64)

sload(listOfBuyersSlotForBuyer)
"><code>let buyerAddress := <span class="hljs-built_in">caller</span>()
let freeMemoryPointer := <span class="hljs-built_in">mload</span>(<span class="hljs-number">0</span>x40)
let initialFreeMemoryPointer := freeMemoryPointer

<span class="hljs-built_in">mstore</span>(freeMemoryPointer, buyerAddress) // <span class="hljs-number">32</span> bytes
freeMemoryPointer := <span class="hljs-built_in">add</span>(freeMemoryPointer, <span class="hljs-number">32</span>)

<span class="hljs-built_in">mstore</span>(freeMemoryPointer, STORAGE_LIST_OF_BUYERS_SLOT)
freeMemoryPointer := <span class="hljs-built_in">add</span>(freeMemoryPointer, <span class="hljs-number">32</span>)
<span class="hljs-built_in">mstore</span>(<span class="hljs-number">0</span>x40, freeMemoryPointer)

let listOfBuyersSlotForBuyer := <span class="hljs-built_in">keccak256</span>(initialFreeMemoryPointer, <span class="hljs-number">64</span>)

<span class="hljs-built_in">sload</span>(listOfBuyersSlotForBuyer)
</code></pre><ol><li><p>I save the address to memory <code>mstore(freeMemoryPointer, buyerAddress)</code></p></li><li><p>I save the slot number <code>mstore(freeMemoryPointer, STORAGE_LIST_OF_BUYERS_SLOT)</code></p></li><li><p>I calculate the <code>keccak256()</code> 32 number by sending the <code>64</code> bytes of memory to the <code>keccak256()</code> function.</p></li></ol><h2 id="h-conclusion" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Conclusion</h2><p>The whole exercise was really useful to me to learn Yul/Assembly and how EVM works. I understand that I have a lot more to learn. I will keep on studying and working on this.</p><h2 id="h-what-i-do" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">What I Do?</h2><p>I am a happy software engineer, working for <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.talentprotocol.com">Talent Protocol</a>, but also for personal projects like a <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.rayo.gr">Web-2-SMS platform</a>. I also like to play tennis and the piano. I read a lot of books. I have a lovely family of humans and animals.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c39e4b8850225166fa6be26b5effcfb6933a804685ee028d96cb0e0d45a690e5.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Arigatai; Express Gratitude That Stays In History]]></title>
            <link>https://paragraph.com/@panagiotismatsinopoulos/arigatai-express-gratitude-that-stays-in-history</link>
            <guid>CjMyQYFbEJAgRTRaiJxO</guid>
            <pubDate>Sat, 28 Sep 2024 09:11:11 GMT</pubDate>
            <description><![CDATA[IntroductionThis is the idea behind Arigatai, a project that dreams to save the gratitude statements in history.When I First Had The Idea for a #thankyou Slack ChannelBack when I worked at Lavanda, I introduced the #thankyou Slack channel. Why? I wanted to express my gratitude to another colleague, in public. Why? I always sent my gratitude with a thank you message to the person having offered his time, knowledge and help, to make my life at work better...but I didn&apos;t want this to stay o...]]></description>
            <content:encoded><![CDATA[<h1 id="h-introduction" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Introduction</h1><p>This is the idea behind <code>Arigatai</code>, a project that dreams to save the gratitude statements in history.</p><h1 id="h-when-i-first-had-the-idea-for-a-thankyou-slack-channel" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">When I First Had The Idea for a <code>#thankyou</code> Slack Channel</h1><p>Back when I worked at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.getlavanda.com/">Lavanda</a>, I introduced the <code>#thankyou</code> Slack channel.</p><p>Why?</p><p>I wanted to express my gratitude to another colleague, <strong>in public</strong>.</p><p>Why?</p><p>I always sent my gratitude with a <em>thank you</em> message to the person having offered his time, knowledge and help, to make my life at work better...but I didn&apos;t want this to stay only between us.</p><ul><li><p>That person made me happy, and I wanted to show to the rest of the company <strong>that I was happy</strong>.</p></li><li><p>I also wanted to show to the rest of the company my happiness of <strong>having colleagues who could make others happy</strong>.</p></li><li><p>I wanted to show to the rest of the company <strong>who is the person that can make others happy</strong>.</p></li><li><p>I believe that my colleagues who can make others happy, they deserve to be, at least, <strong>recognized in public</strong>.</p></li></ul><p>And, I wanted to make public, <strong>the reason I am thanking</strong> my colleague. Not many details. Brief short phrase to let anyone actually be in my shoes for the reason I am happy for. To give a little more context and value to this open gratitude.</p><p>Then, everyone went mad about it. They loved it.</p><p>And they even created a <em>thank you</em> counter. The <strong>tacos</strong>.</p><p>And they put some limits. Every day you can thank up to 5 times. You can give your 5 tacos to the same person, if you want, or give each one of the 5 to different. Doesn&apos;t matter, but you can only spend up to 5 tacos each day.</p><p>And they even have a leader board:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4c1ab8d8c548d59c28d1b5a34eeb4c1d57bd60c2961aa3b48012832004b3fb9e.jpg" alt="Lavanda Taco Factory Leader Board" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Lavanda Taco Factory Leader Board</figcaption></figure><p>So, employees, earned tacos on a daily basis. And they gave tacos to other colleagues too.</p><h1 id="h-other-companies" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Other Companies</h1><p>Is this a common practice in companies. I didn&apos;t know. Until I saw this:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://handbook.gitlab.com/handbook/values/#say-thanks">https://handbook.gitlab.com/handbook/values/#say-thanks</a></p><p>Ohhh! They have a <code>#thanks</code> channel. Similar to the one I introduced at Lavanda! I was so happy I saw that.</p><p>And then I also found this:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://heytaco.com/">https://heytaco.com/</a></p><p>A company that offers a <em>peer-to-peer recognition platform</em>. Great!</p><p>So, it make sense!</p><h1 id="h-the-problem" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The Problem</h1><blockquote><p>All tacos were deleted 😭.</p></blockquote><blockquote><p>I left Lavanda, my tacos have gone away. I couldn&apos;t take my tacos with me 😢.</p></blockquote><p>I guess you immediately get the point.</p><p>The tacos, they presented a <em>value</em> to me, which</p><ul><li><p>I lose when I leave the company,</p></li><li><p>I lose when the centralized entity that keeps track of them stops to exist.</p></li></ul><p><em>So what</em> one might say!</p><p>I am saying that gratitude that I receive throughout my professional life should belong to me.</p><p>It is a value that I want to keep on having, forever, registered in history.</p><p>And I want to accumulate such value even, going from one company to the next.</p><p><strong>It is part of my reputation as a professional</strong>.</p><h1 id="h-the-solution" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The Solution</h1><p>Join <strong>Arigatai</strong>.</p><h2 id="h-erc-20-token" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC-20 Token</h2><p>Arigatai, with symbol <code>ARIG</code>, stores the gratitude value on Ethereum, as an <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/en/developers/docs/standards/tokens/erc-20/">ERC-20 token</a>.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9fc939c6479b8922eec0c6db9a97ab5b3afe5d60b388f62bb273fd258b77ebe9.png" alt="ARIGATAI is an ERC-20 Token" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">ARIGATAI is an ERC-20 Token</figcaption></figure><p><code>ARIG</code>, is my professional reputation points on the <em>gratitude</em> dimension.</p><h2 id="h-minting" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Minting</h2><p>When <code>Arigatai</code> Smart Contract is deployed it doesn&apos;t have any initial circulation. I.e. the circulating supply is <code>0</code>.</p><p>Minting starts when clients call the external functions <code>arigataiDeliver()</code> and <code>arigatoAccept()</code></p><p>This is the signature of the function:</p><pre data-type="codeBlock" text="function arigatoDeliver(
  address _recipient, // The EOA that will receive the `ARIG`
  uint256 _amount, // The amount of `ARIG` the _recipient will get
  bytes[640] memory _reason, // The reason sender is sending the _recipient this amount of ARIG
) external;
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">arigatoDeliver</span>(<span class="hljs-params">
  <span class="hljs-keyword">address</span> _recipient, <span class="hljs-comment">// The EOA that will receive the `ARIG`</span>
  <span class="hljs-keyword">uint256</span> _amount, <span class="hljs-comment">// The amount of `ARIG` the _recipient will get</span>
  <span class="hljs-keyword">bytes</span>[<span class="hljs-number">640</span>] <span class="hljs-keyword">memory</span> _reason, <span class="hljs-comment">// The reason sender is sending the _recipient this amount of ARIG</span>
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
</code></pre><p>There are some rules for this call to succeed.</p><ol><li><p>The caller should be an EOA, not a contract.</p></li><li><p>Recipient should be an EOA, not a contract.</p></li><li><p>The caller should not exceed the maximum daily delivery amount which is 5.</p></li></ol><p>This function does not actually <em>create</em> <code>ARIG</code>. It is the <code>arigatoAccept()</code> that does.</p><pre data-type="codeBlock" text="function arigatoAccept(
  address _sender, // The EOA that sent the arigatoRequest()
  uint256 _amount, // The amount of $ARIG that will be accepted
) external;
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">arigatoAccept</span>(<span class="hljs-params">
  <span class="hljs-keyword">address</span> _sender, <span class="hljs-comment">// The EOA that sent the arigatoRequest()</span>
  <span class="hljs-keyword">uint256</span> _amount, <span class="hljs-comment">// The amount of $ARIG that will be accepted</span>
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
</code></pre><ol><li><p>The caller needs to have a pending <code>ARIG</code> delivery from the <code>_sender</code>.</p></li><li><p>The pending delivery should be for the <code>_amount</code>.</p></li><li><p>The delivery should have been created within the last 24 hours from acceptance.</p></li></ol><p>So, it is the <code>arigatoAccept()</code> that does the actual minting of the <code>ARIG</code> token.</p><p>When <code>arigatoAccept()</code> succeeds, the caller gets the amount of <code>ARIG</code> and there is a record created (<code>Arigato</code> record) in the state of the contract to record the details of the reward.</p><p>The <code>arigatoAccept()</code> will internally call a <code>_mint()</code> function like:</p><pre data-type="codeBlock" text="_mint(msg.sender, _amount)
"><code>_mint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, _amount)
</code></pre><p>and will also take care to create the <code>Arigato</code> record.</p><h2 id="h-properties-of-the-erc-20-token" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Properties of the ERC-20 Token</h2><ul><li><p><code>name</code>: <code>ARIGATAI</code></p></li><li><p><code>symbol</code>:<code>ARIG</code></p></li><li><p><code>decimals</code>: <code>0</code>. The value <code>ARIG</code> token is not divisible.</p></li></ul><h2 id="h-how-can-an-eoa-find-the-list-of-its-arigato-records" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">How can an EOA Find The List of its <code>Arigato</code> Records?</h2><p>There is a function that allows pagination access to the <code>Arigato</code> records of an address:</p><pre data-type="codeBlock" text="function getArigatos(address receiver, uint256 start, uint256 count) public view returns (Arigato[] memory);
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getArigatos</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> receiver, <span class="hljs-keyword">uint256</span> start, <span class="hljs-keyword">uint256</span> count</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params">Arigato[] <span class="hljs-keyword">memory</span></span>)</span>;
</code></pre><p>Also, there is a function that returns the total number of <code>Arigato</code> records of an address:</p><pre data-type="codeBlock" text="function getArigatosCount(address receiver) public view returns uint256;
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getArigatosCount</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> receiver</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> <span class="hljs-title"><span class="hljs-keyword">uint256</span></span></span>;
</code></pre><h2 id="h-how-can-we-have-a-leader-board" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">How can we have a Leader Board?</h2><p>Contract keeps track of the recipients and their <code>Arigato</code> records. Building a Leader Board, is outside of the scope of the Smart Contract, but we have other plans for it.</p>]]></content:encoded>
            <author>panagiotismatsinopoulos@newsletter.paragraph.com (Panos Matsinopoulos)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/a9eeb3b4b88c1aeb624dc1eef541aa5bf309eb766e46059fbe4ac247f55f864b.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>