Girl Coding Image by brgfx on Freepik

Inotify in ASAB Library

🇨🇿 Česká verze je níže / Czech version is below.

At TeskaLabs, we develop an open-source framework, ASAB, that helps us make writing microservices easy as breathing. One of its components allows the microservices to read from something we call a Library. It is rather a concept than a codebase. The Library contains common files dynamically read by a microservice. Thus, all microservices in a product can share the same content.

The subscribe functionality of ASAB Library enables microservices to be notified of any new changes. Library items live among a few destinations like a local file system, remote git repository, Azure, or ZooKeeper. In this blog post, I would like to share what I've learned when implementing subscribe feature for files on the file system.

Inotify

Inotify is a Linux kernel subsystem that allows applications to monitor filesystem events such as file modifications, creations, and deletions in real time. I started by looking for Python libraries implementing inotify. Asyncinotify and inotify-simple caught my eye, but they were excessive for my purpose and seemed a bit clumsy to me. Since ASAB is a framework our products rely on, we are cautious about introducing new dependencies. I had to connect with inotify myself.

Creating a Custom Inotify Implementation

Naturally, I started robbing the libraries mentioned above but soon got into trouble. Inotify uses file descriptors to propagate notifications. The application receives a unique file descriptor to monitor specific files or directories. When an event occurs, the kernel writes an event notification to the file descriptor. The application then reads this data to take appropriate action.

All I needed was to open the file descriptor and wait for an event. Unfortunately, this is a blocking operation being introduced into the fully asynchronous world of ASAB. After consulting Chat GPT for advice, I received a well-designed code suggestion.

async def inotify_events():
    # initialize inotify
    fd = inotify_init()
    # add watch for directory
    watch_descriptor = inotify_add_watch(
        fd,
        b'/path/to/directory',
        IN_ALL_EVENTS
    )
    # create async file descriptor
    reader = asyncio.open_file(fd, mode='rb')
    while True:
        await reader.read()
        buffer = os.read(fd, 4096)
        for event in buffer:
            print(event)

Amazing! Isn't it? I was amazed. It was perfect. Except asyncio.open_file() method doesn't exist.

Understanding Ctypes and Masks

Playing with inotify, I met ctypes library for the first time, interacting with the C library. Exciting. I stole most of it from inotify-simple, though. I also learned about bitmasks, which specify the event types that should be monitored. At first, I thought those were just some flags, but I was amazed when bitmask operations were shown to me. Being a biologist in IT brings me many "light bulb moments" when I learn something that is probably taught in the first year of Computer Science studies. For example, IN_CREATE mask is represented by 00000100 byte. An event talking about new file creation should carry this mask. Using AND operation with these masks, I could tell whether a file was created.

Overcoming the Blocking Read Challenge

I kept trying to read from the file descriptor - maybe I could check the file descriptor from time to time or try a second thread. It was ugly, and I was desperate. Fortunately, asyncio library in Python provides a miraculous tool to work with the file descriptors. The asyncio loop, the backbone of the ASAB Application can operate on file descriptors asynchronously. This allowed me to effectively integrate inotify into the ASAB framework. (And gave me the lesson again: Read the documentation.)

Happy Ending

My inotify implementation might not cover all of inotify's functionality, but it is sufficient for our use case and lightweight, which is very important when contributing to a framework codebase. By overcoming the blocking read challenge and learning about ctypes and bitmasks, I created a lean and efficient solution that enables the ASAB framework to react to changes in the Library in real time.

🇨🇿 Inotify a ASAB

Ve společnosti TeskaLabs vyvíjíme open-source framework ASAB, který nám pomáhá psát mikroslužby. Jedna z jeho součástí umožňuje mikroslužbám číst ze souborů, kterým dohromady říkáme knihovna. Knihovna obsahuje běžné soubory, které mikroslužba dynamicky čte. Všechny mikroslužby v produktu tak mohou sdílet stejný obsah.

ASAB umožňuje, aby byly mikroslužby informovány o všech nových změnách. Položky knihovny žijí na několika místech - místní souborový systém, vzdálený repozitář v git, Azure nebo ZooKeeper. V tomto blogovém příspěvku bych se chtěla podělit o to, co jsem se naučila při detekci změn v souborech na lokálním disku.

Inotify

Inotify je subsystém linuxového jádra, který umožňuje aplikacím sledovat události v systému souborů, jako jsou změny, vytváření a mazání souborů v reálném čase. Začala jsem hledáním knihoven Pythonu implementujících inotify. Do oka mi padly Asyncinotify a inotify-simple, ale pro můj účel byly zbytečně složité a připadaly mi trochu neohrabané. Vzhledem k tomu, že ASAB je framework, na který se naše produkty spoléhají, jsme při zavádění nových závislostí opatrní. Nezbylo tedy než se spojit s inotify přímo.

Vytvoření vlastní implementace Inotify

Samozřejmě jsem začala vykrádáním výše zmíněných knihoven, ale brzy jsem se dostal do problémů. Inotify používá k šíření oznámení dsile descriptors. Aplikace dostává jedinečný "deskriptor", který monitoruje konkrétní soubory nebo adresáře. Když dojde k události, jádro zapíše do tohoto deskriptoru oznámení o události. Aplikace pak tato data přečte a provede příslušnou akci.

Stačilo otevřít přidělený deskriptor a počkat na událost. Jenže se jedná o blokující operaci, která se vůbec nehodí do plně asynchronního světa ASABu. Teprve jsem se seznamovala s Chat GPT a jeho rady mi připadaly zázračné. Dostala jsem z něj následující návrh kódu:

async def inotify_events():
    # initialize inotify
    fd = inotify_init()
    # add watch for directory
    watch_descriptor = inotify_add_watch(
        fd,
        b'/path/to/directory',
        IN_ALL_EVENTS
    )
    # create async file descriptor
    reader = asyncio.open_file(fd, mode='rb')
    while True:
        await reader.read()
        buffer = os.read(fd, 4096)
        for event in buffer:
            print(event)

Úžasné! Že ano? Byla jsem ohromena. Bylo to dokonalé. Až na to, že metoda asyncio.open_file() neexistuje.

Porozumění typům ze světa C a maskám

Při hraní s inotify jsem se poprvé setkala s pythoní knihovnou ctypes, která komunikuje se základní knihovnou jazyka C. Vzrušující. (Většinu kódu jsem si ale půjčila z inotify-simple.) Dozvěděl jsem se také o maskách, které určují typy událostí, jež mají být sledovány. Nejdřív jsem si myslel, že to jsou jen nějaké příznaky, kódy, ale pak jsem byla poučena, že s těmito maskami mohu rpovádět binární operace. Být bioložkou v IT mi přináší mnoho momentů náhlého prozření, kdy se dozvím něco, co se pravděpodobně učí v prvním ročníku studia informatiky. Například masku IN_CREATE reprezentuje bajt 00000100. Událost hovořící o vytvoření nového souboru by měla nést tuto masku. Pomocí operace AND s těmito maskami mohu zjistit, zda byl vytvořen nový soubor.

Zkrocení blokujícího čtení deskriptoru

Stále jsem se snažila číst z deskriptoru - možná bych ho mohla čas od času zkontrolovat nebo zkusit druhé vlákno? Kód nevypadal nijak hezky a já byla nešťastná. Naštěstí knihovna asyncio poskytuje zázračný nástroj pro práci s file descriptors. Vnitřní asnychronní smyčka, páteř aplikace ASAB, dokáže pracovat s deskriptory asynchronně. To mi umožnilo efektivně integrovat inotify do frameworku ASAB. (A opět mě to naučilo číst dokumentaci raději dřív než se pustím do zoufalého kódování.)

Happy Ending

Moje implementace inotify možná nepokrývá všechny mořné funkce, ale pro náš případ je dostačující, jednoduchá a snadno udržitelná, což je při psaní frameworku velmi důležité. Poprala jsem se s blokující operací, objevila jsem ctypes a masky a při tom jsem vytvořila efektivní řešení, které umožňuje frameworku ASAB reagovat na změny v knihovně v reálném čase.

About the Author

Eliška Novotná

Junior backend developer at TeskaLabs. Python and unicorns lover.

You Might Be Interested in Reading These Articles

Entangled ways of product development in the area of cybersecurity #3 - LogMan.io

At that time I lived in Prague for a short time, which is not a very friendly place to live, but it allowed me to go to the office almost every day. A bigger surprise awaited Vlaďka and Aleš when I told them that I was going to move to a house almost eighty kilometres from the office and that I would need to be mainly at the home office.

Continue reading ...

development tech premek

Published on January 15, 2023

Asynchronous Server App Boilerplate Video Tutorial

Asynchronous Server App Boilerplate (or ASAB for short) is a microservice platform for Python 3.5+ and asyncio. The aim of ASAB is to minimize the amount of code that needs to be written when building a microservice or an aplication server.

Continue reading ...

tutorial development asab

Published on May 01, 2019

Entangled ways of product development in the area of cybersecurity #1 - Asynchronous or parallel?

I started working at TeskaLabs at the beginning of autumn 2017 as a student at the Faculty of Information Technology of CTU. In the job advertisement, I was particularly interested in the fact that it is a small, product-based company that does not focus on just one technology or one programming language.

Continue reading ...

development tech premek

Published on November 15, 2022