Published:

Jarle Aase

Monthly update, September 2024

bookmark 7 min read

These monthly updates may be a bit technical. They are written to my future self (to remember how I spent the month - and to motivate me to do at least something remotely interesting), to friends and colleagues (past and future) to have an idea about what I work on. And of course to potential clients for my C++ freelance business and fellow software developers.

Projects

Next-App

Next-app is an upcoming GTD/productivity application for desktop and mobile.

Recently I realized two things.

1) NextApp cant use the backend as its sole storage. There are just too many requests from the app too the server. It will slow down the app for users that are far from their backend, or on a backend with too many users. It will also be expensive in terms of all the data that needs to be transferred from the backend. So I sketched an updated architecture with a local cache with SQLite on the device, and new gRPC calls that stream new or updated batches of data from the backend to the app when it starts up. Once connected, the app remains updated by subscribing to an updates stream that informs it about every change in the state that is relevant for the user. This lead to the second realization.

2) QT's support for async processing totally sucks! I decided to use gRPC streams to do the initial sync from the server to the client. This allows the server to use a cursor to iterate over the data tables, and send potentially tens of thousands of rows from one query. This is the most effective way to use the database in the backend (unless there are too many slow cursors that cause extra work for the ongoing transactions). I also decided to put the local SQLite database in a separate thread, so that it would not cause small freezes in the UI on slow disks (like some phones or PC's with spinning harddisks). Doing this in QT is possible, but the code will be very hard to read. So I decided to use co-routines, as I do everywhere else. Fortunately, I did not have to wite my own co-routine library. Someone from the KDE project already did this: QCoro - Coroutines for Qt5 and Qt6.

Adding QCoro to the NextApp UI application has been a blessing. With that, I can consume the incoming gRPC stream ( via a template wrapper I made) in a for loop, and I can query the local database from a worker thread without stalling the main thread:

 1
 2   // Simplified code from a method in template extension class that synchs data from
 3   // the server and saves it to SQLite.
 4   QCoro::Task<bool> synchFromServer()
 5    {
 6        ...
 7        auto stream = openServerStream(req);
 8
 9        while (auto update = co_await stream->template next<nextapp::pb::Status>()) {
10            if (update.has_value()) {
11                auto &u = update.value();
12                if (u.error() == nextapp::pb::ErrorGadget::OK) {
13                    if (hasItems(u)) {
14                        // Save the data to SQLite in a worker-thread
15                        co_await save(u.items());
16                    }
17                }
18            }
19        }
20
21        co_return true;
22    }

It's an annoyance that QObject derived classes can't be template classes, so I had do make some work-arounds. But all in all, QT is much simpler to work with now, using QCoro.

By the end of September one of my dogs got seriously ill. I prioritized taking care of him over coding. The migration to use a local cache is therefore still work in progress.

Mysqlpool

Mysqlpool is lightweight async connection-pool library, built on top of boost.mysql.

In order to get "cursors" over MariaDB tables to work in NextApp, I integrated batched queries into mysqlpool.

 1
 2    co_await  rctx.dbh->start_exec(
 3        R"(SELECT deleted, updated, date, color, notes, report FROM day
 4            WHERE user=? AND updated > ?)",
 5        uctx->dbOptions(), cuser, toMsDateTime(req->since(), uctx->tz(), true));
 6
 7    bool read_more = true;
 8    for(auto rows = co_await rctx.dbh->readSome()
 9            ; read_more
10            ; rows = co_await rctx.dbh->readSome()) {
11
12        read_more = rctx.dbh->shouldReadMore(); // For next iteration
13
14        for(const auto& row : rows) {
15            ...

The way the boost.mysql driver works, is to return an array of rows, and than you can come back and read another array of rows in a loop. So in order to process the data (without getting clever with lazy std::view's) is to use an outer and an inner loop to get to the rows.

Metrics in Yahat

yahat-cpp is a trivial HTTP server for simple REST API's and other HTTP interfaces in C++ projects.

Now that I'm getting close to deploy nextapp backends and nsblast clusters for people other than myself, I need "observability" of the servers. I want to know if there are logged errors or warnings, I want to know how many requests per second the gRPC services are processing. For NextApp, I also want to know how many users and devices who are connected. I read a textbook about Prometheus and decided that the best way forward is to add an OpenMetrics HTTP endpoint in NextApp and nsblast. I will use Prometheus and Grafana for now, but with OpenMetrics it's simple to change to whatever I like later on.

OpenMetrics is usually provided from a server by exposing a HTTP endpoint named /metrics. I already had a HTTP server I could use, yahat, so I spent a few hours over a weekend adding support for the most crucial metrics for me, Info, Counter and Gauge. I'll add the remaining types as I need them in my projects.

Now, adding metrics in my server applications is trivial.

 1    std::optional<yahat::HttpServer> http_server_;
 2
 3    ...
 4
 5    LOG_INFO << "Starting HTTP server (metrics).";
 6    http_server_.emplace(config().http, [this](const yahat::AuthReq& ar) {
 7        // TODO: Add actual authentication
 8        return yahat::Auth{"admin", true};
 9    }, metrics_.metrics(), "nextapp "s + NEXTAPP_VERSION);
10
11    http_server_->start();
 1
 2class Metrics
 3{
 4public:
 5    using gauge_t = yahat::Metrics::Gauge<uint64_t>;
 6    using counter_t = yahat::Metrics::Counter<uint64_t>;
 7    using gauge_scoped_t = yahat::Metrics::Scoped<gauge_t>;
 8    using counter_scoped_t = yahat::Metrics::Scoped<counter_t>;
 9
10    Metrics(Server& server);
11
12    // Access to various metrics objects
13    counter_t& errors() {
14        return *errors_;
15    }
16
17    gauge_t& session_subscriptions() {
18        return *session_subscriptions_;
19    }
20
21    gauge_t& sessions_user() {
22        return *sessions_user_;
23    }
24
25    gauge_t& sessions_admin() {
26        return *sessions_admin_;
27    }
28
29
30private:
31    Server& server_;
32    yahat::Metrics metrics_;
33
34    counter_t * errors_{};
35    counter_t * warnings_{};
36    gauge_t * sessions_user_{};
37    gauge_t * sessions_admin_{};
38};
39
 1
 2// We use a log-handler to count error and warning log events.
 3class LogHandler : public logfault::Handler {
 4public:
 5    LogHandler(logfault::LogLevel level, Metrics::counter_t *counter)
 6        : Handler(level),  level_{level}, counter_{counter} {
 7    };
 8
 9    void LogMessage(const logfault::Message& msg) override {
10        if (msg.level_ == level_) {
11            counter_->inc();
12        }
13    }
14
15private:
16    const logfault::LogLevel level_;
17    Metrics::counter_t *counter_;
18};
19}
20
21
22Metrics::Metrics(Server& server)
23    : server_{server}
24{
25    // Create the metrics objects
26    errors_ = metrics_.AddCounter("nextapp_logged_errors", "Number of errors logged", {});
27    warnings_ = metrics_.AddCounter("nextapp_logged_warnings", "Number of warnings logged", {});
28    sessions_user_ = metrics_.AddGauge("nextapp_sessions", "Number of user sessions", {}, {{"kind", "user"}});
29    sessions_admin_ = metrics_.AddGauge("nextapp_sessions", "Number of admin sessions", {}, {{"kind", "admin"}});
30
31    logfault::LogManager::Instance().AddHandler(std::make_unique<LogHandler>(logfault::LogLevel::ERROR, errors_));
32    logfault::LogManager::Instance().AddHandler(std::make_unique<LogHandler>(logfault::LogLevel::WARN, warnings_));
33}
34

And finally a few examples on how to produce the metrics values.

 1
 2    // From code that creates a user-session in a server. gRPC is session based, so it makes sense,
 3    // even in a "stateless" server.
 4    auto scope = is_admin ? metrics().sessions_admin().scoped() : metrics().sessions_user().scoped();
 5
 6    // Here, the `scope` will exist until the session is destroyed.
 7    // The scope increments a gauges value when it is constructed, and decrement it
 8    // when it is destroyed.
 9    auto session = make_shared<UserContext::Session>(ucx, device_uuid, new_sid, std::move(scope));
10    sessions_[session->sessionId()] = session.get();
11
12    ...
13
14    // Just a simple increment of a Counter
15    counter_->inc();
16
17    ...
18
19    // Setting up an Info object when the server starts
20    Server::Server(const Config& config)
21    : config_(config), metrics_(*this)
22    {
23        metrics().AddInfo("nextapp_build", "Build information", {}, {
24            {"version", NEXTAPP_VERSION},
25            {"build_date", __DATE__},
26            {"build_time", __TIME__},
27            {"platform", BOOST_PLATFORM},
28            {"branch", GIT_BRANCH} // From CMake
29        });
30    }
31

All in all, the metrics provided by Yahat is simple to use, gives a low overhead, and produce a standardized output.

Books completed

Terraform: Up and running, 3rd edition, Yevgeniy Brikman.

I am first and foremost a software developer. But I have also done lots of sysadmin things in the past. I was partly responsible for a room full of Linux an Windows servers for local businesses back around 1996 - 2000. Back then I wrote monitoring software with agents on the servers and a server on my workstation with a dashboard that showed a list of all the machines and their health. Later, when the "cloud" began to gain popularity, I wrote an orchestration application to deploy and scale infrastructure on AWS, including templated bootstrapping of Linux, so the servers started up with the software they needed installed and configured. I also wrote a application a few years ago to deploy and scale complex applications in kubernetes, without the need for "operators".

Over the last decade I have defaulted to use ansible whenever I needed to deploy the same stuff on several machines. But in the last few years, I have noticed a lot of chatter on Reddit (devops) about Terraform which everybody seems to be using. Recently Terraform moved away from open source, causing a fork (OpenTofu) which is backed by the Linux Foundation. So I grew curious. I am about to deploy new "cloud" infrastructure shortly to offer a subscription based back-end for NextApp for people who don't run their own servers. That was why I picked this book when I finished x64 Assembly Language Step-by-Step.

My initial thoughts after reading a few chapters was "What a piece of total crap!" Not the book, but Terraform! The book did a decent job describing how it works, and also gave me a much appreciated update on how AWS works today. I prefer to deploy my stuff on Linodes, as the cost there is 100% predictable, and technical support works well. I have not done much anything on AWS for a couple of years. The book also thought me a few things in other areas, and is has a nice checklist for deploying a service to production. There was nothing new on that list, but it had all the essential things one need to deal with in one place.

When it comes to deployments - well, I will probably stay far away from Terraform.

Prometheus Up & Running, 2nd edition, Julien Picotto & Brian Brazil

The book appears a bit uninspired to me, but it gave me the overview and knowledge I needed in order to decide the way forward for observability in my apps in the coming years.

Cancelled Safari Books Online

I have subscribed to Safari Books Online, probably for a decade. It's quite expensive. It does give access to lots of relevant books, but the quality is really bad. Their Android app require more resources than most new games (like Free Fire). I actually ended up scrapping a 3 year old Lenovo Yoga tablet 2 years ago because it became too slow with the Safari app. The new flagship model of the Yoga works. But there is more. The app does not show syntax highlighting for source code, and in many books the code is just unformatted text in a normal font. It's unreadable. And there is even more. In some books the graphics (diagrams, sketches, tables) is just missing!

This year I mostly used Firefox to read the books, using the web version. This also sucks, as the current location is lost if Firefox is closed, and when the session times out.

Recently I have actually bought data books from Amazon and switched from Safari to reading them on the kindle app on my Yoga tablet. I don't like Amazon for many good reasons, but the Kindle app is 100% reliable. The graphics is there. The source code in the books is easy to read. The current position is never lost.

Bye, bye Safari Books. I shall not miss you!

Productivity

I have struggled most of my adult life with being productive, most of the time. Usually I overestimate what I can get done in any time-span, from a few minutes to a 5 years plan.

Now that I am using NextApp myself, I start the day by assigning time slots to the various things on my list. I kind of works for me. I still overestimate what I can do in an hour, but since I need to make adjustments or re-prioritize instantly when reality bites my schedule, I gradually get better at estimating and planning. I also get more productive.

So even if noone else ever use this app, it's a game changer for me. That alone is plenty enough reason to put more effort into it.