How to kill and resurrect an API

Last night all hell broke loose. Partners could not see their dashboards, our people could not share the dashboards, the dashboards don’t show up etc etc. They are in the US, I’m in Estonia. I was prepping to go to bed.

Something had killed our API that uses MongoDB cluster with Beanie ODM. It worked but it didn’t. It was alive but it was dead. It acted completely strange. I started looking for the error. For background and context, our stack in this case:

  • Python 3.12, mostly
  • Beanie ODM
  • MongoDB 7
  • AWS Elasticbeanstalk (means the app runs in Docker)
  • AWS CodePipeline for build and deployment

There it was. Good, clean, full of meaning and hints:


line 26, in merge_models\n for k, right_value in right.__iter__():\n ^^^^^^^^^^^^^^\nAttributeError: 'NoneType' object has no attribute '__iter__'. Did you mean: '__str__'?", "taskName": "Task-7822"}


First I tried to reproduce it in the developent env. Copied all the fresh data from production MongoDB cluster etc etc. Everything works.

Then I tried to reproduce it in our staging env that runs on the same MongoDB cluster but on a different database. It has fresh data and everything. It all works …

I started thinking that when the code is 100% the same in all environments then it must be the data. Something must be wrong with the data in the database, right? It makes sense, it’s logical.
But how if I copied it from production and it all worked in dev and staging? HOW???

I spent a few good hours changing completely pointless things in code like changing type hints from “dict” to “Dict” because why not. It’s 1 a.m and I’m just changing anything I can in the code because perhaps it would fix it.

Around 2 a.m I started becoming desperate and poured me another glass of bourbon.

I was too lazy to set up remote debugging between my PyCharm and AWS env and TBH it’s a pain in the ass to do. I dug around in my code and in Beanie’s. Then, going line by line in Beanie I discovered something – the entity that I’m saving at one point is being read back and it comes back as None. Nada. Null. Nilch. Anti-matter. And then it came to me! During my day, about 8-10 hours earlier I had changed MongoDB connection string, of course “for the better” and “for performance reasons”. I added 2 parameters there:

  • w=0
  • journal=false

In case of MongoDB cluster w=0 means the writes won’t wait for confirmation from ANY replica set members. It means the code can move on fast and let the database deal with writing the data whenever it has time for it. What can go wrong here?

Yes – everything can go wrong here. The point is in those edge cases when the code needs to read the same freshly written entity from the same database very fast. It’s not there, yet. Why? Because the same code didn’t bother to wait for the entity to be properly written. I changed the connection string back, did all kinds of test and it all worked well. I had ressurrected our API.

It was 2:30 a.m. I went to bed and couldn’t get sleep because a new product was occupying my brain – a product that would log all my work-related decisions automatically and then search for it and analyze if something I did earlier today may be the reason of the problems I have now. Anyone interested in such a product? Oh, I guess the MVP is there already – it’s my brain that DOESN’T WORK!

How to get project root / git root from within the project

This simple trick allows to retrieve GIT_ROOT or PROJECT_ROOT from anywhere within the project folder structure. Whenever it’s needed, just use this:

export GIT_ROOT=$(realpath $(git rev-parse --git-dir)/..)
cd $GIT_ROOT;

Fast async Python: Using aiofiles and aiocsv to parse large CSV files

# This file is a demo of using aiocsv and aiofiles libraries to speed up reading and parsing CSV files.
#
# Start reading this code from the entrypoint function main() below.
#
import asyncio
import aiofiles
from csv import QUOTE_NONNUMERIC
from typing import AsyncGenerator
from aiocsv import AsyncDictWriter, AsyncDictReader


async def read_lines(file: str) -> AsyncGenerator[dict, None]:
    """
    Read lines from CSV file.
    """
    async with aiofiles.open(file, "r") as afp:
        async for row in AsyncDictReader(afp, delimiter=","):
            yield row


async def parse_lines(generator: AsyncGenerator[dict, None]) -> AsyncGenerator[dict, None]:
    """
    Parse lines from generator.
    """
    async for line in generator:
        # do some parsing here, like that:
        line = line
        yield line


async def save_lines(file: str, generator: AsyncGenerator[dict, None]):
    """
    Save lines from generator to CSV file.
    """
    async with aiofiles.open(
            file,
            mode="w",
            encoding="utf-8",
            newline="",
    ) as afp:
        rows = []
        writer = None
        async for item in generator:
            if writer is None:
                header = list(item.keys())
                writer = AsyncDictWriter(
                    afp,
                    header,
                    quoting=QUOTE_NONNUMERIC,
                )
                await writer.writeheader()
            # gather rows into a list
            # keep the list size reasonable according to your memory constraints
            rows.append(item)
            if len(rows) % 10000 == 0:
                await writer.writerows(rows)
                rows = []
            await afp.flush()
        # write the rest of the rows if any
        if len(rows) > 0:
            await writer.writerows(rows)


async def main(in_file, out_file):
    """
    Main function that reads lines from in_file, parses them and saves to out_file.
    """
    raw_line_generator = read_lines(in_file)
    parsed_line_generator = parse_lines(generator=raw_line_generator)
    await save_lines(file=out_file, generator=parsed_line_generator)


in_file = "some_input_file.csv"
out_file = "some_output_file.csv"
asyncio.run(main(in_file, out_file))

Removal of postimees.ee comments in Firefox

Foreword about addiction

I’m an addict and I can’t help myself. At least not in an easy way. I’m addicted to reading idiotic, moronic, hateful, homophobic comments posted by postimees.ee readers to Postimees website. FYI – Postimees is one of the oldest and biggest newspapers in Estonia.

So that’s why I was looking for a solution on how to remove comments or at least these deceptive links to comments by every article on postimees.ee website.

It only works in Firefox because that’s my main browser.

Let’s get hands dirty

First, open about:config (type it into address field). Firefox config opens after a warning.
Find the key

toolkit.legacyUserProfileCustomizations.stylesheets

Make sure the value is “true” (double click on the value). Close config.
Now open config of profiles.

Type about:profiles to address field.
Find your profile (not the development one) and there should be “Root Directory”. At the end of this line is button “Open in Finder” (or open I-dont-know-where in Windows). Whatever, just click it.

Your Firefox profile folder opens.

Inside that folder create a new folder named “chrome” (mind the lowercase name, case matters!).

Inside that “chrome” folder create an empty text file called “userContent.css”. Again – mind the naming.
Into that file add following lines:

@-moz-document domain(postimees.ee) {
    span.list-article__comment {
        display: none;
    }
}

Save and close the file. Restart Firefox. Go to postimees.ee. Welcome to your new life!

Poor man’s VPN using SSH and SOCKS proxy for MacOS

Add the following aliases to your .bash_profile:

alias socks_on="ssh -D 8666 -C -N -f -M -S ~/.socks.socket $USER@<your_office_gateway>; networksetup -setsocksfirewallproxystate Wi-Fi on;"
alias socks_off="networksetup -setsocksfirewallproxystate Wi-Fi off; ssh -S ~/.socks.socket -O exit $USER@<your_office_gateway>;"

Later you can start your tunnel with command

socks_on

and stop it with

socks_off

 

😉

ssh-copy-id key to other user than yourself?

There’s a good tool for copying ssh keys to remote host under your account: ssh-copy-id. This lets you copy your public key under your account on the remote server.

But what about other accounts? Let’s say you want to log in as root (with key-only auth method, of course)? How to copy key to root user’s .ssh/authorised_keys? One way to do it is to log as your ordinary user, make yourself root with sudo su -, open authorized_keys with editor, paste, save etc… Tedious? Yes.

That’s why there’s a good oneliner:

 

cat ~/.ssh/id_rsa.pub | ssh your_user@remote.server.com “sudo tee -a /root/.ssh/authorized_keys”

 

 

 

SailsJS and Waterline: native MongoDB queries and Waterline models

Here’s my experience with SailsJS, Waterline and MongoDB native queries. I like SailsJS and Waterline very much but there’s also room for improvement when things get serious.

There’s limitation in current Waterline that one cannot limit the fields in the output when MongoDB is used. Also the aggregation options are limited with Waterline. MongoDB on the other hand is very-very powerful database engine and once you learn how to aggregate then the possibilities seem endless.

My usecase is that I have to use native queries instead of Waterline’s but I also want the retrieved models have all those nice “instance methods” of Waterline model instances like “model.save()”. This example also gives you overview how to use native queries, aggregation.

So here’s very short guide to this. I hope it helps to save a couple of hours for other guys like me (who spent that time to figure it out:)).

Note! It uses another excellent, wonderful, genius etc pattern called Promises.

Custom headers from SailsJS API ignored by AngularJS app

Have you ever tried to return custom HTTP headers from your SailsJS backend REST API to your frontend AngularJS application and wondered why they don’t show up in AngularJS?

I had pretty standard case where I wanted to implement server side pagination for my data sets returned by the API. For that you need to return the total number of records in order to implement pagination properly in the frontend. I decided to return the total number of records in a custom header called “X-TotalRecords”. It is returned together with the response but it didn’t show up in AngularJS response:

.....    
.then(function(response){
    $log.debug(response.headers()) //does not show my custom header
}) 
..... 

After some googling around I found a solution. You need to create a custom SailsJS policy and send a special header “Access-Control-Expose-Headers” there. Let’s call the policy sendCorsHeaders.

Create a file sendCorsHeaders.js in policies/ folder:

    
module.exports = function (req, res, next) {
    res.header('Access-Control-Expose-Headers', sails.config.cors.headers);
    next();
};

As you can see it re-uses headers defined in your cors.js under config/ folder.

From now on you can retrieve your custom header in AngularJS $http service.

Accepting BDOC container upload from PUT method in SailsJS app

I just struggled with a complex problem of uploading application/bdoc (digital signature container) files to a SailsJS app and I want to share my story. I hope it will make the life easier for those who are working with digidoc and Signwise.

We at Prototypely are creating a solution that heavily uses digital signatures. Signwise is the preferred partner for handling containers and signing process. Signwise process states that they create the container and their system makes a HTTP PUT request to target system to put the newly created container back.

Standard file uploads are handled very nicely in SailsJS by great Skipper library.

However when it comes to uploading quite rare mime types like application/bdoc or application/x-bdoc then it needs some tweaking.

Open config/http.js and add custom body parser there and you’ll be able to accept BDOC files:

bodyParser: function (options) {
  return function (req, res, next) {
    if (req.get('content-type') != 'application/bdoc') {
      return next();
    }
    var bodyParser = require('body-parser').raw({type: 'application/bdoc'});
    return bodyParser(req, res, next);
  }
}

After that you’ll be able to save the file in your controller. Mind the req.body – this is the buffer that will be written down.

acceptBdocFile: function(req, res){
    var fileId = req.param('fileId');
    var tmpFile = process.cwd() + '/.tmp/' + fileId;
    fs.writeFileSync(tmpFile, req.body);
    return res.status(201).json();
} 

How to delete Magento maintenance.flag without FTP?

Sometimes Magento gets stuck in “Maintenance mode”. It means that there is maintenance.flag file in Magento’s root folder.
The standard maintenance mode of Magento is a bit “too universal” – it sets Magento backend (admin) to maintenance mode also. Once you’re in maintenance mode, it’s hard to get out of this if you don’t have access server’s shell.
Anyway – there is one option if you have not removed Magento Connect Manager (a.k.a /downloader). This program is be impacted by the maintenance.flag file. Log in to Connect Manager at /downloader and check/uncheck checkbox ““.

That’s it.