5 things Railway’s 8 hour outage should change about how you think about redundancy

Railway runs on Google Cloud, AWS, and its own metal.

So when I first saw that Railway was down for hours, my first thought was probably the same as yours.

“How does a multi cloud platform go dark like that?”

Then I read the incident report, the Hacker News discussion, and the follow up coverage. And the real lesson is uncomfortable.

This was not really a cloud outage.

The servers did not all die. AWS did not die. Railway Metal did not die. Google Cloud infrastructure itself did not have to collapse.

What failed was much higher up the stack.

The account.

Google Cloud placed Railway’s production account into suspended status incorrectly as part of an automated action. Railway says this happened around 22:20 UTC on May 19, and the platform was not fully recovered until the next morning. (https://blog.railway.com/p/incident-report-may-19-2026-gcp-account-outage)

That should make every CloudOps, platform, SRE, and engineering leader stop for a minute.

Because most redundancy plans are built for the wrong failure.

We design for dead VMs.
We design for unavailable zones.
We design for regional failover.
We design for database replicas.

But what do we do when the provider says, incorrectly or automatically, “your account is no longer allowed to exist normally”?

Not much, usually.

1. This was not a cloud outage. It was an account suspension

That is the first big lesson.

A lot of people hear “cloud outage” and instantly think of regions, zones, load balancers, or broken hardware. But Railway’s case was different.

Google Cloud’s automated systems suspended Railway’s production account. Railway says this was incorrect, and that the action was part of a wider automated event affecting many accounts. (https://blog.railway.com/p/incident-report-may-19-2026-gcp-account-outage)

That kind of failure does not look like a server going unhealthy.

It looks like identity, billing, trust, abuse detection, policy, support, and account control all becoming part of your availability story.

Your health checks can say everything is fine.

Your multi zone architecture can be green.

Your workloads can still technically exist.

But if the account is restricted, your beautiful infrastructure diagram does not matter much.

This is the part many teams do not model.

They model “what if eu west 1 is down?”

They rarely model “what if our production cloud account is frozen by an automated system at 11 PM?”

And honestly, that second one is scarier.

Because you do not debug it with kubectl.

You debug it with support tickets, escalation paths, account managers, legal trust, and luck.

2. The control plane was the real single point of failure

Railway had workloads on AWS and Railway Metal that were still running during the incident. But users still saw errors.

Why?

Because the routing control plane was hosted on Google Cloud.

Railway’s edge proxies needed that control plane to know where workloads lived. They had cached route data for a while, but once the cache expired, the edge could not keep routing properly. Railway’s community update said route cache expiry caused the incident to spread beyond GCP hosted workloads and affect the wider platform. (https://station.railway.com/community/what-we-know-so-far-may-19th-2026-86354cdd)

This is the second lesson.

Your data plane can be redundant while your control plane is still fragile.

And this is where a lot of “multi cloud” thinking becomes a little fake.

You can run compute in three places.
You can run storage in two places.
You can have Kubernetes clusters everywhere.

But if the scheduler, routing map, identity service, deployment API, config database, or certificate automation lives in one provider, your multi cloud story may only be multi cloud on paper.

The thing customers see as “the product” is often not the workload.

It is the control plane around the workload.

For Railway, customers were not just buying raw compute. They were buying routing, builds, deployments, dashboard access, APIs, orchestration and platform magic.

And the platform magic had a dependency.

That dependency became the outage.

3. Getting the account back is not the same as getting the service back

This one is very important.

According to Railway, Google reversed the suspension shortly after escalation. But recovery still took hours because account restoration did not automatically bring everything back cleanly. Persistent disks, compute instances, networking and orchestration layers had to be restored and verified step by step. (https://blog.railway.com/p/incident-report-may-19-2026-gcp-account-outage)

This is the part people underestimate.

A provider can say, “access restored.”

But your system still has to wake up.

Disks need to attach.
Networks need to behave.
Queues need to drain.
Deployments need to stop stampeding.
Databases need to agree again.
Caches need to be repopulated.
Humans need to verify what is safe.

That is not instant.

And in a complex platform, bringing things back too fast can be worse than bringing them back slowly.

Railway also throttled queued deploys during recovery, which sounds boring, but it is actually the responsible move. Because after an outage, your own backlog becomes traffic. And that traffic can flatten the recovering system.

So the real RTO is not:

“How fast can the provider undo the mistake?”

It is:

“How fast can we safely restore the whole chain after the provider undo the mistake?”

Small difference in words.

Huge difference in reality.

4. Recovery can create a second outage

This is probably my favorite lesson from the whole incident, because it is so real.

When Railway started recovering, queued retries and user activity came back in a burst. That burst hit GitHub OAuth and webhook flows hard enough that GitHub rate limited Railway. So logins and builds had problems again, even after the original Google Cloud issue was no longer the main blocker. (https://blog.railway.com/p/incident-report-may-19-2026-gcp-account-outage)

That is painful.

The first outage came from one provider.

The second problem appeared during recovery, from another dependency.

This happens more often than teams admit.

After an outage, everything tries to catch up.

Cron jobs wake up.
Webhooks retry.
CI pipelines restart.
Users refresh dashboards.
Workers pull old messages.
Integrations suddenly see a wall of traffic.

And then some other system says, “this looks abusive.”

Now your recovery has become its own incident.

This is why serious resilience is not just failover.

It is controlled recovery.

Backpressure matters.
Retry budgets matter.
Queue draining matters.
Circuit breakers matter.
Rate limit awareness matters.
Runbooks matter.

And boring old institutional memory matters even more.

Railway had already hardened parts of the GitHub rate limit path after a prior incident, which helped reduce damage this time. That is not luck. That is the value of learning properly from past pain.

5. Most teams insure the wrong half of the risk

The Railway incident is not the first time account level cloud risk became real.

In 2024, UniSuper, a major Australian pension fund, had a serious Google Cloud incident where its private cloud environment was deleted because of a misconfiguration. Google later published details saying backups in Google Cloud Storage and third party backup software helped restoration. (https://cloud.google.com/blog/products/infrastructure/details-of-google-cloud-gcve-incident)

So no, account level and provider control plane risk is not some imaginary edge case.

It happens.

But most companies still talk about redundancy like this:

“We use multiple clouds.”

Ok, but what does that mean?

Does it mean workloads can run somewhere else?

Or does it mean you can actually operate the business if one provider account disappears?

Those are very different things.

Flexera’s 2026 State of the Cloud report shows multi cloud is still a major enterprise pattern, and its report is based on 753 cloud decision makers. (https://info.flexera.com/CM-REPORT-State-of-the-Cloud?lead_source=Organic+Search) But in practice, many companies are multi cloud for procurement, politics, analytics, or workload placement.

Not always for true survivability.

True survivability asks much harder questions.

Can we deploy without this provider?
Can we route without this provider?
Can we authenticate without this provider?
Can we restore backups without this provider?
Can we contact support fast enough?
Can we prove ownership if an automated trust system flags us?
Can we keep serving read only traffic if the control plane dies?
Can we rebuild from another account, another org, or another provider?

That is not as sexy as “active active multi cloud.”

But it is probably more useful.

The real takeaway

Railway did have redundancy.

Just not for the layer that failed.

And that is the uncomfortable lesson for the rest of us.

Redundancy at the compute layer does not protect you from account suspension.

Multi region databases do not protect you from provider level identity actions.

Healthy servers do not help when routing control planes cannot tell traffic where to go.

And getting your cloud account back does not mean your service is back.

The next resilience review should not only ask:

“What happens if a region dies?”

It should also ask:

“What happens if our cloud provider suspends our production account by mistake tonight?”

I Inherited 47,000 Lines of Terraform Spaghetti — Here’s How I Untangled It Without Burning Production

sanjaysundarmurthy image

The Slack Message That Ruined My Monday

“Hey, the previous platform team left. Here’s the repo. Good luck 🫡”

I stared at the Git repository. 47,000 lines of Terraform. One state file. Zero modules. Variables named x, temp2, and my personal favorite — DO_NOT_TOUCH_ask_raj. Raj had left the company two years ago.

If you’ve been a Senior DevOps Engineer for more than a year, you’ve inherited something like this. Maybe not 47K lines, but you’ve opened a main.tf that made you question your career choices.

This isn’t a “Terraform best practices” article. Those are written by people who’ve never had to run terraform plan on a 3,000-resource state file at 2 AM while the VP of Engineering watches.

This is a survival guide.

Anti-Pattern #1: The Monolith State File (aka “The Single Point of Career Failure”)

What I Found

# main.tf — 8,400 lines
# "Managed" networking, compute, databases, DNS, IAM, monitoring,
# and somehow... a CloudFront distribution for a marketing site
# that was decommissioned in 2023.

resource "aws_vpc" "main" { ... }
resource "aws_instance" "api_server_1" { ... }
resource "aws_instance" "api_server_2" { ... }
# ... 200 more instances ...
resource "aws_rds_instance" "prod_db" { ... }
resource "aws_iam_role" "god_mode" { ... }  # yes, really

A single terraform apply touched everything. Networking, databases, compute, DNS — all entangled like Christmas lights in January. One typo in a security group rule? Congratulations, your plan just showed 847 resources to evaluate, and Terraform decided your RDS instance needs replacing.

The Real Danger

This isn’t just messy — it’s operationally catastrophic. Here’s what happens:

  • terraform plan takes 14 minutes. Developers stop running it.
  • State file locking means only one person can work at a time.
  • Blast radius of any mistake = the entire infrastructure.
  • New team members are terrified to touch anything (rightfully so).

How I Fixed It (Without Downtime)

Step 1: State Surgery with terraform state mv

# First, I mapped resource dependencies visually
terraform graph | dot -Tsvg > infra-dependency-map.svg

# Then, split by domain boundaries
terraform state mv 'aws_vpc.main' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[0]' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[1]' -state-out=networking/terraform.tfstate

Step 2: Introduce State Boundaries by Blast Radius

I split into five state files based on change frequency and blast radius:

Layer Contents Change Frequency Blast Radius
foundation VPC, Subnets, Route Tables Monthly Critical
security IAM, KMS, Security Groups Weekly Critical
data RDS, ElastiCache, S3 Rare Catastrophic
compute ECS/EKS, ASGs, ALBs Daily High
edge CloudFront, Route53, WAF Weekly Medium

Step 3: Wire Them Together with Remote State Data Sources

# In compute/main.tf
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "company-terraform-state"
    key    = "foundation/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_ecs_service" "api" {
  # Reference networking outputs safely
  network_configuration {
    subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
  }
}

Result: terraform plan went from 14 minutes to 45 seconds. Team velocity tripled. I stopped getting 2 AM pages about state locks.

Anti-Pattern #2: The Copy-Paste Empire (aka “Modules at Home”)

What I Found

environments/
├── dev/
│   └── main.tf      # 1,200 lines
├── staging/
│   └── main.tf      # 1,200 lines (95% identical to dev)
├── prod/
│   └── main.tf      # 1,200 lines (90% identical... with 47 "hotfixes")
└── dr/
    └── main.tf      # 1,200 lines (copied from prod 8 months ago, never updated)

Four copies of the same infrastructure with subtle drift. Staging had a security group rule that prod didn’t. DR was missing three services entirely. Nobody knew which differences were intentional.

Why This Kills Senior Engineers

You can’t diff your way out of this. The files have diverged in ways that are both intentional (prod has larger instances) and accidental (someone fixed a bug in dev but forgot to propagate it). You have no source of truth.

The Refactoring Strategy That Actually Works

Don’t try to unify everything at once. I learned this the hard way after a failed “big bang” refactor that took 3 sprints and broke staging for a week.

Instead, use the Strangler Fig pattern:

# modules/api-platform/main.tf
variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod", "dr"], var.environment)
    error_message = "Environment must be dev, staging, prod, or dr."
  }
}

variable "config" {
  type = object({
    instance_type    = string
    min_capacity     = number
    max_capacity     = number
    enable_waf       = bool
    multi_az         = bool
    backup_retention = number
  })
}

locals {
  # Environment-specific defaults that document WHY they differ
  env_config = {
    dev = {
      instance_type    = "t3.medium"
      min_capacity     = 1
      max_capacity     = 2
      enable_waf       = false
      multi_az         = false
      backup_retention = 1
    }
    prod = {
      instance_type    = "m5.xlarge"
      min_capacity     = 3
      max_capacity     = 20
      enable_waf       = true
      multi_az         = true
      backup_retention = 35
    }
  }
}

The key insight: Every environment difference should be documented in code as a conscious decision, not hidden in a 1,200-line file as an accidental divergence.

Anti-Pattern #3: The terraform apply -auto-approve YOLO Pipeline

What I Found in .gitlab-ci.yml

deploy_prod:
  stage: deploy
  script:
    - terraform init
    - terraform apply -auto-approve  # 🚨 WHAT
  only:
    - main

No plan artifact. No approval gate. No diff review. Push to main → infrastructure changes in production. The commit history told the horror story:

fix: revert the revert of the fix
fix: actually fix prod this time
fix: ok THIS one fixes it
revert: revert everything from today

What Senior Engineers Actually Need

# .github/workflows/terraform.yml
name: "Terraform"

on:
  pull_request:
    paths: ['infrastructure/**']
  push:
    branches: [main]
    paths: ['infrastructure/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Terraform Plan
        id: plan
        run: |
          terraform init
          terraform plan -no-color -out=tfplan 
            -detailed-exitcode 2>&1 | tee plan_output.txt
        continue-on-error: true

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('plan_output.txt', 'utf8');
            const truncated = plan.length > 60000 
              ? plan.substring(0, 60000) + 'nn... truncated ...' 
              : plan;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan Outputn```n${truncated}n````
            });

      - name: Upload Plan Artifact
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan

  apply:
    needs: plan
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production  # Requires manual approval
    steps:
      - uses: actions/checkout@v4

      - name: Download Plan
        uses: actions/download-artifact@v4
        with:
          name: tfplan

      - name: Terraform Apply
        run: terraform apply tfplan  # Apply ONLY the reviewed plan

The non-negotiable rules:

  1. Plans are generated on PR and attached as artifacts.
  2. Humans review the diff before any production apply.
  3. Apply uses the exact plan that was reviewed (not a new plan).
  4. The production environment requires manual approval from a senior engineer.

Anti-Pattern #4: Secrets in State (The Ticking Compliance Bomb)

What I Found

resource "aws_db_instance" "prod" {
  engine               = "postgres"
  instance_class       = "db.r5.2xlarge"
  username             = "admin"
  password             = "Pr0d_P@ssw0rd_2022!"  # I wish I was joking
  publicly_accessible  = true                    # I really wish I was joking
}

The password was in the .tf file, the state file, the plan output, and the Git history. Four places to leak from. And publicly_accessible = true was the cherry on this dumpster fire sundae.

The Fix (That Also Passes Audit)

# Use a data source to pull secrets at plan/apply time
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/rds/master-password"
}

resource "aws_db_instance" "prod" {
  engine              = "postgres"
  instance_class      = "db.r5.2xlarge"
  username            = "admin"
  password            = data.aws_secretsmanager_secret_version.db_password.secret_string
  publicly_accessible = false

  # Prevent Terraform from detecting password "drift"
  lifecycle {
    ignore_changes = [password]
  }
}

But that’s not enough. The state file still contains sensitive values. The complete solution:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/data/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true                          # SSE-KMS encryption
    kms_key_id     = "arn:aws:kms:us-east-1:xxx:key/yyy"
    dynamodb_table = "terraform-state-lock"
  }
}

Plus strict S3 bucket policies, access logging, and never giving developers direct state file access. Use terraform output instead.

Anti-Pattern #5: The “God Resource” With 200 Lines of Nested Blocks

What I Found

resource "aws_ecs_task_definition" "api" {
  family                   = "api"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 1024
  memory                   = 2048
  execution_role_arn       = aws_iam_role.ecs_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "api"
      image = "company/api:latest"  # 🚨 LATEST TAG IN PROD
      portMappings = [{ containerPort = 8080 }]
      environment = [
        { name = "DB_HOST", value = "prod-db.cluster-xxx.us-east-1.rds.amazonaws.com" },
        { name = "DB_NAME", value = "production" },
        { name = "REDIS_URL", value = "prod-redis.xxx.cache.amazonaws.com:6379" },
        # ... 45 more environment variables hardcoded here ...
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/api"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "api"
        }
      }
      # ... 80 more lines of health checks, mount points, ulimits ...
    }
  ])
}

The problems compound:

  • Environment variables are hardcoded (not sourced from SSM/Secrets Manager).
  • latest tag means deployments are non-reproducible.
  • The jsonencode blob is untestable and un-diffable in PR reviews.
  • One change to any env var triggers a full task definition replacement.

The Refactored Version

# Use templatefile for complex JSON — it's testable and readable
resource "aws_ecs_task_definition" "api" {
  family                   = "api-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.task_cpu
  memory                   = var.task_memory
  execution_role_arn       = aws_iam_role.ecs_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = templatefile("${path.module}/templates/api-container.json.tpl", {
    image_tag     = var.image_tag  # Pinned, passed from CI/CD
    environment   = var.environment
    db_host       = data.aws_ssm_parameter.db_host.value
    redis_url     = data.aws_ssm_parameter.redis_url.value
    log_group     = aws_cloudwatch_log_group.api.name
    aws_region    = data.aws_region.current.name
  })
}

The Refactoring Playbook (Do This Monday)

After untangling this mess across three months, here’s the sequence that works:

Week 1: Triage and Protect

# 1. Enable state file encryption and locking NOW
# 2. Add branch protection — no direct pushes to main
# 3. Run terraform plan and SAVE the output as your baseline
terraform plan -no-color > baseline_plan_$(date +%Y%m%d).txt

# 4. Enable detailed audit logging on your state bucket

Week 2-4: Split the Monolith

# Use terraform state list to inventory everything
terraform state list > all_resources.txt
wc -l all_resources.txt  # Mine had 2,847 resources

# Group by service domain
grep "aws_vpc|aws_subnet|aws_route" all_resources.txt > networking.txt
grep "aws_iam|aws_kms" all_resources.txt > security.txt
grep "aws_rds|aws_elasticache|aws_s3" all_resources.txt > data.txt
grep "aws_ecs|aws_alb|aws_autoscaling" all_resources.txt > compute.txt

Week 5-8: Modularize Incrementally

Move one service at a time into a module. After each move:

  1. Run terraform plan — it should show zero changes.
  2. If plan shows changes, you have a bug. Fix it before moving on.
  3. Get a PR review from another senior engineer.
  4. Apply and monitor for 24 hours.

Week 9-12: Harden the Pipeline

  • Add terraform validate and tflint to CI.
  • Add checkov or tfsec for security scanning.
  • Implement drift detection (scheduled plan that alerts on differences).
  • Add cost estimation with infracost.

The Drift Detection Cron That Saved Us

This is the thing nobody talks about. Even after a perfect refactor, drift happens. Someone clicks in the console. An auto-remediation tool makes changes. A Lambda modifies a security group.

# .github/workflows/drift-detection.yml
name: "Drift Detection"

on:
  schedule:
    - cron: '0 6 * * 1-5'  # Every weekday at 6 AM

jobs:
  detect-drift:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        layer: [foundation, security, data, compute, edge]
    steps:
      - uses: actions/checkout@v4

      - name: Terraform Plan (Drift Check)
        id: plan
        working-directory: infrastructure/${{ matrix.layer }}
        run: |
          terraform init
          terraform plan -detailed-exitcode -no-color > plan.txt 2>&1
          echo "exitcode=$?" >> $GITHUB_OUTPUT
        continue-on-error: true

      - name: Alert on Drift
        if: steps.plan.outputs.exitcode == '2'
        run: |
          # Exit code 2 = changes detected (drift!)
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" 
            -H 'Content-type: application/json' 
            -d "{"text":"🚨 Drift detected in *${{ matrix.layer }}* layer. Check the plan output."}"

We caught 3 unauthorized console changes in the first week alone.

Parting Wisdom for the Senior Engineer Who Just Inherited a Mess

  1. Don’t refactor everything at once. You’ll break things and lose credibility.

  2. Document what you find before you fix it. Screenshot the horrors. You’ll need them for the post-mortem and for your performance review.

  3. Get buy-in from leadership BEFORE you start. “I need 3 sprints for tech debt” is a hard sell. “Our current setup means any infrastructure change has a 40% chance of causing an incident” gets budget approved.

  4. Every terraform state mv should be a separate, reviewed PR. Not because it’s technically necessary, but because when something breaks at step 37 of 50, you want a clean git history to bisect.

  5. The goal isn’t perfect Terraform. The goal is Terraform that your team can safely operate at 2 AM. If a junior engineer can’t run terraform plan without fear, your refactor isn’t done.

TL;DR for the Scrollers

Anti-Pattern Fix Priority
Monolith state file Split by blast radius and change frequency P0
Copy-paste environments Modules + environment configs P1
-auto-approve in CI Plan artifacts + manual approval gates P0
Secrets in state/code Secrets Manager + encrypted state + ignore_changes P0
God resources with inline JSON templatefile + SSM parameters P2
No drift detection Scheduled plan with alerting P1

If you’ve ever stared at a Terraform codebase and whispered “who did this?!” into the void — you’re not alone. We’ve all been there. The good news? It’s fixable. One state move at a time.

Found this useful? Follow me for more battle-tested DevOps content. I write about the stuff that actually happens in production — not the happy path from the docs.

How I Turned My Terminal Into an Aesthetic Experience

I’ve always been addicted to staring at a screen all day, writing — whether it’s code or a story. But this passion comes with a cost, at least for me. I struggle with eye strain and painful migraines, and it genuinely hurts to use these devices, no matter how many “protective layers” I add. By that, I mean things like enabling dark mode, blocking blue light from screens, and wearing anti-reflective glasses. They help, sure — but not enough. A fraction of the discomfort always remains.

And it gets even worse WHEN WORKING IN THE TERMINAL.

All that plain white output gets me dizzy. It forces me to take frequent breaks throughout the day. I just can’t stand looking at walls of unstyled, uncolored text. Ironically, it ends up being distracting.

Of course, I’ve tried to fix this. I added an Oh My Zsh syntax-highlighting plugin, switched to most as a pager for man pages, and installed tools like bat, eza, and grc.

And yes — it helped.

My terminal looks better now, and spending time in it has become easier.

But still… some commands remain stubbornly plain.

Maybe this sounds like overreacting — and maybe you’re right — but I just want a fully colored terminal experience. You can’t blame me for that, right?

That’s why I built this function, colored-help, which automatically colorizes help output to improve readability via bat. At first, I implemented it with a Zsh preexec hook, but I ran into strange issues: commands wouldn’t exit cleanly with q or Ctrl+C, and — help outputs would linger on the screen. So, I turned to my beloved ChatGPT, which introduced me to ZLE widgets — and that changed everything.

Terms explained

bat — A modern replacement for cat with syntax highlighting, Git integration, and prettier output formatting.

eza — A modern alternative to ls with colors, icons, tree views, and improved file metadata display.

most — A terminal pager similar to less, but with syntax highlighting and support for multiple windows.

grc — A tool that adds color and formatting to the output of many common command-line programs.

ZLE (Zsh Line Editor) — The interactive command-line editor built into Zsh. It allows you to create custom widgets, keybindings, and shell behaviors.

preexec hook — A Zsh hook function that runs automatically right before a command executes.

Why am I sharing this?

Because I’d really appreciate your feedback. What do you think of my approach? Is there a better way to implement this? I’m also thinking of extending it. Are there other tools or “10x hacks” I might be missing?

Please don’t hesitate to share your thoughts — I’d genuinely love to hear them.

Synchronous Functions in Dart

This series of post will try to explain a complex topic: concurrent and parallel programming, in Dart. I think the only way to deal with that is using the Erlang VM (BEAM), but Clojure and other functional languages are usually doing better job on this part. Unfortunately, to me, most of other languages using OOP don’t offer a great abstraction to concurrency and parallelism, but during the last decade, things are changing a bit.

Anyway, before talking about concurrency, parallelism, or even distributed systems, everything starts with synchronous call, the “standard” way to execute a function or a procedure on a computer. In dart, synchronous function call and generators functions need to me studied a little bit.

Firstly, all function without any specific keyword are all defined as synchronous:

int addition(int x, int y) {
  return x + y;
}

If the keyword sync was present in the language, the previous snippet would be equivalent to the one below. Don’t try to compile it, it will fail.

// could be equivalent to the following
// snippet with the sync keyword. But this
// function will not work, sync keyword is
// invalid during compilation
int addition_sync(int x, int y) sync {
  return x + y;
}

When adding the addition() function inside the main() entry-point, it will directly return the value. In fact, you can’t really see this because it’s quick, but the computer will block a little bit to wait the result of the computation.

int main() {
  return addition(1, 2);
}

A synchronous function will always wait for any kind of return before continuing the execution of the program. For example, if an infinite loop is created, the function will never return, and the function will block the whole program. The following snippet, when executed, will never return because of the while loop defined in the infinite_loop() function. The only way to close this program is to kill it (for example using Ctrl-C or sending another signal on UNIX systems).

int infinite_loop() {
  while (true) {
    // do something
  }
  return 0;
}

int main() {
  return infinite_loop();
}

Great, we know how to create synchronous functions. Now, let have a look on a specific keyword: sync*. This one is a syntactic sugar to help developers creating generators, a function that return many elements, one at a time. The best example is the List class (or []). This data type – a list – is seen as a generator, and more precisely an Iterable.

A function is a generator if its body is marked with the sync* or async* modifier. […] Generator functions are a sugar for functions that produce collections in a systematic way, by lazily applying a function that generates individual elements of a collection. Dart provides such a sugar in both the synchronous case, where one returns an iterable, and in the asynchronous case, where one returns a stream.

— Dart Language Specification, Chapter 9, page 23

Let create our first generator from scratch, let call it int_generator(). It will take 2 integers as arguments and will return the list of elements between them. You can see it as a kind of primitive range or sequence function.

Iterable<int> int_generator(int x, int y) sync* {
  if (y<x) throw "y<x"; 
  while (x <= y) {
    yield x;
    x++;
  }
}

int main() {
  for (var i in int_generator(0, 10)) {
    print(i);
  }
  return 0;
}

In the first place, instead of returning a simple int this function is returning an Iterable<int>. Why? Because we are using yield, but we will explain that a bit later. Next, the function definition looks like a classic one, but the sync* keyword is added just after. It means this function will be a generator.

Then, the body of the function where a first step is to avoid creating an infinite loop by creating a simple guard throwing an exception if the second argument is less than the first argument. Immediately after, a while loop is created, where the first argument is incremented until the first argument is less or equal to the second one.

The yield statement adds an object to the result of a generator function.

— Dart Language Specification, Chapter 18, Section 16, page 213

In this loop, the keyword yield followed by variable (or any valid terms) is used. This specific expression is important, because it will adds the object into the result of the generator. In other words, if another function is calling the previous function inside a loop, the sequence of integer will be printed to the screen.

Let execute that from the console.

$ dart run bin/int_generator.dart
0
1
2
3
4
5
6
7
8
9
10

yield is not the only unique term here able to put an object inside a generator, yield* (yield-each) can also be used to push a series of objects, in fact a subtype of Iterable class.

The yield-each statement adds a series of objects to the result of a generator function.

— Dart Language Specification, Chapter 18, Section 17, page 214

Iterable<int> int_generator(int x, int y) sync* {
  if (y<x) throw "y<x"; 
  yield* [x,y];
}

int main() {
  print(int_generator(0, 10));
  for (var i in int_generator(0, 10)) {
    print(i);
  }
  return 0;
}

Let execute this code.

$ dart run bin/int_each_generator.dart 
(0, 10)
0
10

To be honest, I don’t currently understand why it returns a record on the first part of the program. I was reading the specification but I was unable to find and answer. I will let this problem for future-me. A quick note though: a generator cannot return a value.

Case ⟨Generator functions⟩. It is a compile-time error if a return statement of the form return e; appears in a generator function.

— Dart Language Specification, Chapter 18, Section 12, page 211

Based on the documentation, it also seems yield* can be used to improve recursive function performance. Here the one of the previous example implemented using recursive function call.

Iterable<int> int_rec_generator(int x, int y) sync* {
  if (x<=y) {
    yield x;
    yield* int_rec_generator(x+1, y);
  }
}

int main() {
  print(int_rec_generator(0, 10));
  return 0;
}

Let run it now.

$ dart run bin/int_recursive_generator.dart 
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Same result than before, but it was using a recursive function. If I remember correctly, calling recursively a function in Dart with tail queue optimization and trampoline is a very bad idea (it will consume a lot of stack and memory). Anyway, let switch to another topic: Iterable class.

Iterable class

We know how to create generators, and then Iterable from scratch, but what we can do with this kind of objects? Let read the definition from the API documentation.

A collection of values, or “elements”, that can be accessed sequentially.

— Dart Language Documentation, Iterable class

Not all features will be illustrated there, only the one looking interesting and useful for my own use cases.

  • Iterate.generate() constructor creates a new Iterable object using Integer by default (like a Range/Sequence). The second argument offers a way to change the type returned based on the integer sequence produced (a bit like a map()).
int main() {
  var x = Iterable.generate(10);
  print("generate(10): $x");

  var y = Iterable.generate(26, (int x) => String.fromCharCode(x+65));
  print("generate(10, _): $y");

  return 0;
}
$ dart run bin/t.dart 
generate(10): (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
generate(10, _): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z)
  • Iterable.contains() method returns true if the object passed in the first argument is present in the collection of objects managed by the Iterable.
$ cat bin/contains.dart 
void main() {
  var a = Iterable.generate(26, (int x) => String.fromCharCode(x+65));
  print('"A"? ${a.contains("A")}');
  print('65? ${a.contains(65)}');
}
$ dart bin/contains.dart 
"A"? true
65? false
  • Iterable.elementAt() method returns the element present at the position defined by the first argument. If the position does not exist, it throws a RangeError exception.
$ cat bin/elementAt.dart 
void main() {
  var x = Iterable.generate(10);
  print(x.elementAt(0));
  print(x.elementAt(9));
  print(x.elementAt(1024));
}
$ dart bin/elementAt.dart 
0
9
Unhandled exception:
RangeError (index): Index out of range: index should be less than 10: 1024
#0      IndexError.check (dart:core/errors.dart:520:7)
#1      _GeneratorIterable.elementAt (dart:core/iterable.dart:928:16)
#2      main (file:///dart/t/bin/elementAt.dart:5:11)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)
  • Iterable.every() method returns true if the collection of items satisfies the anonymous function passed in argument.
$ cat bin/every.dart 
void main() {
  var x = Iterable.generate(10);
  print(x.every((y) => y >= 0 ));
  print(x.every((y) => y == String ));
}
$ dart bin/every.dart 
true
false
  • Iterable.fold() method reduces the collection of items using a combine function present in second argument and an initial value defined in the first argument.
void main() {
  Iterable<int> i = Iterable.generate(10);
  print(i.fold<int>(1, (v, e) => v + e));

  var s = Iterable.generate(26, (int x) => String.fromCharCode(x+65));
  print(s.fold<String>("string: ", (v, e) => "${v} ${e}"));
}
$ dart bin/fold.dart 
46
string:  A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  • Iterable.forEach() method iterates of the collection of item. This method returns nothing.
void main() {
  var list = [1,2,3,4, "end"];
  list.forEach((x) => print(x));
}
$ dart bin/forEach.dart 
1
2
3
4
end
  • Iterable.join() method joins the collection of item with a String and converts all object in the collection t String.
void main() {
  var list = ["start", 1, 2, 3, "end"];
  print(list.join(", "));
}
$ dart bin/join.dart 
start, 1, 2, 3, end
  • Iterable.map() method modifies each element of the collection by calling a function defined in the first argument.
void main() {
  var m = [
    {'a': 1},
    {'b': 2}
  ];
  print(
    m.map(
      (x) => {x.values.first: x.keys.first}
    )
  );
}
$ dart bin/map.dart 
({1: a}, {2: b})
  •  Iterable.reduce() methods is similar to Iterable.fold() excepts it does not have an initial value defined.
void main() {
  Iterable<int> list = Iterable.generate(20);
  print(list.reduce((a, e) {
    print("$a $e");
    if (e%2==0) {
      return a + e;
    }
    else {
      return a;
    }
  }));
}
$ dart bin/reduce.dart 
0 1
0 2
2 3
2 4
6 5
6 6
12 7
12 8
20 9
20 10
30 11
30 12
42 13
42 14
56 15
56 16
72 17
72 18
90 19
90

Way more methods are available, but it was not expected to go so deep in this post. If you want to go even deeper, you will need to read the test cases and the Dart implementation:

With these 2 links, you will have more than enough to understand how things are working.

Cover Image by Will H McMahan on Unsplash