Go とシグナルの検出

今日はとっても簡単なお題なのだけど、ちゃんと理解していないトピックとして、Go 言語でのシグナルのハンドリングについて書いてみたい。

# お題

タイマーと、go routine　と、シグナルの割り込みのいづれか一番早く発生した内容表示して、プログラムを終了する。シグナルの場合はシグナルの内容を表示する。

# シグナルのハンドリング

次のサンプルがほぼすべてになります。

```go 
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)

s := <-sig
fmt.Printf("Signal received: %s \n", s.String())
```

`os.Signal` のチャネルを作成します。その後、`signal.Notify`で監視すべきシグナルを列挙します。すると、そこに列挙されたシグナルが発生すると、作成したチャネルにメッセージが通知されます。`os.Signal` の Struct は `Value()` と、`String()` を持っていますが、`Value()`は戻りが無いので、`String()` を使います。

# シグナルの種類

理解があいまいなポイントとして、シグナルの理解があります。全部理解するのは大変なので、ざっくりと代表的なものだけでも理解しておきましょう。

* [Common Signals](https://bencane.com/stories/2020/08/17/signal-traps/#/5/0/4)

| シグナル | 解説 |　発生のさせ方 |
|--------- | --------- | ------ |
| SIGHUP | ターミナルの終了 | terminal を終わらせる, kill -1 process_id |
| SIGINT | プロセスの停止 | ctr+c | ctr+c, kill -2 process_id |
| SIGQUIT | プロセスを停止させてコアダンプをはく | ctr+\ , kill -3 process_id |
| SIGKILL | プロセスの強制終了 | kill -9 process_id |
| SIGTERM | デフォルトシグナル。通常はプロセスの停止を表す。| kill process_id or kill -15 process_id |

SIGHUP は元々は、`HANGUP` つまり電話を切ることで、昔はモデルとかで通信していたのでその名残で、現在は、ターミナル殺したときに発生します。

# サンプルプログラム

次のような簡単なプログラムで実行してみます。プログラムを停止させる時に、いきなり落とすのではなく、終了処理をしてからGracefulに終わってほしいと思う場面は多いでしょう。そういう場面を想定して書いています。go routine でシグナルを待っていますが、３０秒たったら終わるようにしています。`Shutdown gracefully ` の箇所にそういう処理を入れ込めばいい感じです。

_main.go_

```go
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	fmt.Println("Start process....")
	go func() {
		trap := make(chan os.Signal, 1)
		signal.Notify(trap, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT)
		s := <-trap
		fmt.Printf("Received shutdown signal %s\n", s)
		fmt.Printf("Shutdown gracefully....\n")
		os.Exit(0)
	}()
	fmt.Println("Waiting for the signal....")
	time.Sleep(30 * time.Second)
}

```

## 実行

```go
$ go build main.go -o signal
$ ./signal > aaa.txt
```

ここで、ターミナルを閉じる

```bash
$ cat aaa.txt 
Start process....
Waiting for the signal....
Received shutdown signal hangup
Shutdown gracefully....
```

同様の結果は、

```bash
$ ./signal
```

別ターミナル

```bash
$ ps -ef | grep signal
ushio    29566 31751  0 19:00 pts/13   00:00:00 ./signal
ushio    29635 29030  0 19:00 pts/14   00:00:00 grep --color=auto signal
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill -1 29566
``` 
最初のターミナル

```bash
$ ./signal 
Start process....
Waiting for the signal....
Received shutdown signal hangup
Shutdown gracefully....
```

# go run main.go にご注意

このようなシグナルのプログラムをテストしている時に一つだけ注意事項があります。

```bash
$ go run main.go
Start process....
Waiting for the signal....
```

上記のような形で、プログラムをランして、`SIG_TERM` を発生させます。

```bash
$ ps -ef | grep go 
ushio      186    46  0 Feb05 pts/0    00:03:15 /home/ushio/go/bin/gopls -mode=stdio
ushio      547 32362  0 15:35 pts/0    00:00:11 /home/ushio/go/bin/gopls -mode=stdio
ushio     3932 31751  0 19:15 pts/13   00:00:00 go run main.go
ushio     4042  3932  0 19:15 pts/13   00:00:00 /tmp/go-build777782526/b001/exe/main
ushio     4158 29030  0 19:15 pts/14   00:00:00 grep --color=auto go
ushio    20332 20134  0 15:08 pts/0    00:00:33 /home/ushio/go/bin/gopls -mode=stdio
ushio    25461 25267  0 15:22 pts/0    00:00:20 /home/ushio/go/bin/gopls -mode=stdio
ushio    28569 28174  0 15:28 pts/0    00:00:45 /home/ushio/go/bin/gopls -mode=stdio
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill 3932
```

あれ、単に終了して、graceful shutdown が走りません。

```bash
$ go run main.go
Start process....
Waiting for the signal....
Terminated
```

理由は簡単で、`go run` のコマンドの実体は、`go build -o /tmp/something && ./tmp/something` に相当します。上記の `ps -ef` の `go run main.go` のほかに、`/tmp/go-build777782526/b001/exe/main` が動いています。`go run main.go` の方は、単なる go のSDKであり、実態の方は、`/tmp/..../main` の方なので、そちらの方にシグナルを送らないといけないので、間違えないでください。`/tmp/.../main` の方にシグナルを送るとうまくいきます。

```bash
$ ps -ef | grep go
ushio      186    46  0 Feb05 pts/0    00:03:16 /home/ushio/go/bin/gopls -mode=stdio
ushio      547 32362  0 15:35 pts/0    00:00:12 /home/ushio/go/bin/gopls -mode=stdio
ushio     6559 31751  1 19:20 pts/13   00:00:00 go run main.go
ushio     6663  6559  0 19:20 pts/13   00:00:00 /tmp/go-build604160077/b001/exe/main
ushio     6741 29030  0 19:20 pts/14   00:00:00 grep --color=auto go
ushio    20332 20134  0 15:08 pts/0    00:00:33 /home/ushio/go/bin/gopls -mode=stdio
ushio    25461 25267  0 15:22 pts/0    00:00:20 /home/ushio/go/bin/gopls -mode=stdio
ushio    28569 28174  0 15:28 pts/0    00:00:46 /home/ushio/go/bin/gopls -mode=stdio
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill 6663
```

別ターミナル

```bash
$ go run main.go
Start process....
Waiting for the signal....
Received shutdown signal terminated
Shutdown gracefully....
```

# お題のプログラミングの解答

こんなものを作ってみました。

```go
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	timer := time.NewTimer(20 * time.Second)
	finished := make(chan bool)
	sigterm := make(chan os.Signal, 1)
	signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP, syscall.SIGKILL)
	go func() {
		time.Sleep(21 * time.Second)
		finished <- true
	}()

	select {
	case <-timer.C:
		fmt.Println("Timer wins!")
	case signal := <-sigterm:
		fmt.Printf("Signal wins! Signal %s\n", signal.String())
	case <-finished:
		fmt.Println("Go routine wins!")
	}

}
```

バリエーションは多くないので、このあたりのプログラムはこれで大体さっと書ける感じです。シグナルをもっと拾いたいときは、その都度シグナルの意味を調べてみましょう。