ゴルーチンの同期

チャネルを使ってゴルーチンの同期をとることができる。
次のコードを見てほしい。

package main

import (
    "fmt"
    "time"
)

func makeRoutine(code string, in <-chan int) chan int {
    out := make(chan int)
    go func() {
        for {
            <- in fmt.Print(code)
            time.Sleep(200 * time.Millisecond) out <- 0
        }
    }()
    return out
}

func main() {
    ch1 := make(chan int)
    ch2 := makeRoutine("h", ch1)
    ch3 := makeRoutine("e", ch2)
    ch4 := makeRoutine("y", ch3)
    ch5 := makeRoutine("!", ch4)
    ch6 := makeRoutine(" ", ch5)
    for i := 0; i < 10; i++ {
        ch1 <- 0 <- ch6
    }
}

makeRoutine 関数は、文字列 code とチャネル in を引数にとる。まずチャネル out を作っておいて、無名関数をゴルーチンで起動、最後に out を返している。無名関数は無限ループになっていて、in から何か送信されてくるのを待って code を出力し、200 ミリ秒待ってから out にデータを送信する。
main 関数では、最初の(送信用)チャネル ch1 を作り、makeRoutine 関数に渡している。返ってきたチャネルは、次の makeRoutine 関数に渡され、返ってきたチャネルはさらに次の makeRoutine 関数に渡される。こうして5つの makeRoutine 関数の中のゴルーチンがチャネルを通じて数珠つなぎのようになる。これらのゴルーチンは、最初のチャネル ch1 にデータを送信することによって動作を開始し、最後のチャネル(main 関数から見ると ch6)にデータを送信して終わる。これを10回繰り返している。

実行してみよう。

^o^ > go run go_hey.go
hey! hey! hey! hey! hey! hey! hey! hey! hey! hey!

この実行例ではわからないけど、1文字ずつ時間を空けて hey! の文字列が10回出力されている。各ゴルーチンは1文字出力するだけなので、うまく同期して動作している様子がわかる。

sync.WaitGroup

ゴルーチンの終了待ちには、チャネルを使うほかに sync パッケージの WaitGroup を使う方法もある。
使い方はこうだ:

  1. sync.WaitGroup の変数を作る
  2. その変数に、終了待ちをするゴルーチンの数を設定する
  3. ゴルーチンを呼び出す。このとき、sync.WaitGroup の変数を渡す
  4. ゴルーチン側では、終了したら Done 関数を呼ぶ
  5. メインルーチン側で、Wait 関数を呼ぶ

実際に試してみよう。

package main

import (
    "fmt"
    "time"
    "sync"
)

func test(n int, name string, wg *sync.WaitGroup) {
    for i := 0; i < n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup wg.Add(3)
    go test(6, "foo", &wg)
    go test(4, "bar", &wg)
    go test(8, "baz", &wg) wg.Wait()
}
^o^ > go run go_waitgroup.go
0 baz
0 bar
0 foo
1 baz
1 bar
1 foo
2 baz
2 bar
2 foo
3 baz
3 bar
3 foo
4 baz
4 foo
5 baz
5 foo
6 baz
7 baz

チャネルとゴルーチンの終了待ち

チャネルはゴルーチンの間で通信するためのデータだ。次のように生成する。

ch := make(chan T, bufsize)

T はチャネルでやり取りするデータの型、bufsize はデータを格納するバッファのサイズで省略すると 0 になる。チャネルの型は chan T。
関数の引数や変数の型指定の時、chan の前に <- をつけると受信専用に、後に <- をつけると送信専用になる。 チャネルを使うと、ゴルーチンの終了待ちができるようになる。 次の例では test 関数をゴルーチンとして呼び出し、チャネルを渡している。test 関数は 0.5 秒間隔で name を出力し、終了するときにチャネルを通じて name を送ってくる。main 関数ではチャネルからデータが送られてくるのを待っている。

package main

import (
    "fmt"
    "time"
)

func test(n int, name string, c chan<- string) {
    for i := 1; i <= n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
    c <- name
}

func main() {
    c := make(chan string)
    go test(6, "foo", c)
    go test(4, "bar", c)
    go test(8, "baz", c)
    for i := 0; i < 3; i++ {
        name := <- c fmt.Println(name)
    }
}

実行してみよう。

^o^ > go run go_channel.go
1 foo
1 baz
1 bar
2 foo
2 baz
2 bar
3 foo
3 baz
3 bar
4 foo
4 baz
4 bar
5 foo
5 baz
bar
6 foo
6 baz
foo
7 baz
8 baz
baz

数字とともに出力されているのが test 関数内で出力したもの、数字のないのがゴルーチンが終了した後に main 関数で出力したものだ。3つのゴルーチンが並行して動き、main 関数ではその終了を待っていることがわかる。

reverse

Go でスライスを逆順にしたかったんだけど、そういう関数は用意されてないようだ。じゃあどうするかというと for 文を使ってひとつずつ入れ替えてくしかないみたい。こんなふうに。

package main

import (
    "fmt"
)

func main() {
    s := []int{ 1,2,3,4,5 }
    for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 {
        s[i], s[j] = s[j], s[i]
    }
    fmt.Println(s)
}
^o^ > go run reverse.go
[5 4 3 2 1]

文字列を逆順にするにはいったん rune 型のスライスにしてから。

package main

import (
    "fmt"
)

func main() {
    s := "あいうえお"

    runes := []rune(s)
    for i, j := 0, len(runes) - 1; i < j; i, j = i + 1, j - 1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    fmt.Println(string(runes))
}
^o^ > go run reverse_string.go
おえういあ

MediaWikiでインターウィキの編集を出来るようにする

MediaWiki では、デフォルトでインターウィキ機能が使えるようになっているけど、接頭辞の追加とか編集とかはデータベースを直接操作する必要があるらしい。
そこでエクステンション(拡張機能)を有効にして、特別ページでインターウィキの編集を出来るようにする。

具体的には LocalSettings.php ファイルの末尾に次のように追記する。

require_once('extensions/Interwiki/Interwiki.php');
$wgGroupPermissions['*']['interwiki'] = false;
$wgGroupPermissions['sysop']['interwiki'] = true;

こうすることで、管理者でログインすると、特別ページに「インターウィキデータの閲覧と編集」というページができて、インターウィキの追加・編集・削除ができるようになる。

Nginxを使って仮想サーバーをたてる

先日作った Sinatra アプリに、sombrero という仮想サーバー名でアクセスできるようにする。
Nginx の仮想サーバーの設定は、/etc/nginx/conf.d 以下に「仮想サーバー名.conf」という設定ファイルを作ることで行う。
というわけで、今回は /etc/nginx/conf.d/sombrero.conf ファイルを作る。

[takatoh@aybesea ~]$ cd /etc/nginx/conf.d
[takatoh@aybesea conf.d]$ sudo vim sombrero.conf

ファイルの内容は次の通り:

upstream unicorn-sombrero {
    server 127.0.0.1:9000;
}

server {
    # port
    listen 80;

    # server name
    server_name sombrero;

    client_max_body_size 8M;

    # log files
    access_log /var/log/nginx/sombrero/access.log combined;
    error_log /var/log/nginx/sombrero/error.log warn;

    keepalive_timeout 60;
    proxy_connect_timeout 60;
    proxy_read_timeout 60;
    proxy_send_timeout 60;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://unicorn-sombrero;
    }
}

80 番ポートで待ち受けて、アプリの動いている 9000 番ポートへ転送している。

ログファイルのためのディレクトリを作る。

[takatoh@aybesea conf.d]$ sudo mkdir /var/log/nginx/sombrero

これで OK のはず。Nginx をリスタート。

[takatoh@aybesea conf.d]$ sudo systemctl restart nginx

別のマシンから http://sombrero/ にアクセスしてみると、ちゃんと動作していることが確認出来た。

CentOS 7にNginxをインストールする

Nginx は CentOS 7 の標準リポジトリでは提供されていないので、まずは EPEL リポジトリを登録する。

[takatoh@aybesea ~]$ sudo yum install epel-release.noarch

Nginx をインストール。

[takatoh@aybesea ~]$ sudo yum install nginx

firewalld の設定とリスタート。

[takatoh@aybesea ~]$ sudo firewall-cmd --add-service=http --zone=public --permanent
[takatoh@aybesea ~]$ sudo systemctl restart firewalld

Nginx の起動と自動起動の設定。

[takatoh@aybesea ~]$ sudo systemctl enable nginx
[takatoh@aybesea ~]$ sudo systemctl start nginx

これでとりあえずは OK。他のマシンからもアクセスできることを確認した。

SinatraアプリをCentOSで動かす(2)

前のエントリで、単純に動かすことには成功したので、今度はデーモンとして動かすことにチャレンジする。一部内容が重複するけど御勘弁。

まずは専用のユーザーを作る。

[takatoh@aybesea ~]$ sudo useradd sombrero
[sudo] takatoh のパスワード:
[takatoh@aybesea ~]$ sudo passwd sombrero
ユーザー sombrero のパスワードを変更。
新しいパスワード:
よくないパスワード: このパスワードには一部に何らかの形でユーザー名が含まれています。
新しいパスワードを再入力してください:
passwd: すべての認証トークンが正しく更新できました。
[takatoh@aybesea ~]$ sudo usermod -G wheel sombrero
[takatoh@aybesea ~]$ sudo usermod -aG staff sombrero

よくないパスワードと言われてるけど無視。
ここで新しいユーザーでログインし直す。

アプリの clone から セットアップまで。

[sombrero@aybesea ~]$ git clone https://github.com/takatoh/sombrero.git
[sombrero@aybesea ~]$ cd sombrero

と思ったら bundler のインストールでつまづいた。

[sombrero@aybesea sombrero]$ gem install bundler
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /usr/local/rbenv/versions/2.4.3/lib/ruby/gems/2.4.0 directory.

指摘されているディレクトリのパーミッションを見てみると、bill のものになっている。

[sombrero@aybesea sombrero]$ ls -l /usr/local/rbenv/versions/2.4.3/lib/ruby/gems
合計 0
drwxr-xr-x. 8 bill bill 100  1月  2 10:11 2.4.0

これは bill で rbenv global したせいだろうけど、このパーミッション設定は良くないよな。
変えてしまえ。

[sombrero@aybesea sombrero]$ sudo chgrp -R staff /usr/local/rbenv/versions/2.4.3
[sombrero@aybesea sombrero]$ sudo chmod -R 775 /usr/local/rbenv/versions/2.4.3

これでどうか。

[sombrero@aybesea sombrero]$ gem install bundler
Fetching: bundler-1.16.1.gem (100%)
Successfully installed bundler-1.16.1
Parsing documentation for bundler-1.16.1
Installing ri documentation for bundler-1.16.1
Done installing documentation for bundler after 5 seconds
1 gem installed

OK。つづき。

[sombrero@aybesea sombrero]$ bundle install
[sombrero@aybesea sombrero]$ cp config.yaml.example config.yaml
[sombrero@aybesea sombrero]$ rake setup

ここでいったん起動してみる。

[sombrero@aybesea sombrero]$ bundle exec rackup config.ru

OK。

unicorn のインストール。

[sombrero@aybesea sombrero]$ bundle exec gem install unicorn

unicorn の設定ファイル unicorn.conf を書く。

listen "9000"
worker_processes 2
pid "./unicorn.pid"
stderr_path "./unicorn.log"
stdout_path "./unicorn.log"

もう一度、今度は unicorn で起動。

[sombrero@aybesea sombrero]$ bundle exec unicorn -c unicorn.conf
bundler: failed to load command: unicorn (/usr/local/rbenv/versions/2.4.3/bin/unicorn)
Gem::Exception: can't find executable unicorn for gem unicorn. unicorn is not currently included in the bundle, perhaps you meant to add it to your Gemfile?
  /usr/local/rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/bundler-1.16.1/lib/bundler/rubygems_integration.rb:458:in `block in replace_bin_path'
  /usr/local/rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/bundler-1.16.1/lib/bundler/rubygems_integration.rb:478:in `block in replace_bin_path'
  /usr/local/rbenv/versions/2.4.3/bin/unicorn:23:in `'

あれ?Gemfile に書き足さなきゃいけないのか。じゃ、そうしてからもう一度。

[sombrero@aybesea sombrero]$ bundle update
[sombrero@aybesea sombrero]$ bundle exec unicorn -c unicorn.conf

今度はOK。

UNIT ファイルを書く。

[sombrero@aybesea sombrero]$ sudo vim /etc/systemd/system/sombrero.service
[Unit]
Description=Sombrero service

[Service]
Type=forking
PIDFile=/home/sombrero/sombrero/unicorn.pid
ExecStart=/usr/local/rbenv/shims/bundle exec unicorn -c unicorn.conf -D
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -QUIT $MAINPID
WorkingDirectory=/home/sombrero/sombrero
User=sombrero
Group=sombrero

[Install]
WantedBy=multi-user.target

これでいってみよう。

[sombrero@aybesea sombrero]$ sudo systemctl start sombrero

ブラウザで確認……OK!!!!!
start したり stop したりしてもちゃんと動いていることが確認出来た。

最後は、自動起動するようにして、外部からもアクセスできるようにポートを開ければ完了。

[sombrero@aybesea sombrero]$ sudo systemctl enable sombrero
Created symlink from /etc/systemd/system/multi-user.target.wants/sombrero.service to /etc/systemd/system/sombrero.service.
[sombrero@aybesea sombrero]$ sudo firewall-cmd --add-port=9000/tcp --zone=public --permanent
success

これでマシンを再起動してみよう。

他のマシンから確認。ちゃんと動作していることが確認出来た。これで完了。

SinatraアプリをCentOSで動かす

ImageMagick を使うのでインストール。

[takatoh@aybesea ~]$ sudo yum install ImageMagick

GitHub から clone。

[takatoh@aybesea ~]$ cd w
[takatoh@aybesea w]$ git clone [email protected]:takatoh/sombrero.git

依存するライブラリをインストール。bundler はインストール済み。

[takatoh@aybesea w]$ cd sombrero
[takatoh@aybesea sombrero]$ bundle install

設定ファイルを作る。実験なのでデフォルトのまま。

[takatoh@aybesea sombrero]$ cp config.yaml.example config.yaml

アプリのセットアップ。データベースや必要なディレクトリなど。

[takatoh@aybesea sombrero]$ rake setup

これで一応の準備は済んだはず。rackup してみる。

[takatoh@aybesea sombrero]$ bundle exec rackup config.ru

これで localhost:9292 で待ち受ける。ブラウザで確認したら OK だった。

外部からアクセスするにはポートを開けてやる必要がある。

[takatoh@aybesea sombrero]$ sudo firewall-cmd --add-port=9292/tcp --zone=public --permanent
[sudo] takatoh のパスワード:
success

さらにアプリを起動するときにも -o 0.0.0.0 オプションが必要。これは Sinatra の仕様。

[takatoh@aybesea sombrero]$ bundle exec rackup -o 0.0.0.0 config.ru

これでアクセスできるはず……できないな。
firewalld をリスタートさせてみる。

[takatoh@aybesea sombrero]$ sudo systemctl restart firewalld
[sudo] takatoh のパスワード:
[takatoh@aybesea sombrero]$ sudo firewall-cmd --list-ports
9292/tcp

今度は大丈夫。外部からもアクセスできて、期待どおりに動作していることが確認出来た。

WindowsからSambaの共有フォルダにコピーするときエラーが発生するようになってしまった

Samba で共有フォルダを作っている Ubuntu マシン(wplj)の外付け HDD を ext4 にフォーマットしたところ、Windows からファイルをコピーするときに、頻繁にエラーが発生するようになってしまった。
そもそも wplj の共有フォルダは Windows マシンのデータバックアップに使っていたもので、もとは NTFS のまま使っていた。そこに robocopy コマンドでバックアップをとっていた。例えば次のように。

robocopy /mir F:\softwares P:\

F: は Windows マシンの外付け HDD、P: は wplj の共有フォルダをネットワークドライブとして割り当てたものだ。wplj の外付け HDD が NTFS のときはこれで何の問題もなくバックアップできていたんだけど、ext4 にしたら「予期しないネットワークエラー」が頻繁に発生するようになってしまった、というわけ。robocopy コマンドだけでなく、エクスプローラーを使ってコピーしてみても状況は同じだった。まったくコピーができないわけじゃないので、ますますわからない。Web を見ながらいくつか解決方法を試してみたけれど、状況は変わらず。それで困っている。

ところで、よくわからないんだけど、CentOS マシン(aybesea)にたてた Samba の共有フォルダでは、上記のようなエラーは起こらない。なぜだろう?