Mein erstes Mal PHP

This entry is part 13 of 13 in the series Azubis erzählen

Wie mein Azubi-Kollege Loei schon in seinem Weekly BlogPost hervorgebracht hat, wie es ihm mit Python erging, möchte ich da gleich anknüpfen und meine Erfahrungen ab dem Zeitpunkt vom Praktikum bei NETWAYS und über die Sprache PHP kundgeben. Vorsicht Plain text 😉.

Vom Praktikanten zum Azubi. Damals noch im alten Büro, mit meinem jetzt Azubi-Kollege Loei, voller Vorfreude endlich mal wieder in die Tasten zu hauen und Ergebnisse zu sehen. Angekommen im Raum erst einmal hingesetzt und verzweifelt nach dem Tower gesucht. Woas? Keiner da. Wir nutzen Laptops, wer hätte es gedacht? 🙂

Unser Praktikumsleiter war Marius, unser jetziger Abteilungsleiter und Ausbilder. Am ersten Tag hat Marius uns in das Betriebssystem Linux eingeführt und die gängigen Befehle beigebracht.
Danach ging es schon direkt los mit dem Aufsetzen eines Apache auf dem lokalen System. Fehlen durfte da natürlich nicht Vagrant und VirtualBox. Mit Vagrant war die Aufgabe das sogenannte Vagrantfile und die dazugehörige bootstrap-Datei, die Configs der Maschine, so zu konfigurieren das einige PHP-Pakete wie fpm und zusätzlich proxy-fcgi beim Ersten Start direkt mitinstalliert werden. Die Umgebung damals gleicht der heutigen sehr, mit dem einzigem Unterschied das ich zusätzlich noch in der aktuellen Projekt-Umgebung mariadb nutze.

Anschließend erwartete uns auch direkt die erste Aufgabe. In einem Projekt sollten wir die Schaltjahre bis zu einer angegebenen Jahreszahl, durch Modulo-Berechnung, auf der Konsole und in JSON-Format, ausgeben. Mit Learning by doing kämpften wir uns in der Welt von PHP und Konsolenausgaben den Weg zum Spaghetti-Code. Hatten wir eine Lösung wurde sie direkt evaluiert und wenn nötig abgeändert. Am Ende des Praktikums folgte dann die Bekanntgabe das wir Auszubildende werden könnten. Da die Zeit bis dahin sehr viel Spaß gemacht hat, und wir während dem Praktikum noch einen Azubi kennengelernt hatten der uns viel über NETWAYS erzählte, fiel uns die Entscheidung nicht schwer und nahmen den Vertrag an.

Fast ein Jahr später
Die Zeit, vom Ausbildungsbeginn, 03.09.2018 bis jetzt, den 19.06.2019, verging rasend. Mittlerweile wirken wir, die neuen Developement-Azubis, schon an den großen Projekten mit. Unter anderem dürfen wir kleine Bug-Fixes bis zu Implementierungen verschiedener Funktionen oder Klassen in die großen Projekte einbringen. Das Verwendungsspektrum von PHP ist bei uns sehr vielfältig und eigentlich überall vorzufinden. Meinem Azubi-Kollegen gefiel aber PHP gleich so gut, dass er anfing mit Python herumzuspielen.
Es gibt aber nicht nur PHP in unseren Projekten. Unzählige andere Projekte werden natürlich auch mit CSS, JavaScript, Python und noch weitere vorangetrieben.

Mittlerweile hab ich mich an PHP gewöhnt und darf mich bei den größeren Projekten austoben. Da PHP eine große Rolle spielt, sowohl bei uns in Projekten als auch weltweit im Internet, kann ich PHP nur weiter empfehlen. 😉

Niko Martini
Niko Martini
Junior Developer

Egal ob zu Hause oder bei NETWAYS, Niko hockt gern vor dem PC. Ab und zu fährt er auch mal mit seinem Dad auf eine Fahrradtour quer durch Deutschland. Nach seinen ersten Tagen bei uns, in denen er NETWAYS, die Kollegen und Tools näher kennengelernt hat, freut er sich besonders auf die kommenden Jahre.

Python lernen?

Hier möchte ich meine Erfahrung teilen, wie ich in die Sprache Python eingestiegen bin.

 

Am Anfang der Ausbildung bin ich in PHP eingestiegen und fand die Sprache sehr angenehm.

Dazu gehören natürlich auch die Auszeichnungs-, Definitionsprachen HTML & CSS.

Doch mit der Zeit wurde PHP für mich als Programmier-Neuling immer komplexer.

Im Hinterkopf behielt ich Python, weil ich immer wieder von Programmierern hörte wie einfach diese Sprache sei

und diese auch von ihnen empfohlen wurde.

Zudem soll sie einer der beliebtesten und sehr gut für Beginner sein.

Also was spricht dagegen sie zu lernen?          Richtig. Gar nichts.

Aber wie lernt man eine Programmiersprache? 

Durch Blogs? Durch Videokurse?  Durch Bücher?

Der eine findet Videokurse gut, der andere Blogs und reinen Text. Das ist ganz ok.

Solange man Fortschritte macht, macht man nichts falsch.

Ehrlich gesagt muss man für sich selber herausfinden wie man am besten lernen kann.

Vielleicht mal alle Lernangebote durchstöbern und dann entscheiden was passt.

Ich persönlich habe Bücher sehr in mein Herz eingeschlossen. Ein Grund:

Das Buch liegt direkt vor einem. Videokurse sind etwas umständlicher.

Also habe ich mir ein Buch geholt. Sogar ein sehr gutes von O’REILLY.

Es ist echt wichtig das in einem Buch alles ausführlich erklärt wird. In dem Fall tut es das Buch.

Und wie komme ich mit der Sprache zurecht?

Anders als bei der anderen Sprachen gibt es eine kleine Umstellung.

Und zwar sind die Einrückungen (die Suiten) sehr wichtig. Da wird gleich die Syntax mit geübt.

Daran muss man sich erst gewöhnen. Sonst komme ich zurzeit gut zurecht.

So möchte ich mir mit Python ein weiteres Fundament aufbauen.

Ich kann Python nur weiter empfehlen!

Loei Petrus Marogi
Loei Petrus Marogi
Junior Developer

Loei ist Fachinformatik-Azubi im ersten Lehrjahr und lernt momentan unseren Toolstack kennen. Nach der Linux-Schulung freut er sich besonders aufs Programmieren. Wenn er mal nicht bei NETWAYS ist, spielt er Fußball im Verein oder geht ins Fitnessstudio.

Generics waren gestern. Lang lebe Golangs Reflection!

Vor einiger Zeit habe ich die Programmiersprache Golang und all ihren Nutzen für die Entwickler vorgestellt. Zugegeben, eine in der Konkurrenz sehr verbreitete Funktionalität besitzt Go nicht: Generics. Und diese Funktionalität is noch dazu sehr gefragt. Allein das Gesamtbild der Reaktionen (Smileys) auf den Vorschlag, Generics in Go v2 zu integrieren, sagt mehr als 1000 Worte. Aber es geht auch anders…

Problem

Aktuell arbeite ich an einem (streng geheimen) Programm, das u.a. mit SQL-Datenbanken kommunizieren soll. Die grundsätzliche Infrastruktur dafür bringt Go von Haus aus mit. Jedoch kann es etwas mühselig sein, bei jeder Abfrage dieselbe Routine (samt Fehlerbehandlung) durchzukauen:

package blog

import (
	"database/sql"
)

type Employee struct {
	GivenName, FamilyName string
}

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errQuery := db.Query("SELECT given_name, family_name FROM employee")
	if errQuery != nil {
		return nil, errQuery
	}

	defer rows.Close()

	employees := []Employee{}

	for {
		if rows.Next() {
			row := Employee{}

			if errScan := rows.Scan(&row.GivenName, &row.FamilyName); errScan != nil {
				return nil, errScan
			}

			employees = append(employees, row)
		} else if errNext := rows.Err(); errNext == nil {
			break
		} else {
			return nil, errNext
		}
	}

	return employees, nil
}

Außerdem hat sich in einem vergangenen Projekt herausgestellt, dass (zumindest in Transaktionen) erst nach rows.Close() die nächste Datenbank-Operation beginnen kann. Dies verpflichtete fast schon dazu, den Code ab defer db.Close() bei jeder Abfrage so oder so ähnlich zu schreiben. Letztendlich löste das Team das Problem mit folgender Funktion:

func FetchAll(db *sql.DB, query string, args ...interface{}) ([][]interface{}, error)

Diese erledigte die oben gezeigte Routinearbeit und verringerte damit den Aufwand pro Abfrage deutlich:

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errFetchAll := FetchAll(db, "SELECT given_name, family_name FROM employee")
	if errFetchAll != nil {
		return nil, errFetchAll
	}

	employees := []Employee{}

	for _, row := range rows {
		employees = append(employees, Employee{row[0].(string), row[1].(string)})
	}

	return employees, nil
}

Jedoch war nun jede Spalte jeder Zeile des Ergebnisses ein interface{}, das erstmal in den richtigen Datentyp umgewandelt werden musste. Dafür wiederum musste die neue Funktion zusätzlich den Spaltentyp beim Datenbanktreiber erfragen, um immer die (hinter dem interface{} versteckten) Datentypen zurückzugeben, die die konkrete Abfrage erwartet. Andernfalls hätten wir uns auf die Standard-Datentypen der Datenbanktreiber verlassen müssen.

Lösung

Nun übernehme ich also den Code Schritt für Schritt in das neue Projekt und frage mich: Geht das nicht auch einfacher? Ja, mit sog. Reflection:

package blog

import (
	"database/sql"
	"reflect"
)

func FetchAll(db *sql.DB, rowType interface{}, query string, args ...interface{}) (interface{}, error) {
	rows, errQuery := db.Query(query, args...)

	if errQuery != nil {
		return nil, errQuery
	}

	defer rows.Close()

	blankRow := reflect.ValueOf(rowType)
	res := reflect.MakeSlice(reflect.SliceOf(blankRow.Type()), 0, 0)
	idx := -1
	scanDest := make([]interface{}, blankRow.NumField())

	for {
		if rows.Next() {
			res = reflect.Append(res, blankRow)
			idx++

			row := res.Index(idx)

			for i := range scanDest {
				scanDest[i] = row.Field(i).Addr().Interface()
			}

			if errScan := rows.Scan(scanDest...); errScan != nil {
				return nil, errScan
			}
		} else if errNext := rows.Err(); errNext == nil {
			break
		} else {
			return nil, errNext
		}
	}

	return res.Interface(), nil
}

Diese Funktion erwartet einen zusätzlichen Parameter, rowType. Dessen eigentlicher Typ hinter interface{} (Employee) bestimmt den Typ einer Zeile des Abfrage-Ergebnisses. Das komplette Ergebnis ist logischerweise eine Slice aus Zeilen ([]Employee). Mit Hilfe von Funktionen aus dem reflect-Paket arbeitet FetchAll() zur Laufzeit mit dem konkreten Datentyp Employee, fast so als wäre er mittels Generics zur Kompilierzeit bekannt:

  • reflect.ValueOf(rowType) analysiert rowType und kapselt ihn als Wert vom Typ Employee
  • reflect.ValueOf(rowType).Type() steht für Employee
  • reflect.SliceOf(Employee) steht für []Employee
  • reflect.MakeSlice([]Employee, 0, 0) steht für make([]Employee, 0, 0)
  • reflect.ValueOf(rowType).NumField() zählt die Felder des Structs Employee

Ja, richtig, rowType muss ein Struct sein, sonst stürzt das Programm spätestens bei reflect.ValueOf(rowType).NumField() ab. Jedes Feld des Structs steht nämlich für eine Spalte des Abfrage-Ergebnisses. Genau das wird in der darauf folgenden Schleife wie folgt bewerkstelligt:

  • res = reflect.Append(res, reflect.ValueOf(rowType)) steht für res = append(res, Employee{})
  • res.Index(idx) steht für res[idx]
  • res[idx].Field(0) steht für res[idx].GivenName
  • res[idx].GivenName.Addr() steht für &res[idx].GivenName

Und .Interface() holt letztendlich den Zeiger auf das Struct-Feld aus der Reflection-Versenkung, damit rows.Scan() die entsprechende Spalte des Abfrage-Ergebnisses darin speichert. Am Ende verbirgt sich hinter res tatsächlich ein []Employee, das mit res.Interface() in ein interface{} gekapselt, um es zurückzugeben. Damit bestimmt GetEmployees() den Zeilen-Typ im voraus und schrumpft auf ein vernünftiges Minimum:

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errFetchAll := FetchAll(db, Employee{}, "SELECT given_name, family_name FROM employee")
	if errFetchAll != nil {
		return nil, errFetchAll
	}

	return rows.([]Employee), nil
}

Fazit

Nachdem ich zuletzt schon eine C-Bibliothek in Go wiederverwendet habe, spare ich schon zum zweiten mal in Folge Code und damit Zeit. Sprich, wir arbeiten jetzt noch ein bisschen effizienter (als sowieso schon) an euren Projekten. Bestelle noch heute!

Alexander Klimov
Alexander Klimov
Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

Verschachtelte Listen mit Sortable.js

Nachdem ich vor einiger Zeit die Ehre hatte Drag & Drop im Business Process Modul für Icinga Web 2 zu integrieren, dachte ich mir ich plaudere heute mal ein wenig aus dem Nähkästchen welche Stolpersteine ich dabei überwinden musste.

Aber erst einmal eine kleine Vorstellung der Bibliothek die eingesetzt wurde: Sortable.js 

Die Entscheidung hierfür fiel erst nicht leicht, da das Angebot jener Bibliotheken die sich Drag & Drop verschrieben haben, nicht besonders klein ist. Aber wo wir gerade von klein reden, ist das bereits der wichtigste Punkt der Sortable.js von anderen abhebt. Weil sie nur das nötigste an Funktionalität abdeckt und keine fancy Animationen nutzt die nicht nur Leistung fressen sondern auch Dateigröße ist diese Bibliothek mit 25kB (minimized) eines der Leichtgewichte unter ihren Vertretern. Außerdem existiert keine Abhängigkeit zu jQueryUI, das alleine hat bereits überzeugt.

Nun aber zum eigentlichen Thema. Während Sortable.js wunderbar mit einfachen Listen arbeiten kann, wird es etwas anspruchsvoller wenn es um verschachtelte Listen geht. Glücklicherweise wurde in diesem Segment in den letzen Wochen einiges getan und die Unterstützung erheblich verbessert. Dennoch gibt es etwas das immer noch existiert und mir einige ruhelose Nächte bereitet hat. Gut, so schlimm war es nun auch wieder nicht, dennoch, es könnte ja einigen genauso wie mir nicht sofort wie Schuppen von den Augen fallen.

Aber erst einmal die Demo die das Problem darstellt. Versucht einmal alles aus der roten Liste zu entfernen und dann wieder Elemente aus der blauen hineinzuschieben. Demo

Klappt nicht so ganz? Tja, das liegt daran, dass die rote Liste zu klein ist sobald keine Elemente mehr enthalten sind. Der findige Leser denkt jetzt eventuell daran der Liste eine Mindesthöhe zu geben. Gar nicht so falsch. Demo

Klappt dennoch nicht? Hehe, willkommen im Club. Ein Blick in die README.md von Sortable.js und man findet emptyInsertThreshold. Leider führt der Name dieser Option eher in die Irre, denn die Lösung ist sie nicht. Euer Blick sollte eher auf invertSwap fallen. Demo

Warum das nun funktioniert? Arr, das geht etwas über das Ziel des Beitrags hinaus. Ich kann euch aber folgendes ans Herz legen.

Johannes Meyer
Johannes Meyer
Developer

Johannes ist seit 2011 bei uns und hilft bei der Entwicklung zukünftiger Knüller (Icinga2, Icinga Web 2, ...) aus dem Hause NETWAYS.

i-doit API Ruby-Scripting

Wie in meinem letzten Blogpost angekündigt stelle ich heute eine gescriptete Variante für die API der i-doit CMDB vor.

Einzelne Abfragen, sowie das Anlegen von Objekten über den RESTClient mögen auf den ersten Blick zum Testen bzw. Debuggen  ziemlich sinnvoll erscheinen, aber man stößt schnell an Grenzen, sobald es sich über mehrere hundert solcher Objekte handelt. Darüber hinaus ist diese Methode sehr umständlich.

Meine Idee war es eine Webanwendung zu benutzen, welche mit einem einzelnen Requests (im JSON-Format) an die API von I-doit Abfragen oder das Neuanlegen von Objekten vereinfacht. Da bot sich das Framework Sinatra an, da dieses eine sehr einfache und schnell zu lernende DSL besitzt. Wenn diese Anwendung läuft, kann mit der Eingabe einer bestimmten URL, ein Objekt abgefragt bzw. erstellt werden. In diesem Beispiel, werde ich die Namen sowie IDs aller Server aus der CMDB auslesen.

Zunächst müssen wir folgende drei Bibliotheken einbinden:

#!/usr/bin/env ruby

require 'sinatra/base'
require 'json'
require 'rest-client

Im nächsten Schritt bauen wir uns einen generischen Request (wie in meinem vorherigen Blogpost zu lesen) zusammen:

$baseurl = "https://example-idoit-web-gui.de"
$apikey = "random_key"

class CMDBApi
  def initialize(url=$baseurl, apikey=$apikey, objID=nil)
    @url = url
    @apikey = apikey
    @objID = objID
  end

  def postHeader(endpoint, method, payload)
    response = RestClient::Request.new({
      :method => method,
      :headers => {:accept => :json, :content_type => :json},
      :url => "#{@url}/#{endpoint}",
      :payload => payload
    }).execute
  end
  def readAllServer()
    payloadMetadata = Hash.new
    payloadMetadata[:version] = "2.0"
    payloadMetadata[:id] = "1"
    payloadMetadata[:method] = "cmdb.objects.read"
    payloadMetadata[:params] = Hash.new
    payloadMetadata[:params][:apikey] = @apikey
    payloadMetadata[:params][:filter] = {"type": "5"}
    @payloadJsonFormat = payloadMetadata.to_json
    response = postHeader("src/jsonrpc.php", :post, @payloadJsonFormat)
    response_hash = JSON.parse(response)
  end
end

get "/server" do
  api = CMDBApi.new($baseurl, $apikey)
  response = api.readAllServer(objectID)
  allServerHash = {}
  response["result"].each do |result|
    serverID = result["id"]
    serverName = result["title"]
    allServerHash[:"#{serverName}"] = serverID
  end
  allServerHash.to_json
end

Wenn man das Skript ausführt, wird (je nach Konfiguration) über http://127.0.0.1:4567 auf das Webfrontend zu gegriffen. Durch das zusätzliche Anhängen von  “/server” ist es möglich, alle Servernamen und deren zugehörigen Server-IDs aus i-doit auszulesen.

{
  • "Server_A" : 66273,
  • "Server_B" : 94647,
  • “Server_C” : 21221,
  • “…”: ….
}

Der große Vorteil einer solchen Webanwendung ist, dass man durch Erweiterung eine Automatisierung erreichen kann. Erhält man z.B. einen neuen Server, der mittels Puppet provisioniert wurde, kann vom Puppetmaster ein Objekt im i-doit angelegt werden. Hierzu kann damit auch auf alle ermittelten Facts zurückgegriffen werden. Diese sind dann über das zu erweiternde Skript an die API weiterzugeben, um final ein Server-Objekt mit allen angereicherten Informationen zu erhalten.

Philipp Dorschner
Philipp Dorschner
Junior Consultant

Philipp hat im September 2017 seine Ausbildung zum Fachinformatiker gestartet. Er hat sogar schon eine Ausbildung im Gepäck und zwar zum technischen Assistenten für Informatik. Danach hat hat er sein Abi nachgeholt und anschließend begonnen Wirtschaftswissenschaften zu studieren. Da sein Herz während des Studiums ständig nach technischen Inhalten geschrien hat, wechselte er zu Verfahrenstechnik. Aber auch dieses Studium konnte Ihn nicht erfüllen, weshalb er sich für die Ausbildung bei NETWAYS entschieden hat, "back to the...

Modern C++ programming: Coroutines with Boost

(c) https://xkcd.com/303/

We’re rewriting our network stack in Icinga 2.11 in order to to eliminate bugs with timeouts, connection problems, improve the overall performance. Last but not least, we want to use modern library code instead of many thousands of lines of custom written code. More details can be found in this GitHub issue.

From a developer’s point of view, we’ve evaluated different libraries and frameworks before deciding on a possible solution. Alex created several PoCs and already did a deep-dive into several Boost libraries and modern application programming. This really is a challenge for me, keeping up with the new standards and possibilities. Always learning, always improving, so I had a read on the weekend in “Boost C++ Application Development Cookbook – Second Edition“.

One of things which are quite resource consuming in Icinga 2 Core is multi threading with locks, waits and context switching. The more threads you spawn and manage, the more work needs to be done in the Kernel, especially on (embedded) hardware with a single CPU core. Jean already shared insights how Go solves this with Goroutines, now I am looking into Coroutines in C++.

 

Coroutine – what’s that?

Typically, a function in a thread runs, waits for locks, and later returns, freeing the locked resource. What if such a function could be suspended at certain points, and continue once there’s resources available again? The benefit would also be that wait times for locks are reduced.

Boost Coroutine as library provides this functionality. Whenever a function is suspended, its frame is put onto the stack. At a later point, it is then resumed. In the background, the Kernel is not needed for context switching as only stack pointers are stored. This is done with Boost’s Context library which uses hardware registers, and is not portable. Some architectures don’t support it yet (like Sparc).

Boost.Context is a foundational library that provides a sort of cooperative multitasking on a single thread. By providing an abstraction of the current execution state in the current thread, including the stack (with local variables) and stack pointer, all registers and CPU flags, and the instruction pointer, a execution context represents a specific point in the application’s execution path. This is useful for building higher-level abstractions, like coroutinescooperative threads (userland threads) or an equivalent to C# keyword yield in C++.

callcc()/continuation provides the means to suspend the current execution path and to transfer execution control, thereby permitting another context to run on the current thread. This state full transfer mechanism enables a context to suspend execution from within nested functions and, later, to resume from where it was suspended. While the execution path represented by a continuation only runs on a single thread, it can be migrated to another thread at any given time.

context switch between threads requires system calls (involving the OS kernel), which can cost more than thousand CPU cycles on x86 CPUs. By contrast, transferring control vias callcc()/continuation requires only few CPU cycles because it does not involve system calls as it is done within a single thread.

TL;DR – in the way we write our code, we can suspend function calls and free resources for other functions requiring it, without typical thread context switches enforced by the Kernel. A more deep-dive into Coroutines, await and concurrency can be found in this presentation and this blog post.

 

A simple Example

$ vim coroutine.cpp

#include <boost/coroutine/all.hpp>
#include <iostream>

using namespace boost::coroutines;

void coro(coroutine::push_type &yield)
{
        std::cout << "[coro]: Helloooooooooo" << std::endl;
        /* Suspend here, wait for resume. */
        yield();
        std::cout << "[coro]: Just awesome, this coroutine " << std::endl;
}

int main()
{
        coroutine::pull_type resume{coro};
        /* coro is called once, and returns here. */

        std::cout << "[main]: ....... " << std::endl; //flush here

        /* Now resume the coro. */
        resume();

        std::cout << "[main]: here at NETWAYS! :)" << std::endl;
}

 

Build it

On macOS, you can install Boost like this, Linux and Windows require some more effort listed in the Icinga development docs). You’ll also need CMake and g++/clang as build tool.

brew install ccache boost cmake 

Add the following CMakeLists.txt file into the same directory:

$ vim CMakeLists.txt

cmake_minimum_required(VERSION 2.8.8)
set(BOOST_MIN_VERSION "1.66.0")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")

find_package(Boost ${BOOST_MIN_VERSION} COMPONENTS context coroutine date_time thread system program_options regex REQUIRED)

# Boost.Coroutine2 (the successor of Boost.Coroutine)
# (1) doesn't even exist in old Boost versions and
# (2) isn't supported by ASIO, yet.
add_definitions(-DBOOST_COROUTINES_NO_DEPRECATION_WARNING)

link_directories(${Boost_LIBRARY_DIRS})
include_directories(${Boost_INCLUDE_DIRS})

set(base_DEPS ${CMAKE_DL_LIBS} ${Boost_LIBRARIES})

set(base_SOURCES
  coroutine.cpp
)

add_executable(coroutine
        ${base_SOURCES}
)

target_link_libraries(coroutine ${base_DEPS})

set_target_properties(
        coroutine PROPERTIES
        FOLDER Bin
        OUTPUT_NAME boost-coroutine
)

Next, run CMake to check for the requirements and invoke make to build the project afters.

cmake .
make

 

Run and understand the program

$ ./boost-coroutine
[coro]: Helloooooooooo
[main]: .......
[coro]: Just awesome, this coroutine
[main]: here at NETWAYS! :)

Now, what exactly happened here? The Boost coroutine library allows us to specify the “push_type” where this functions should be suspended, after reaching this point, a subsequent call to “yield()” is required to resume this function.

void coro(coroutine::push_type &yield)

Up until “yield()”, the function logs the first line to stdout.

The first call happens inside the “main()” function, by specifying the pull_type and directly calling the function as coroutine. The pull_type called “resume()” (free form naming!) must then be explicitly invoked in order to resume the coroutine.

coroutine::pull_type resume{coro};

After the first line is logged from the coroutine, it stops before “yield()”. The main function logs the second line.

[coro]: Helloooooooooo
[main]: .......

Now comes the fun part – let’s resume the coroutine. It doesn’t start again, but the function’s progress is stored as stack pointer, targeting “yield()”. Exactly this resume function is called with “resume()”.

        /* Now resume the coro. */
        resume();

That being said, there’s more to log inside the coroutine.

[coro]: Just awesome, this coroutine

After that, it reaches the end and returns to the main function. That one logs the last line and terminates.

[main]: here at NETWAYS! :)

Without a coroutine, such synchronisation between functions and threads would need waits, condition variables and lock guards.

 

Icinga and Coroutines

With Boost ASIO, the spawn() method wraps coroutines on a higher level and hides the strand required. This is used in the current code and binds a function into its scope. We’re using lambda functions available with C++11 in most locations.

The following example implements the server side of our API waiting for new connections. An endless loop listens for incoming connections with “server->async_accept()”.

Then comes the tricky part:

  • 2.9 and before spawned a thread for each connection. Lots of threads, context switches and memory leaks with stalled connections.
  • 2.10 implemented a thread pool, managing the resources. Handling the client including asynchronous TLS handshakes are slower, and still many context switches ahead between multiple connections until everything stalls.
  • 2.11 spawns a coroutine which handles the client connection. The yield_context is required to suspend/resume the function inside.

 

void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext)
{
	namespace asio = boost::asio;

	auto& io (server->get_io_service());

	for (;;) {
		try {
			auto sslConn (std::make_shared(io, *sslContext));

			server->async_accept(sslConn->lowest_layer(), yc);

			asio::spawn(io, [this, sslConn](asio::yield_context yc) { NewClientHandler(yc, sslConn, String(), RoleServer); });
		} catch (const std::exception& ex) {
			Log(LogCritical, "ApiListener")
				<< "Cannot accept new connection: " << DiagnosticInformation(ex, false);
		}
	}
}

The client handling is done in “NewClientHandlerInternal()” which follows this flow:

  • Asynchronous TLS handshake using the yield context (Boost does context switches for us), Boost ASIO internally suspends functions.
    • TLS Shutdown if needed (again, yield_context handled by Boost ASIO)
  • JSON-RPC client
    • Send hello message (and use the context for coroutines)

And again, this is the IOBoundWork done here. For the more CPU hungry tasks, we’re using a different CpuBoundWork pool which again spawns another coroutine. For JSON-RPC clients this mainly affects syncing runtime objects, config files and the replay logs.

Generally speaking, we’ve replaced our custom thread pool and message queues for IO handling with the power of Boost ASIO, Coroutines and Context thus far.

 

What’s next?

After finalizing the implementation, testing and benchmarks are on the schedule – snapshot packages are already available for you at packages.icinga.com. Coroutines will certainly help embedded devices with low CPU power to run even faster with many network connections.

Boost ASIO is not yet compatible with Coroutine2. Once it is, the next shift in modernizing our code is planned. Up until then, there are more Boost features available with the move from 1.53 to 1.66. Our developers are hard at work with implementing bug fixes, features and learning all the good things.

There’s many cool things under the hood with Icinga 2 Core. If you want to learn more and become a future maintainer, join our adventure! 🙂

Michael Friedrich
Michael Friedrich
Senior Developer

Michael ist seit vielen Jahren Icinga-Entwickler und hat sich Ende 2012 in das Abenteuer NETWAYS gewagt. Ein Umzug von Wien nach Nürnberg mit der Vorliebe, österreichische Köstlichkeiten zu importieren - so mancher Kollege verzweifelt an den süchtig machenden Dragee-Keksi und der Linzer Torte. Oder schlicht am österreichischen Dialekt der gerne mit Thomas im Büro intensiviert wird ("Jo eh."). Wenn sich Michael mal nicht in der Community helfend meldet, arbeitet er am nächsten LEGO-Projekt oder geniesst...