Kafkaコンシューマグループのパラメータについて

この記事はKafka Advent Calendar 2021の2日目の記事です。

最近仕事でKafkaコンシューマグループの動作に関わる幾つかのパラメータチューニングをしたので、備忘録として残しておきます。

Kafkaコンシューマーの日本語での説明はKafka コンシューマー — Confluent Documentation の記事が詳しいです。

heartbeat.interval.ms

コンシューマーはグループに参加した後一定間隔でコーディネーター(に指定されたブローカー)にハートビートを送りますが、その間隔を指定するパラーメータです。ハートビートは通常時ではコンシューマーがまだ生きていることをコーディネーターに通知する役割を果たします。リバランスの際にはコーディネーターからコンシューマーにリバランスが発生していること(REBALANCE_IN_PROGRESS)を通知する役割も果たしているため、この値を小さくするとリバランスの時間を短くすることができます。下記の session.timeout.ms の1/3より小さな値が推奨されています。 デフォルトは3秒です。

session.timeout.ms

コーディネーターが一定期間コンシューマーからハートビートを受け取らないとそのコンシューマーは死んだと見なされてグループから追放され、リバランスが発生します。この期間を指定するパラメータです。 デフォルトは10秒です(3.0から45秒になります))。 小さい値にすると既に死んだコンシューマーを早く検知できるというメリットがありますが、実はまだ生きているがGCやネットワーク遅延などの予期せぬトラブルで単にハートビートが少しの期間送れなかっただけという場合もあり、あまり小さすぎる値にすると不必要にリバランスを発生させてしまうので注意が必要です。 ブローカーのgroup.min.session.timeout.msgroup.max.session.timeout.msの範囲内になる必要があります。

ちなみに仕事のプロジェクトではちょっと特殊なケースで、ある制約でパーティションが1つしかないトピックにコンシューマーをスタンバイ2台含めて3台配置してHA構成にしていました。アクティブなコンシューマーが何らかの理由で死んだらなるべく早くスタンバイに切り替わって欲しいのでかなり小さい(1秒) session.timeout.msを設定しようとしたのですが、Confluent Cloudは group.min.session.timeout.ms (デフォルト6秒)をユーザ側で設定できるようになっておらず、6秒以下の値を使うことができないことが判明しました。。

参考

Apache Kafka Data Access Semantics: Consumers and Membership | Confluent

Integrantのコードナビゲーションを可能にするIntelliJプラグイン

この記事はJetBrains Advent Calendar 2021の1日目の記事です。

今の会社ではClojureでバックエンドのアプリケーションを開発しており、Integrant というライブラリを使っていわゆるDI(Dependency Injection)をしています。

Integrantの詳細はこちらの記事が詳しいです。

scrapbox.io

コンポーネントの設定と初期化のコードは同じファイルに書くこともできますが、規模が大きい場合は大抵設定のマップを別途ednファイルに書くと思います。以下が例です。

コンポーネントの設定(edn)

{:adapter/jetty {:port 8080, :handler #ig/ref :handler/greet}
 :handler/greet {:name "Alice"}}

コンポーネントの初期化(clj)

(require '[ring.adapter.jetty :as jetty]
         '[ring.util.response :as resp])

(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}]
  (jetty/run-jetty handler (-> opts (dissoc :handler) (assoc :join? false))))

(defmethod ig/init-key :handler/greet [_ {:keys [name]}]
  (fn [_] (resp/response (str "Hello " name))))

普段コードを読んでいる時にあるコンポーネントがどういう風に初期化されているか調べたい場合が結構あるんですが、これを行うにはコンポーネントのキーでgrepしてinit-keyが前にある行にジャンプするという手間がかかり、これをもっと簡単に行えるIntelliJプラグインを開発しました。

デモ


www.youtube.com

以下の機能をサポートしています。

プラグインIntelliJのMarketplaceに公開されています。 Integrant - IntelliJ IDEs Plugin | Marketplace

実装の詳細

ソースコードはこちらです。 https://github.com/shinichy/integrant-intellij-plugin

ベースにしたテンプレートのコードがKotlinだったので、Kotlinで実装しました。

Direct Navigationという仕組みを使って実装しています。Go to Declaration or Usages コマンド(MacだとCmd+BもしくはCmdキー押しながらマウスクリック)を関数や変数を使用している箇所で実行するとそれらの定義の箇所にジャンプできますが、Clojureのキーワードを対象に実行しても通常何も起きません。Direct Navigationはそのコマンドの実行をフックする仕組みで、getNavigationElement(element: PsiElement) で 対象の elementに対応する PsiElementを返すと返したPsiElementにジャンプしてくれます。例えば element がMapのキーの時に対応するコンポーネントinit-keyPsiElementを返すことで、Mapのキーからinit-keyの初期化コードにジャンプさせることができます。PsiElementIntelliJがコードをパースした結果のASTの要素です。PSI Elements | IntelliJ Platform Plugin SDK

Mapのキーに対応したinit-key の定義を探す際はintegrant.core/init-keyの使用箇所を検索し、init-keyの後にMapのキーと一致するキーワードを探しています。コマンドの実行の度に毎回検索しているのでちょっと遅いですが、今の会社のプロジェクトの規模くらいだとそこまで気にはなりませんでした。もっと早くするために専用のインデックスを用意してもよさそうです。

IntelliJが生成するASTは当然Clojureのコードに対応していることが前提なので、このプラグインCursiveに依存しています。結構Cursiveの内部の実装クラス(cursive.psi.impl.*)に依存してしまっているので、将来のCursiveのアップデートで動かなくなってしまうかもしれません。。

感想

プラグインを開発するのは今回初めてだったのですが、公式のテンプレートがあり、これをベースにするとGithub Actions等も含めてプロジェクトの雛形を生成してくれるので立ち上がりはかなりいい開発体験でした。

Kotlinは今回初めて書いたのですが、Scalaに似ているのでそれほど違和感なくすんなり書けました。慣れのせいもありますがやっぱりScalaの方が好みです(returnをいちいち書かなくてはいけなかったり、nullable型よりOptionの方が扱いやすい等)。

プラグイン開発のドキュメントは公式ドキュメントが結構充実していて参考になりますが、それ以外ほとんど情報がなく、公式ドキュメントにない情報は他のプラグインIntelliJソースコードを参照するしかなく結構大変でした。テストも実装しようとしたのですが、何故かうまく動かず諦めました。。

2020年振り返り

色々今年の振り返りブログ読んで触発されたので、自分も振り返ってみた。

私生活

子供(第一子)が生まれた。完全に子供中心の生活になった。大変だけど幸福度も上がった。 子育てが忙しいのとCOVIDであまりでかけたりできなかった。高い家賃払ってアメリカにいる意味あるんだろうか。

ゲーム

忙しいと言いつつ夜はゲーム結構した。どうぶつの森FF7リメイク、ファイアーエムブレム風花雪月、Fall Guys、オクトパストラベラー、聖剣伝説3リメイク、桃鉄。子供が寝た後にしかできないので最近はSwitchばかり。

読書(漫画以外)

プログラミング、子供が生まれてからは子育て本中心。読書時間は明らかに減った。

  • Practical FP in Scala - A hands-on approach
  • Functional and Reactive Domain Modeling
  • フランスの子どもは夜泣きをしない パリ発「子育て」の秘密
  • ジーナ式 カリスマ・ナニーが教える 赤ちゃんとおかあさんの快眠講座
  • 赤ちゃんにもママにも優しい安眠ガイド
  • Solve Your Child's Sleep Problems
  • 夢をかなえるゾウ4 ガネーシャと死神
  • Slack: Getting Past Burnout, Busywork, and the Myth of Total Efficiency
  • Unit Testing Principles, Practices, and Patterns

漫画

日本にいる頃から読んでた漫画の最新刊等読んだ。Kindleアメリカにいながら日本の漫画読めるのありがたい。

MLの学習

https://www.coursera.org/learn/machine-learning 会社の同僚達と一緒にスタンフォード大学がCourseraで提供しているMLのオンラインコースでMLの基礎を学んだ。解説がすごく分かりやすく、ニューラルネットバックプロパゲーションとかSVMカーネルとか割と理解があやふやだったものがようやくちゃんと理解できた。

仕事

COVIDでリモートワークになった。子育てもしながら仕事ができてタイミングとしてはちょうどよかった。ただ1ベッドルームなので仕事部屋がなく、寝室の隅っこに小さい机と椅子で仕事しているのでモニタすら置くスペースもなく、あまり効率は良くない。来年は広いとこに引っ越して仕事部屋を確保しなければ。 チームには新しい人が2人入ってきて順調に拡大した。ただその分以前よりもマネジメント等に割く時間が増えて自分でコード書く時間は明らかに減ってきた。チームとしては成長してるけれども、個人のキャリアとして今後マネジメントに比重を置いていくかどうか悩ましい所。

QuiverのノートブックをInkdropにインポート

Inkdropというマークダウンエディタについての記事がバズっていたので興味を持ちました。

Markdownエディタを作って月15万円稼ぐまでにやったこと — Inkdrop

今はQuiverを使っています。デスクトップアプリの出来はすごくいいんですが、iOSアプリが全然使いものにならないのが不満でした。InkdropはiOSAndroidアプリ両方揃っているので、試しに使ってみようと思いました。

InkdropはHTMLのインポートに対応していますが、Quiverの出力するHTMLファイルをそのままインポートすると、タイトルが本文に現れたりNext、Prevのようなリンクが挿入されたりとイマイチなので、これらの邪魔なものを削除して綺麗なHTMLに出力するシェルスクリプトを書きました。

#!/usr/bin/env bash

SEARCH_DIR="default"
OUTPUT_DIR="output"

rm -rf ${OUTPUT_DIR}
mkdir -p ${OUTPUT_DIR}/${SEARCH_DIR}

for note in "${SEARCH_DIR}"/*
do
LINES=`wc -l < "$note" | bc`
START=`expr $LINES - 9`
END=`expr $LINES - 6`
cat "$note" | sed -e 26,29d | sed -e "$START,${END}d" > "$OUTPUT_DIR/$note"
done

上記スクリプトを使用したQuiverのノートブックをInkdropにインポートする手順です。
1. Quiverでノートブックを選択してExport Notebook -> As HTML でノートブックを出力します(ここではdefaultノートブックを出力するとします)
2. 出力されたHTMLファイルが含まれたdefaultというディレクトリができていると思うので、そのディレクトリと同じ階層に上記スクリプトを置きます(ここではfilter-html-notes.shとします)
3. ノートブックがdefault以外の場合、スクリプトのSEARCH_DIRを出力されたディレクトリ名に変更して下さい
4. ターミナルを開いて上記ディレクトリ、スクリプトのあるディレクトリに移動します
5. chmod +x filter-html-notes.shで実行権限を付与します
6. ./filter-html-notesで実行します
7. うまくいくとoutputというディレクトリができ、その中に不要なものがフィルタされたHTMLファイルが入っています
8. Inkdropを開き、File -> Import -> from HTML files... で上記HTMLファイルをインポートします

Quiverのタグの情報は失われてしまうので、もっといい方法があるといいんですが。。

クラウドサービスで利用可能なGPUインスタンス調査

クラウド上でchainerを高速に動かすためにクラウドサービスのGPUインスタンスを調査してみました。

NVidiaの GPU Cloud ComputingのページGPUが利用可能なクラウドサービスが列挙されていますが、今回はAzureとAWS、さくらについて調査しました。
Google Cloud Platformも調べたのですがGPUインスタンスは現状なさげです。TensorFlow専用のサービスはあります。https://cloud.google.com/ml/

Azure

2016年8月にN-SeriesというGPUインスタンスが発表されました。
https://azure.microsoft.com/ja-jp/blog/azure-n-series-preview-availability/

GPUK80を使用しています。

The Tesla K80 delivers 4992 CUDA cores with a dual-GPU design, up to 2.91 Teraflops of double-precision and up to 8.93 Teraflops of single-precision performance. Following are the Tesla K80 GPU sizes available:

スペック

 

NC6

NC12

NC24

Cores

6

(E5-2690v3)

12

(E5-2690v3)

24

(E5-2690v3)

GPU

1 x K80 GPU (1/2 Physical Card)

2 x K80 GPU (1 Physical Card)

4 x K80 GPU (2 Physical Cards)

Memory

56 GB

112 GB

224 GB

Disk

380 GB SSD

680 GB SSD

1.44 TB SSD

価格

https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/

Instance Cores RAM Disk sizes Price
NV6 6 56.00 GB 340 GB ¥63.24/hr
NV12 12 112.00 GB 680 GB ¥126.48/hr
NV24 24 224.00 GB 1,440 GB ¥252.96/hr
NC6 6 56.00 GB 340 GB ¥57.12/hr
NC12 12 112.00 GB 680 GB ¥115.26/hr
NC24 24 224.00 GB 1,440 GB ¥229.50/hr
NC24r 24 224.00 GB 1,440 GB ¥258.06/hr

NC24rは高スループットのネットワークインターフェースを持つらしい。

Additionally there is a second low latency, high-throughput network interface (RDMA) optimized VM configuration (NC24r) which is tuned for tightly coupled parallel computing workloads.

AWS

2016年9月に新しいGPU特化のインスタンス P2が登場しました。

https://aws.amazon.com/jp/blogs/news/new-p2-instance-type-for-amazon-ec2-up-to-16-gpus/

GPUはN-Seriesと同じK80を使用しています。

スペック

https://aws.amazon.com/ec2/instance-types/

P2 instances are intended for general-purpose GPU compute applications.

Features:

High Frequency Intel Xeon E5-2686v4 (Broadwell) Processors
High-performance NVIDIA K80 GPUs, each with 2,496 parallel processing cores and 12GiB of GPU memory
Supports GPUDirect™ (peer-to-peer GPU communication)
Provides Enhanced Networking using the Amazon EC2 Elastic Network Adaptor with up to 20Gbps of aggregate network bandwidth within a Placement Group
EBS-optimized by default at no additional cost

Model

GPUs

vCPU

Mem (GiB)

GPU Memory (GiB)

p2.xlarge

1

4

61

12

p2.8xlarge

8

32

488

96

p2.16xlarge

16

64

732

192

価格

オンデマンドインスタンス
https://aws.amazon.com/ec2/pricing/on-demand/

US East (N. Virginia)
p2.xlarge41261EBS Only$0.9 per Hour
p2.8xlarge3294488EBS Only$7.2 per Hour
p2.16xlarge64188732EBS Only$14.4 per Hour

スポットインスタンス
https://aws.amazon.com/ec2/spot/pricing/

スポットインスタンスの仕組み解説ブログ
http://qiita.com/megadreams14/items/766ce04ca6cb418e95ca

US East (N. Virginia)
p2.xlarge$0.2831 per Hour$0.2947 per Hour
p2.8xlarge$4.1064 per Hour$3.536 per Hour
p2.16xlarge$144.0 per Hour$173.44 per Hour

さくら

さくらインターネット、高火力コンピューティングの機械学習向けGPUサービスを提供開始
https://www.sakura.ad.jp/press/2016/0831_koukaryoku/

スペック&料金

https://www.sakura.ad.jp/koukaryoku/specification/

Quad GPUモデル、Teslaモデルの2種類あります。

サービス名さくらの専用サーバ 高火力シリーズ
モデル名Quad GPU モデルTesla モデル
料金
※1
月額料金
※2
85,000
(税込:91,800円)
95,000
(税込:102,600円)
初期費用
735,000円(税込:793,800円)
(分割:61,250円×12回)
795,000円(税込:858,600円)
(分割:66,250円×12回)
CPUXeon E5-2623 v3 4Core × 2CPU
(8C/16T 3.0GHz 最大3.5GHz)
GPUカード標準GeForce GTX TITAN X
(Maxwell アーキテクチャ) ×4
NVIDIA Tesla M40 ×1
オプション-最大4枚まで増設可能
メモリ標準128GB
オプション最大1TBまで増設可能
ストレージ標準SSD 480GB
2台/1組(RAID1)
オプション 最大8台まで増設可能(標準2台+6台追加可能)
グローバル回線標準100Mbps(ベストエフォート)
オプション帯域増加、優先制御可能
冗長構成※3標準提供
ローカル回線標準10Gbps
冗長構成※3標準提供
OS標準OSUbuntu 14.04 (64bit)
カスタムOSCentOS 7 (64bit) / Ubuntu 16.04 (64bit)

まとめ

スペックはAWSのp2.16xlargeが一番いいですが、その分値段も張ります(1ヶ月フルに動かすと100万超える・・・)。


Azure (NC24)
AWS (p2.16xlarge)
さくら (Tesla)
K80
K80
M40
GPU
4
16
1 (最大4枚まで増設可能)
CPU
Xeon E5-2690v3
Xeon E5-2686v4
Xeon E5-2623 v3
CPUコア数
24
64
8
GPUメモリ
48 GB
192 GB
12 GB
メモリ
224 GB
732 GB
128 GB (最大1TBまで増設可能)
料金
229.50円/hour (165,240円/30日)
オンデマンド 1,512円/hour (1,088,640円/30日)
スポット 460.32円/hour (331,430.4円/30日)
$1=105円換算
税込:102,600円 (月額)

QEventLoopでイベントループを自在に操る

この記事は Qt Advent Calender 2015 の14日の記事です。
QEventLoopクラスを使って非同期処理を同期的に扱う方法を紹介したいと思います。

QEventLoop はQtのイベントループを扱うクラスです。イベントループについては2日目の記事で分かりやすく解説されているので、詳しくない方はまずはこちらを参考にして下さい。

QEventLoop はQApplication::execやQDialog::execなどexecというメソッドを持つクラスの中で使われており、その中でQEventLoop::execが呼ばれて実際にイベントループが実行されます。QEventLoopはpublicなクラスなのでユーザのコードからも呼ぶことができ、これを使うことで非同期処理を同期な方法で呼ぶことができるようになります。実際に例を見てみましょう。

まずはQEventLoopを使わずにシグナルとスロットを用いて非同期処理をシーケンシャルに行うサンプルです。

#include 
#include
#include
#include

class SleepThread : public QThread {
protected:
void run() {
qDebug() << "doing heavy task";
sleep(1);
}
};

int main(int argv, char** args) {
QApplication app(argv, args);

SleepThread* thread1 = new SleepThread();
QObject::connect(thread1, &QThread::finished, [&] {
SleepThread* thread2 = new SleepThread()
;
QObject::connect(thread2, &QThread::finished, [&] {
SleepThread* thread3 = new SleepThread()
;
QObject::connect(thread3, &QThread::finished, [&] {
qDebug()
<< "finished all tasks!";
app.exit();
});
thread3->start();
});
thread2->start();
});
thread1->start();
app.exec();
}

以下の様な出力になります。’doing heavy task’は1秒毎に出力されます。

doing heavy task
doing heavy task
doing heavy task
finished all tasks!

SleepThreadはrunの中で何か重い処理をしていると仮定して、ここでは単に1秒スリープしています。SleepThreadのfinishedシグナルにC++11のラムダ関数をconnectしてスレッドの処理が終了したらラムダ関数が実行され、次のSleepThreadを生成・実行しています。3つ全ての非同期処理が終わったら”finished all tasks!”を表示して終了します。
ご覧のとおりネストが深くなって少々読みづらいですよね。これをQEventLoopを使って同期的に書き直してみます。

#include 
#include
#include
#include
#include

class SleepThread : public QThread {
protected:
void run() {
qDebug() << "doing heavy task";
sleep(1);
}
};

int main(int argv, char** args) {
QApplication app(argv, args);

SleepThread* thread1 = new SleepThread();
QEventLoop loop1;
QObject::connect(thread1, &QThread::finished, &loop1, &QEventLoop::quit);
thread1->start();
loop1.exec();

SleepThread* thread2 = new SleepThread();
QEventLoop loop2;
QObject::connect(thread2, &QThread::finished, &loop2, &QEventLoop::quit);
thread2->start();
loop2.exec();

SleepThread* thread3 = new SleepThread();
QEventLoop loop3;
QObject::connect(thread3, &QThread::finished, &loop3, &QEventLoop::quit);
thread3->start();
loop3.exec();

qDebug() << "finished all tasks!";
}

出力はもちろんさっきと同じです。
QThead::finishedシグナルをQEventLoop::quitスロットにconnectして、スレッドが終了したらイベントループも終了するようにしています。その後QEventLoop::execを実行してイベントループに入ります。こうすることで非同期処理を行っているにも関わらず同期的に記述でき、プログラムもすっきりしました。さらにこの処理を関数として抽出すればもっと読みやすくできそうです。
QEventLoop::execを実行した後は見かけ上そこで処理が止まったようになりますが、実際はイベントループが回っているのでマウスやキーボードのイベントもその中で通常通り処理され、フリーズしたりはしません。

ネストしたイベントループ

このようにQEventLoopを使うことでいつでもイベントループを実行できることを確認しました。次はイベントループを実行した中でさらにイベントループを実行する例を見てみます。

main.cpp

#include <QApplication>
#include <QDebug>
#include <QObject>
#include "SleepThread.h"

int main(int argv, char** args) {
QApplication app(argv, args);

SleepThread* thread = new SleepThread();
QObject::connect(thread, &QThread::finished, &app, &QApplication::quit);
thread->moveToThread(thread);
thread->start();
QMetaObject::invokeMethod(thread, "sleep", Q_ARG(unsigned long, 3000));
QMetaObject::invokeMethod(thread, "sleep", Q_ARG(unsigned long, 5000));
app.exec();
}

main関数はSleepThread(先ほどのSleepThreadとは違います)を作成し、finishedシグナルにQApplication::quitをconnectしています。その後thread->moveToThread(thread) でthreadオブジェクトのthread affinityをSleepThreadに移します。
その後thread->start()でスレッドのイベントループを実行します。(今回QThread::runをオーバーライドしていないので、デフォルト動作のQThread::execが実行されます。)
QMetaObject::invokeMethodでSleepThreadのsleepメソッドを非同期で呼び出します。先ほどthread->moveToThread(thread) でthreadオブジェクトのthread affnityをSleepThreadに移したので、invokeMethodを呼び出した時にQt::QueuedConnectionが使われます。invokeMethodの詳細はQtのドキュメントを確認して下さい。
http://doc.qt.io/qt-5/qmetaobject.html#invokeMethod

invokeMethodでSleepThreadのsleepメソッドを2回非同期で呼び出した後app.exec()でQApplicationのイベントループに入ります。
次はSleepThreadの中身を見てみます。

SleepThread.h

#pragma once

#include
#include
#include

class SleepThread : public QThread {
Q_OBJECT
public slots:
void sleep(unsigned long msec) {
QEventLoop loop;
qDebug() << "start timer with" << msec << "[msec]";
QTimer::singleShot(msec, &loop, [&loop, msec]{
qDebug() << "finished waiting" << msec << "[msec]";
loop.quit();
});
loop.exec();
qDebug() << "event loop of" << msec << "[msec] finished";
exit();
}
};

sleepメソッドはQEventLoopでイベントループを実行しています。QTimer::singleShotを使ってmsecミリ秒後にイベントループを終了しています。今回なぜsleep(msec)としなかったかというと、sleep(msec)は現在のスレッドをブロックしてしまうため、SleepThreadで回っているイベントループを止めてしまうためです。
イベントループ終了後QThread::exitを呼び出してスレッドを終了します。

出力は以下のようになります。

start timer with 3000 [msec]
start timer with 5000 [msec]
finished waiting 3000 [msec]
finished waiting 5000 [msec]
event loop of 5000 [msec] finished
event loop of 3000 [msec] finished

イベントループはスレッドごとに存在していて、thread->start()でSleepThreadのイベントループが始まります。
次にQMetaObject::invokeMethod(thread, "sleep", Q_ARG(unsigned long, 3000)); を呼び出すとQt::QueuedConnectionの時に内部ではQMetaCallEventというイベントがSleepThreadのイベントループで発生し、sleep(3000) の形でメソッドが呼ばれます。sleepメソッドの中ではさらにQEventLoop::execでイベントループを実行しているため、イベントループがネストした形になります。
その後QMetaObject::invokeMethod(thread, "sleep", Q_ARG(unsigned long, 5000)); でsleep(5000)が実行され、さらに内部でイベントループが実行されます。

QThread::exec()で作られた親イベントループ
sleep(3000)の中で作られた子イベントループ
sleep(5000)の中で作られた孫イベントループ

このように3つイベントループがネストしていますが、実際にイベントを処理するのは常に一番深いイベントループで、他のイベントループは止まった状態になっています。この例ではsleep(3000)で作られた子イベントループの方がタイムアウト時間が短いので先にloop.quit(); が呼ばれますが、QEventLoop::quitを呼んでもこの子イベントループは止まっているので実際には即座には終了しません。その2秒後にsleep(5000)のQTimerのタイムアウトが発生しloop.quit(); が呼ばれ、sleep(5000)で作られた孫イベントループが終了します。
その後sleep(3000)の子イベントループが再開しますが、すでに孫イベントループの中でloop.quit()が呼ばれているので即座に終了します。
そしてようやく親イベントループが再開しますが、こちらもすでにsleepメソッドの中でQThread::exitが呼ばれているので、再開しても即座に終了します。

シグナル・スロットを用いた非同期処理とQEventLoopは相性がいいので、色々な場面で役立つこと間違いなしです!
明日はasobotさんのQtCreatorとqmakeについてです。

参考

アロー関数はコンストラクタとして使えない

util.inheritsでアロー関数を渡すとエラーになるので調査していたところ、アロー関数はコンストラクタとして使えないことが分かった。
原因はアロー関数のprototypeがundefinedになるため。

再現コード

'use strict';
var util = require('util');
var EventEmitter = require('events');

function InputDialog() { }

console.log('InputDialog prototype: ' + InputDialog.prototype)
util.inherits(InputDialog, EventEmitter);

const InputDialog2 = () => { }

console.log('InputDialog2 prototype: ' + InputDialog2.prototype)
util.inherits(InputDialog2, EventEmitter);

出力

InputDialog prototype: [object Object]
InputDialog2 prototype: undefined
util.js:764
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
^

TypeError: Object.setPrototypeOf called on null or undefined
at Function.setPrototypeOf (native)
at Object.exports.inherits (util.js:764:10)
at Object. (/Users/shinichi/code/silkedit/test.js:13:6)
at Module._compile (module.js:425:26)
at Object.Module._extensions..js (module.js:432:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:311:12)
at Function.Module.runMain (module.js:457:10)
at startup (node.js:136:18)
at node.js:972:3

util.inheritsのコード
https://github.com/nodejs/node/blob/v5.1.0/lib/util.js

exports.inherits = function(ctor, superCtor) {

if (ctor === undefined || ctor === null)
throw new TypeError('The constructor to `inherits` must not be ' +
'null or undefined.');

if (superCtor === undefined || superCtor === null)
throw new TypeError('The super constructor to `inherits` must not ' +
'be null or undefined.');

if (superCtor.prototype === undefined)
throw new TypeError('The super constructor to `inherits` must ' +
'have a prototype.');

ctor.super_ = superCtor;
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
};

参考

Arrow functions versus normal functions
An arrow function is different from a normal function in only three ways: First, it always has a bound this. Second, it can’t be used as a constructor: There is no internal method Construct (that allows a normal function to be invoked via new) and no property prototype. Therefore, new (() => {}) throws an error. Third, as arrow functions are an ECMAScript.next-only construct, they can rely on new-style argument handling (parameter default values, rest parameters, etc.) and don’t support the special variable arguments. Nor do they have to, because the new mechanisms can do everything that arguments can.

Node.jsのバージョン間での挙動の違い

ちなみにv0.12.0はアロー関数をコンストラクタとしてutil.inheritsに渡しても動作する。
v5.1.0ではダメ。