2018年4月30日月曜日

docker-compose を使って Sinatra を nginx 配下で動かす方法

概要

nginx を挟んでいたほうが何かと便利なので試してみました
最終的には docker-compose で nginx と Sinatra が上がってきます

$ tree . -I vendor
.
├── Dockerfile.web
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── docker-compose.yml
└── nginx.conf

環境

  • macOS 10.13.4
  • docker-compose 18.04.0-ce

Sinatra アプリの作成

  • vim config.ru
require './app.rb'
run MyApp
  • vim app.rb
require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/' do
    request.host
  end
end

アプリはリクエストのホストヘッダを返すだけです

  • vim Dockerfile
FROM ruby

ADD . /home
WORKDIR /home
RUN bundle install --path vendor

CMD ["bundle", "exec", "rackup", "config.ru", "-o", "0.0.0.0"]

nginx.conf の作成

今回の肝の部分かと思います

  • vim nginx.conf
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    upstream app {
        server web:9292; 
    }

    server {
        listen       80;
        server_name  localhost;

        location / {
            proxy_pass http://app;
            proxy_set_header Host $host;
        }
    }
    include servers/*;
}

ポイントは proxy_set_header Host $host; です
これがないと Sinatra で 404 エラーが発生した際に正常に表示されません
Sinatra はリクエストされた Host ヘッダを見て画像などの情報を取得します
もし上記の nginx.conf の設定がない場合 Host 情報が app となってしまい画像などを取得しにいくサーバも http://app となり正常に取得できません

なので Host ヘッダをちゃんと設定することで Sinatra が正常に画像などを取得できるようにします

docker-compose.yml の作成

  • vim docker-compose.yml
version: '2'
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.web
    expose:
      - "9292"
  proxy:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    links:
      - "web"

作成した nginx.conf を volumes を使って nginx コンテナ内の nginx.conf に上書きします
web 側のポート解放はホスト側の解放は不要なので expose を使っています
直接ホストから Sinatra にアクセスしたいのであれば ports を使うと良いと思います

動作確認

  • docker-compose up -d

で起動しましょう
nginx と Sinatra が上がってきます
nginx 側にアクセスすれば良いのです http://localhost にアクセスしましょう
するとそのまあプロキシして Sinatra アプリの情報が表示されると思います
また、http://localhost/hoge などにアクセスしてもちゃんと Sinatra のエラー画面が表示されることが確認できると思います

最後に

nginx + Sinatra を docker-compose で動かしてみました
今回 upstream を使っていますがなくても大丈夫です
将来的にアプリをポート分散などさせたい場合は upstream を定義したほうが良いかと思います

nginx を入れた理由としては同一マシン内で別アプリを動かした際に nginx でルーティングすることができるのと、同一マシンに別のドメインを振って VirtualHost の server_name でバランシングできることかなと思います
単一ホストだけで動作させている場合などには必須の構成かなと思います

ただこの構成にすることで nginx の管理、チューニングをする必要が出てくるのでそれはそれで面倒かなとも思います

2018年4月29日日曜日

Sinatra にベーシック認証を追加してみる

概要

Rack の機能を使って Sinatra でベーシック認証を発動させることができるようなので試してみました
Siantra だけでもベーシック認証が使えるので nginx などリバースプロキシが不要になります

環境

  • macOS 10.13.4
  • Ruby 2.4.1p111
  • sinatra 2.0.1

アプリ側でベーシック認証させるページを指定する

  • vim app.rb
class AdminPage < Sinatra::Base
  set :show_exceptions, false

  use Rack::Auth::Basic, "Are you Admin?" do |username, password|
    username == 'admin' && password == 'password'
  end

  get '/' do
    'ok'
  end
end

アプリ側でベーシック認証させないページを指定する

  • vim app.rb
class HomePage < Sinatra::Base
  set :show_exceptions, false

  get '/' do
    'ok'
  end
end

config.ru にルーティングを定義する

  • vim config.ru
require './app.rb'

run Rack::URLMap.new({
  "/" => HomePage,
  "/admin" => AdminPage
})

Rack::URLMap クラスを生成するときにルーティングのハッシュ情報を定義します
先ほど定義したクラスを値に指定しましょう

これでどうなるか

  • / には認証なしでアクセスできます
  • /admin にはベーシック認証が発動し認証に成功しないとアクセスできません

ちなみに /admin/hoge にアクセスしようとしてもベーシック認証が発動するのでサブディレクトリにも効いているようです

config.ru でトップディレクトリを指定するので、アプリ側で定義したルーティングはサブディレクトリになります

別のユーザを追加したい場合は認証の条件をハッシュなどにすれば OK です
別のページにもベーシック認証を追加したい場合は同じようにクラスを追加し、config.ru にルーティングを追加してあげましょう

最後に

Sinatra でベーシック認証を有効にする方法を紹介しました
認証に失敗した場合は真っ白なページが表示されます

認証ごとに Sinatra::Base を継承した class を作成する必要があるので not_founderror などは各クラス内でちゃんと定義してあげる必要があります

参考サイト

2018年4月28日土曜日

Sinatra でアップロードフォームを作成してみた

概要

タイトルの通りですがアップロードフォームを作成してみました

環境

  • macOS 10.13.4
  • Ruby 2.4.1p111
  • sinatra 1.4.8

必要なディレクトリ作成

  • mkdir -p public/img
  • mkdir views

アプリ作成

  • vim upload.rb
require 'sinatra'

class MyApp < Sinatra::Base
  post '/upload' do
    @filename = params[:file][:filename]
    file = params[:file][:tempfile]
    File.open("./public/img/#{@filename}", 'wb') do |f|
      f.write(file.read)
    end
    erb :complete
  end

  get '/' do
    erb :form
  end
end

ポイントは params[:file][:tempfile] でファイルの実体を取得して .read で読み込んで保存する部分です

起動ファイル作成

  • vim config.ru
require './upload.rb'
run MyApp

これで

  • bundle exec rackup config.ru

で起動します

テンプレートファイル作成

  • vim views/form.erb
<html>
<head>
  <title>upload</title>
</head>
<body>
  <form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="Upload image">
  </form>
</body>
</html>

ファイルを送信するので enctype="multipart/form-data" を使います

  • vim views/complete.erb
<html>
<head>
  <title>Complete</title>
</head>
<body>
  <h1>Show your uploaded image</h1>
  <img src="/img/<%= @filename %>" />
</body>
</html>

アップロードされたファイルを参照しているだけです
今回は画像がアップロードされることを想定しています

動作確認

起動してアップロードしてみましょう
sinatra_upload_form.gif

最後に

Sinatra でアップロードフォームを作成してみました
ただ、これだと進捗など確認できないかなと思います
また、JavaScript からやりたくなるケースがあると思うので jQuery からも試してみたいと思います

参考サイト

2018年4月27日金曜日

Google Tag Manager を使って Audio タグで再生したクリック数を調べる

概要

Audio タグは HTML5 で実装されたタグで mp3 などのオーディオファイルをブラウザ上で再生することができるタグです
そのタグの再生ボタンがどれくらい押されたかという情報は取りたくなると思います
今回は Google Tag Mangaer を使ってその Audio タグのクリック情報を取得する方法を紹介します

環境

  • macOS 10.13.4
  • Google Tag Manager 2018/04/23 時点
  • Google Analytics 2018/04/23 時点

事前準備

Google Analytics でプロパティを作成しておいてください
UA からはじまるトラッキング ID が取得できていれば OK です
また Google Tag Manager のアカウント作成とアカウントに紐づくコンテナ作成を行っておいてください

カスタム HTML タグの作成

gtm1_1.png

スクリプトの部分は以下の通りです

<script>
$(function() {
  $('audio').on('play', function(e) {
    var AudioText = $(this).find("source").attr("src");
    dataLayer.push({
      "event" : "Audio Click Event",
      "eventCategory" : "Audio Click",
      "eventAction" : AudioText,
      "eventLabel" : null,
      "eventValue" : null
    });
  });
});
</script>

audio タグの再生ボタンが押された時にイベント情報を送信します
今回はすべてのページで監視する設定ですが特定のページだけの場合はトリガーを変更してください

変数の作成

左メニューから変数を選択し新規作成します
先ほどのスクリプト内で使った変数を定義します
gtm2.png

変数は「データレイヤー変数」を選択してください
gtm3.png

トリガーの作成

左メニューからトリガーを選択し新規作成します
gtm4.png

トリガーの種類は「カスタムイベント」を選択しましょう
名前はスクリプト内で定義した「Audio Click Event」にしました

トラッキングするタグを追加

あとは作成したトリガーと変数を使って Google Analytics のタグを新規で作成するだけです
gtm6.png

カテゴリ、アクション、ラベル、値は作成した変数を選択します
直接入力しないでもテキストボックスの横にボタンがあるはずなのでそこから変数を選択できます
トリガーも先ほど作成したカスタムイベントのトリガーを選択しましょう

公開する

サマリー画面から「公開」すれば OK です
バージョンや説明は適当に設定してください
プレビュー機能を使って設定したタグが 2 つあることを確認してください
※自分はもう一つ Google Analytics のタグを追加しているので 3 つありますが今回とは関係ないので 2 つあれば OK です
gtm7.png

また Google Analytics 側でリアルタイム集計を確認するとちゃんと再生したイベントが送信されていることも確認できます
gtm8.png

応用 audio.js に対応してみる

audio.js の場合 source タグを使いません
また、自動的に audio タグが div タグで囲われてしまうため audio タグを直接クリックすることができません
なので、カスタム HTML を以下のように修正します

<script>
$(function() {
  $(document).on("click", 'div[class="play-pause"]', function(e) {
    var AudioText = $("audio").attr("src");
    dataLayer.push({
      "event" : "Audio Click Event",
      "eventCategory" : "Audio Click",
      "eventAction" : AudioText,
      "eventLabel" : null,
      "eventValue" : null
    });
  });
});
</script>

これで audio.js にも対応することできるようになります

最後に

Google Tag Manager を使って audio タグのクリック数を集計する方法を紹介しました
これを応用すれば audio タグの他のイベント情報も集計することができると思います

ただ、前提として Google Tag Manager のサービス概念を理解していないと厳しいかもしれません
Google Analytics よりも一つ上位のレイヤーのサービスになるので実際は Google Analytics 側にデータを溜め込んでいます

Google Analytics の機能を Google Tag Manager を使うことで一括して管理、公開することができるようになります
要するに Google Analytics の機能を使う場合も Google Analytics の画面からではなく Google Tag Manager からやりましょうということになります

参考サイト

2018年4月26日木曜日

Phaser3 でスペースを押したときにスプライトが落下するサンプル

概要

Phaser3 ではないっぽかったので作ってみました

環境

  • macOS 10.13.2
  • Phaser 3.4

サンプルコード

var score = 0;
var scoreText;

var config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 },
      debug: false
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update,
  }
};

var game = new Phaser.Game(config);

function preload () {
  this.load.image('star', '../assets/star.png');
}

function create () {
  // key
  space = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
  cursors = this.input.keyboard.createCursorKeys();
  // mark
  mark = this.physics.add.sprite(0, 0, 'star');
  mark.disableBody(true, false);
  // star
  stars = this.physics.add.group();
}

function update() {
  // key
  if (Phaser.Input.Keyboard.JustDown(space)) {
    fallStar();
  }
  // cursors
  if (cursors.left.isDown) {
    mark.x += -4
  } else if (cursors.right.isDown) {
    mark.x += 4
  }
}

function fallStar() {
  // 最上段の真ん中に星を生成
  var star = stars.create(mark.x, 0, 'star');
  // 反発係数を設定
  star.setBounce(0.1);
  // 壁と衝突する設定
  star.setCollideWorldBounds(true);
  // 星に X, Y 軸に対して力を与える
  star.setVelocity(0, 20);
  // 重力の影響を受ける
  star.allowGravity = true;
}

動作確認

左右のカーソルで星を移動させスペースを押すと星が次々に落下します
phaser_fall_star.gif

Tips

create メソッドの最後に

  • this.physics.add.collider(stars, stars);

を追加して星同士が衝突するようにしてみたのですが一定の星が上に積み上げられると星が重なってしまいました
本当は重ならないようにしたかったのですがやり方がわからず断念しました

最後に

Phaser3 でキーを押した時にスプライトが落下するサンプルを紹介しました
Phaser3 はドキュメントがまだ未成熟なので console.log などを駆使してプロパティなどを探っていく必要がある気がします

2018年4月25日水曜日

Phaser3 のチュートリアルを Sinatra 上で動かしてみた

概要

Phaser3 は HTML + Javascript でゲームを開発することができるフレームワークです
公式のチュートリアルがあったのでやってみました
また今回は Sinatra 上で動かしてみました
成果物は以下の通りです

$ tree . -I vendor
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── public
│   ├── assets
│   │   ├── bomb.png
│   │   ├── dude.png
│   │   ├── platform.png
│   │   ├── sky.png
│   │   └── star.png
│   └── js
│       └── game1.js
└── views
    └── index.erb

4 directories, 11 files

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1
  • Phaser 3.4

準備

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor
  • mkdir -p public/assets
  • mkdir -p public/js
  • mkdir views

Sinatra アプリの作成

あとで作成するテンプレートファイルを呼び出すだけです

  • vim app.rb
require 'sinatra/base'

class GameApp < Sinatra::Base
  get '/' do
    erb :index
  end
end
  • vim config.ru
require './app.rb'
run GameApp

リソースファイルの取得

ゲームに必要な画像などのリソースファイルを取得します

  • cd public
  • wget 'http://phaser.io/tutorials/making-your-first-phaser-3-game/phaser3-tutorial-src.zip'
  • unzip phaser3-tutorial-src.zip

とりあえずほしいのは assets ディレクトリ配下にある画像ファイルです
他にも html ファイルや js ファイルがありますが今回は使いません

テンプレートファイルの作成

ここでゲーム用の JavaScript を参照することでゲームを開始します

  • vim views/index.erb
<!DOCTYPE html>
<html>
<head>
  <script src="http://cdn.jsdelivr.net/npm/phaser@3.4.0/dist/phaser.min.js"></script>
  <script src="../js/game1.js"></script>
</head>
<body>
</body>
</html>

ゲーム用の JavaScript ファイル作成

ここが本題です
Phaser を使ってゲームを作成しています

  • vim public/js/game1.js
var score = 0;
var scoreText;

var config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 },
      debug: false
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update,
  }
};

var game = new Phaser.Game(config);

function preload () {
  this.load.image('sky', '../assets/sky.png');
  this.load.image('star', '../assets/star.png');
  this.load.image('platform', '../assets/platform.png');
  this.load.image('bomb', '../assets/bomb.png');
  this.load.spritesheet(
    'dude',
    '../assets/dude.png',
    { frameWidth: 32, frameHeight: 48 }
  );
}

function create () {
  this.add.image(400, 300, 'sky');
  platforms = this.physics.add.staticGroup();
  platforms.create(400, 568, 'platform').setScale(2).refreshBody();
  platforms.create(600, 400, 'platform');
  platforms.create(50, 250, 'platform');
  platforms.create(750, 220, 'platform');

  dude = this.physics.add.sprite(100, 450, 'dude');
  dude.setBounce(0.2);
  dude.setCollideWorldBounds(true);
  this.anims.create({
    key: 'left',
    frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
  });
  this.anims.create({
    key: 'turn',
    frames:[ { key: 'dude', frame: 4 } ],
    frameRate: 20
  });
  this.anims.create({
    key: 'right',
    frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
    frameRate: 10,
    repeat: -1
  });
  dude.body.setGravityY(300)
  this.physics.add.collider(dude, platforms);

  cursors = this.input.keyboard.createCursorKeys();

  stars = this.physics.add.group({
    key: 'star',
    repeat: 11,
    setXY: { x: 12, y: 0, stepX: 70 }
  });
  stars.children.iterate(function (child) {
    child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
  });
  this.physics.add.collider(stars, platforms);
  this.physics.add.overlap(dude, stars, collectStar, null, this);

  scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000'});

  bombs = this.physics.add.group();
  this.physics.add.collider(bombs, platforms);
  this.physics.add.overlap(dude, bombs, hitBomb, null, this);
}

function collectStar(dude, star) {
  star.disableBody(true, true);
  score += 10;
  scoreText.setText('score: ' + score);
  if (stars.countActive(true) == 0) {
    stars.children.iterate(function (child) {
      child.enableBody(true, child.x, 0, true, true);
    });
    var x = (dude.x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);
    var bomb = bombs.create(x, 16, 'bomb');
    bomb.setBounce(1);
    bomb.setCollideWorldBounds(true);
    bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
    bomb.allowGravity = false;
  }
}

function hitBomb(dude, bomb) {
  this.physics.pause();
  dude.setTint('0xff0000');
  dude.anims.play('turn');
  gameOver = true;
}

function update() {
  if (cursors.left.isDown) {
    dude.setVelocityX(-160);
    dude.anims.play('left', true);
  } else if (cursors.right.isDown) {
    dude.setVelocityX(160);
    dude.anims.play('right', true);
  } else {
    dude.setVelocityX(0);
    dude.anims.play('turn');
  }
  if (cursors.up.isDown && dude.body.touching.down) {
    val = -530;
    console.log(val);
    dude.setVelocityY(val);
  }
}

上記は完成版になります

少しだけ説明

詳細は後述にある参考サイトの URL を確認してください

流れとしては config を設定し、その configを元にゲームを作成 new Phaser.Game(config) するだけです
実装しなければならないのは config.scene で定義した preload, create, update のそれぞれの関数です

名前の通りですがそれぞれには役割があり preload はゲーム開始前に行うことを記載し、create はゲーム開始時に作成するべきキャラクターや背景、敵などを作成し、update はイベントなどフレームが更新されたときに随時呼ばれるような処理を記載します
今回だと preload はリソースファイルの読み込み、create は各種スプライトや背景などの作成、update はカーソルが押されたときの処理を実装しています

Phaser には物理エンジンがあります
物理エンジンを使ったスプライトを作成したい場合には this.physics.add を使用します
物理エンジンには static と dynamic の 2 種類があり static は重力の影響を受けません
dynamic は重量の影響を受けるので自然落下します

物理エンジンを使ったスプライト同士は当たり判定を行うことができます
当たり判定を行う場合には this.physics.add.collider を使います
また衝突時に何かしら処理を行いたい場合には this.physics.add.overlap(sprite1, sprite2, callback, null, this) を使います
3 つ目の引数でコールバックとして呼び出す関数名を指定することで sprite1 と sprite2 が衝突した際にその関数が呼ばれるようになります

アニメーションの作成は this.anims.create を使います
今回やプライヤーの方向展開をするアニメーションを定義するのに使っています
定義したアニメーションはグローバルに使うことができるようになります

物理エンジンを動的に割り当てたい場合は disableBody/enableBody を使います

動作確認

  • bundle exec rackup config.ru

localhost:9292 にアクセスするとゲームが開始されます
星をすべて取ると爆弾が生成され爆弾に当たると終了です
phaser_demo.gif

とりあえずこんな感じで動作するようになればチュートリアルは完了です

最後に

ブラウザゲームが作成できる Phaser3 を試してみました
Sinatra で動かした理由としては CORS のエラー対策で公式でも説明があるのですがファイルをブラウザで開くだけだと Uncaught DOMException: Failed to execute 'texImage2D' on 'WebGLRenderingContext': The image element contains cross-origin data, and may not be loaded. のエラーが発生します
なので必ず何かしらの Web サーバ上で動作させる必要があり今回は Sinatra を使いました
nginx だけでも動作するので面倒な場合は nginx のドキュメントルートに配置しても OK です

Sinatra で動かしておけばサーバ側のコードが必要になったときもそのまま使えるので良いかなと思います
本当にブラウザだけで簡潔するゲームなのであれば不要だとは思いますが

参考サイト

2018年4月24日火曜日

Sinatra で erb を分割する方法

概要

Sinatra で erb を複数に分割する方法を紹介します
ヘッダやフッタ、サイドバーなど同じスタイルのページを作成するときに便利です

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1

ヘッダーファイル作成

  • vim views/header.erb
<head>
  <title>title</title>
  <link rel="stylesheet" type="text/css" href="../css/bulma.min.css">
  <script type="text/javascript" src="../js/fontawesome-all.min.js"></script>
</head>

css や js などを参照しています

ページ本体で読み込む

  • vim views/index.erb
<html>
<%= render(:erb, :header) %>
<body>
</body>
</html>

render メソッドをコールすることで他の erb ファイルを参照します

参照された側で変数を使いたい場合

Sinatra 側でインスタンス変数を定義した場合は、それをそのままどこの erb でも参照することができます
ただ、index.erb で生成した変数などを header.erb で参照したい場合は以下のように render メソッドをコールします

<%= render(:erb, :header, :locals => { :title => "hogefuga" }) %>

locals を使って変数を指定します
上記の場合であれば header.erb 側で以下の用に参照することができます

<title><%= title %></title>

最後に

Sinatra で erb を分割する方法を紹介しました
過去に haml を使う方法を紹介しましたが haml でも同じようなことができます
デフォルトが erb なので erb を使う場合には参考にしてください

2018年4月23日月曜日

rkt を使ってコンテナを立ち上げてみた

概要

前回 CoreOS を vagrant を使って構築してみました
せっかく構築してみたので CoreOS の特徴でもある rkt を使ってみました

環境

  • macOS 10.13.4
  • vagrant 2.0.3
  • coreos 4.15.15
  • rkt 1.29.0

dockerhub からイメージを取得して起動する

すでに docker で構築したイメージを使うことができます

  • sudo rkt run --insecure-options=image docker://redis

--insecure-options=image を指定する必要があります
ちなみに公式の library は上記で良いですが個人で push したイメージなどはリポジトリを指定する必要があるので以下のようにします

  • sudo rkt run --insecure-options=image docker://registry-1.docker.io/kakakikikeke/request-dumper

docker のように pull だけしたい場合は fetch というコマンドがあるのでそれを使います

コンテナの操作

あとは一覧を取得してコンテナの操作をします
停止 -> 削除は以下の通りです

  • rkt list
  • sudo rkt stop 6e9111b9
  • sudo rkt rm 6e9111b9

ちなみにバックグラウンドで run する場合には systemd-run を噛ませる必要があります

  • sudo systemd-run rkt --insecure-options=image run docker://redis
Running as unit: run-rd6fe84ddc868479299f433bf79b8d434.service

コンテナにアクセスする場合は rkt list で表示される IP にアクセスすれば OK です
rkt の場合ネットワークはホストにあるものを直接利用します
なので、docker のようにホストにポートをバインドしないとコンテナにアクセスできないということがありません

ただ、rkt list ではコンテナ上で LISTEN しているポートはわかりません
なのでポートを調べたい場合はマニフェストを調べるコマンドがあるのでこれを使えばポートがわかります

  • rkt cat-manifest 6e9111b9

最後に

rkt コマンドを使って Dockerhub 上にあるイメージを引っ張ってきてコンテナを起動してみました
基本的な操作は docker に似ているかなと思います

rkt には actool というビルドコマンドがありこれを使って rkt 用のイメージを作成することができます
が、結構面倒なので個人的にはイメージは docker で作ってしまって適当なレポジトリで公開しコンテナを起動する時に rkt を使うという風にしたほうが簡単かなと思います

参考サイト

2018年4月22日日曜日

Mac 上に vagrant で CoreOS 環境を構築する

概要

Mac で docker を使う場合は docker for Mac を使っています
基本的にはこれで問題ないのですが mody がた

環境

  • macOS 10.13.4
  • vagrant 2.0.3
  • coreos vagrant (rev 62f3b54958aaef3b75d0ebaa254f7303ac2926c1)

CoreOS を立ち上げる

2375 でアクセスできるようにする

この作業は必須ではありません
CoreOS 上に ssh してそこで docker コマンドを叩いても OK です
Mac 上からアクセスしたいために 2375 ポートを LISTEN にします

  • vagrant ssh

ここから coreos 内での操作

  • sudo su -
  • vim /etc/systemd/system/docker-tcp.socket
[Unit]
Description=Docker Socket for the API

[Socket]
ListenStream=2375
BindIPv6Only=both
Service=docker.service

[Install]
WantedBy=sockets.target
  • systemctl enable docker-tcp.socket
  • systemctl stop docker
  • systemctl start docker-tcp.socket
  • systemctl start docker

これでリモートからアクセス出来るようになります

動作確認

今回は Mac 上から CoreOS 上の docker にアクセスします
Mac のターミナルから docker コマンドを使って以下のようにアクセスすることができます

  • docker -H 172.17.8.101:2375 info

IP の部分は Virtualbox の vboxnet のネットワークになります
ご自身の環境に併せて変更してください

vagrant の場合 VM 上のポートは localhost にフォーワードしてから使うということができます
2375 ポートも localhost にフォーワードしたいという場合は

  • mv config.rb.sample config.rb
  • vim config.rb
$expose_docker_tcp=2375

としてから再度 vagrant up すれば localhost でもアクセス出来ます

  • docker -H localhost:2375 info

最後に

Vagrant を使って CoreOS 環境を構築してみました
docker のバージョンもほぼ最新がインストールされてます
執筆時点の CoreOS では 18.03.0-ce がインストールされていました
ちなみに Mac 側のクライアントの docker は 18.04.0-ce-rc2 を使いました

今回はシングル構成で立ち上げる方法を紹介しましたが etcd を使ってクラスタ構成で立ち上げることもできるようです

参考サイト

2018年4月21日土曜日

ruby の open-uri で「encode: "\x87U" from Shift_JIS to UTF-8 (Encoding::UndefinedConversionError)」

概要

クロール先のサイトに機種依存文字が入っている場合などに発生します
例えば などがそれにあたります

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111

対応方法

基本は shift_jis を指定していると思いますがその場合は別の文字コードを指定します
CP932 を指定します

html = open(url, "r:CP932").read.encode('utf-8')

こんな感じでエラーを回避することができます

2018年4月20日金曜日

ld: library not found for -lrealmcore-ios 対策

概要

いつも対策を忘れるのでメモしておきます
個人的に発生しやすいと思うケースは Xcode や macOS のバージョンがあがったり Cocoapods のバージョンを上げたりしたときでしょうか

環境

  • macOS 10.13.4
  • Xcode version 9.3 (9E145)
  • Cocoapod 1.4.0

対策一覧

上から順に実施してどこかでエラーがなくなれば OK
それでもなくならない場合は諦めずに 2 周目突入

  • Shift + Cmd + k
  • Alt + Shift + Cmd + k
  • Xcode 再起動
  • pod update
  • Build Settings -> Build Active Architecture Only -> No (参考)

2018年4月19日木曜日

UIAlertController の handler 内で UI の描画処理を行いたい場合に注意すること

概要

UIAlertController は iOS 上でアラートを表示するためのクラスです
基本的な使い方は Web 上に転がっているのでそれらを参考にしてください
もしくは自分の記事もあります -> こちら

UIAlertController はボタンを押したときの処理を handler にクロージャとして記載することができます
ただこの handler 内で UI の操作や描画行おうとする上手くいかないことがあります
自分の場合は handler 内で UIActivityIndicatorView の startAnimating と stopAnimating をしようとしたのですがハマったのでその対策方法を紹介します

環境

  • macOS 10.13.4
  • Xcode version 9.3 (9E145)
  • Cocoapod 1.4.0

方法

答えとしては DispatchQueue を使います
UI の描画はメインスレッドで行い、それ以外の処理はバックグラウンドで処理させるようにします

let defaultAction: UIAlertAction = UIAlertAction(title: "初期化", style: UIAlertActionStyle.destructive, handler:{
    (action: UIAlertAction!) -> Void in
    // インジケータ表示
    self.indicator.isHidden = false
    self.indicator.startAnimating()
    DispatchQueue.global(qos: .default).async {
        // 何か処理
        self.doSomething()
        DispatchQueue.main.async {
            // インジケータ非表示
            self.indicator.stopAnimating()
            self.initView()
        }
    }
})

ポイントは 3 つあります
まずインジケータの開始は DispatchQueue の外で行います
当然ですが DispatchQueue.global(qos: .default).async 内で UI の操作を行うと怒られるためです

DispatchQueue.global(qos: .default).async 内ではアラート画面のボタンが押されたときの処理を描きます
具体的には Realm などのデータベース処理や Firebase などのクラウド処理を記載します

そしてそれらの処理が完了したら DispatchQueue.main.async でメイン処理に戻りインジケータの停止とその他の UI の描画を行っています
DispatchQueue.main.async の処理は必ずロジック側の処理が完了してからコールされます

参考サイト

2018年4月18日水曜日

Sinatra でホスト名を取得する方法

概要

ホスト名というかドメイン名を取得する方法です
API とかだとエンドポイントが複数ありそのエンドポイントに応じて処理を分けたいというケースがあると思います

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1

コード

require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/' do
    puts request.host
  end
end

request.host で OK でした
request は Sinatra::Request のオブジェクトでクライアントからリクエストされた情報が詰まっています

2018年4月17日火曜日

bulma でモーダルを制御してみる

概要

bulma にはモーダルを表示するための機能があります
https://bulma.io/documentation/components/modal/

今回はそれを使ってみました
説明のベースとなるアプリは過去に紹介した Sinatra アプリになります
必要な部分のコードのみ紹介します
ただ、今回紹介するコードでは Sinatra アプリ側のコードは一切登場しないので Sinatra 環境がなくても全く問題ありません

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1
  • bulma 0.6.2
  • jQuery 3.3.1

モーダルを表示するボタンの作成

id 属性を使ってボタンを特定できるようにします

<a class="button is-small is-primary" id="show_action">Show</a></td>

モーダル用の div タグを作成

HTML 内のどこでもいいのでモーダル表示用の div タグを追記しましょう
今回は body タグの直前に記載しました

... 省略

  <div class="modal">
    <div class="modal-background"></div>
    <div class="modal-content">
      <div class="notification is-danger">
        This area has danger messages.
      </div>
    </div>
  <button class="modal-close is-large" aria-label="close"></button>
  </div>
  <script async type="text/javascript" src="../js/bulma.js"></script>
  <script async type="text/javascript" src="../js/form.js"></script>
</body>
</html>

表示する内容は bulma の notification を使っています
ここの内容は何でも OK なので好きなものを設置してください

ボタンを押した際の処理を実装

jQuery を使っています
ポイントはボタンを押した際に先ほどの div タグの modal クラスに is-active というクラスを追加してあげる点です

$("#show_action").on("click", function() {
  $("div.modal").addClass("is-active");
})

$("div.modal-background").on("click", function() {
  $("div.modal").removeClass("is-active");
})

またモーダルが表示されたあとに画面をクリックしたらモーダルが消える処理も追加しています
モーダルを消すのは単純に追加した is-active クラスを削除するだけで OK です

動作確認

では動作確認してみましょう
今回であれば以下のように動作すれば OK です
bulma_modal1.gif

最後に

bulma のモーダル機能を使ってみました
JavaScript を使って表示/非表示を制御する必要はありますが簡単に使えると思います

参考サイト

2018年4月16日月曜日

Sinatra + jQuery で UI の制御をしてみる

概要

前回 Sinatra + bulma で Sinatra アプリにデザインを当て込む方法を紹介しました
bulma はボタンなどの UI コンポーネントを a タグで定義するケースが多いです
form タブが使えないわけではないですがせっかく用意された bulma のコンポーネントは使えません
今回は a タグで定義したボタンから jQuery (Ajax) を使って POST リクエストを送信して Sinatra 側で処理して UI 側で結果を表示してみたいと思います

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1
  • bulma 0.6.2
  • jQuery 3.3.1

jQuery のインストール

ローカルに .js ファイルをダウンロードしてもいいですが jQuery CDN という .js ファイルを配信してくれている CDN サーバがあるのでそれを使います

テンプレートファイルの head タグ内に jQuery の情報を定義しましょう

  • vim views/admin.erb
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Admin - Free Bulma template</title>
  <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700" rel="stylesheet">
  <link rel="stylesheet" type="text/css" href="../css/admin.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
  <script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
  <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>

... 以下省略

とりあえず head タグの最後に追記しました
今回 Ajax を使います
jQuery CDN に slim minified というタイプの jQuery がありますがこれだと Ajax 関数が用意されていないので minified というタイプを使うようにしてください

テンプレートファイルの編集

すでに用意されている a タグを変数して jQuery でタグの情報を取得できるようにします

155 行目くらいにある a タグで用意されたボタンがあるのでそこに属性を追加します

  • vim views/admin.erb
<table class="table is-fullwidth is-striped">
  <tbody>
    <tr>
      <td width="5%"><i class="fa fa-bell-o"></i></td>
      <td>Lorum ipsum dolem aire</td>
      <td><a class="button is-small is-primary" id="action1" data-value="action1_data">Action</a></td>
    </tr>
    <tr>
      <td width="5%"><i class="fa fa-bell-o"></i></td>
      <td>Lorum ipsum dolem aire</td>
      <td><a class="button is-small is-primary" id="action2" data-value="action2_data">Action</a></td>
    </tr>

... 以下省略

id="action1" data-value="action1_data" という属性を追加しています
とりあえず 2 つの a タグに追加しています
HTML5 に data-* というカスタム属性がありこれを使うと好きな名前の属性をタグに追加することができます
今回は data-value というカスタム属性を追加しています

独自 js ファイルの作成

先ほど追記したカスタム属性を操作できる JavaScript ファイルを作成します
当然ですがここで jQuery を使っていきます

  • vim public/js/form.js
$("a[id^='action']").on("click",function() {
    var value =  $(this).data("value");
    console.log(value);
    $.ajax({
      type: "POST",
      url: "/action",
      data: {
        "value": value
      }
    }).then(
      // success
      function (data) {
          alert(data);
      },
      // error
      function (edata) {
          console.log(edata);
          alert(edata.responseText);
      }
    );
})

まずボタンがクリックされたかの判定をセレクタの正規表現を使って判断しています
a タグに設定された id タグが action で始まるタグがクリックされた場合に処理が実行されるようにしています

  • $("a[id^='action']").on("click",function() {

そして data-value で定義したカスタム属性の値を取得しています

  • $(this).data("value");

次にこれを Ajax を使って送信しています
Ajax の送信先は Sinatra になります
Sinatra 側のルーティング定義はこのあと追加します
フォームを想定して POST で送るようにしてみました
カスタム属性で取得した値をそのまま送信しています

あとは成功時と失敗時の処理を then を使って定義しています
最近の jQuery では then という構文が使えるのでそれを使ってモダンな感じで書いています
もちろん success, error を使って書いても問題なく動作はします

作成した form.js はテンプレート内から参照しておいてください
参照箇所は body タグの直前になります

  • vim views/admin.erb
... 省略

  <script async type="text/javascript" src="../js/form.js"></script>
</body>
</html>

Sinatra アプリケーションの修正

では最後に Ajax から送られたデータを受け取るルーティングを追加します
以下のルーティングを追加しましょう

  • vim app.rb
post '/action' do
  "ok -> #{params['value']}"
end

処理は特に何もしていません
送信されてきた値に ok という文字列を付与して返しているだけです
これを受け取った jQuery 側はこの文字列情報をアラートに表示します

動作確認

  • bundle exec ruby app.rb

で localhost:4567 にアクセスし該当のボタンをクリックしてみましょう
成功するとアラートが表示されると思います
sinatra_bulma_jquery_sample1.gif

ちなみに Ajax のエラー側の処理をさせたい場合は Sinatra アプリのレスポンスをエラーになるように変更してあげれば OK です

post '/action' do
  status 400
  "ng -> #{params['value']}"
end

最後に

Sinatra に jQuery を組み込んで UI 側の制御もできるようにしてみました
Sinatra アプリ側で生成した HTML を返却してあとはフロントサイド側 (JavaScript) だけで処理をしてあげればサーバ側へのアクセスも減るので軽量な UI を実現できるかなと思います

今回は主に Ajax のサンプルを紹介しましたが同じように DOM の操作も行えるのでクリックした際に新たに div タグを生成するなどすればインタラクティブな UI を実現することができると思います

2018年4月15日日曜日

Sinatra + bulma で簡単ダッシュボード作成

概要

bulma はモダンな WebUI がデザインできる CSS フレームワークです
今回は Sinatra と組み合わせて動的なダッシュボード UI を作成してみます

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1
  • bulma 0.6.2

テンプレートファイルの作成

bulma には様々なテンプレートが準備されておりその中にダッシュボード用のテンプレートがあります
これを Sinatra で動くように移植していきます

  • mkdir views
  • cd views
  • wget 'https://raw.githubusercontent.com/dansup/bulma-templates/master/templates/admin.html'
  • mv admin.html admin.erb

テンプレート用のディレクトリを作成しここに用意されているテンプレートファイルを配置します
更に Ruby から参照できるように拡張子を .erb に変更します

変数埋め込み

とりあえず今回はタイトルの部分を変数にしてみたいと思います

  • vim admin.erb
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Admin - Free Bulma template</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
  <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700" rel="stylesheet">
  <!-- Bulma Version 0.6.0 -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css" integrity="sha256-HEtF7HLJZSC3Le1HcsWbz1hDYFPZCqDhZa9QsCgVUdw=" crossorigin="anonymous" />
  <link rel="stylesheet" type="text/css" href="../css/admin.css">
</head>
<body>

  <!-- START NAV -->
  <nav class="navbar is-white">
    <div class="container">
      <div class="navbar-brand">
        <a class="navbar-item brand-text" href="../">
          <%= @title %> 
        </a>

長いので先頭から対象部分まで表示しています
下の方でタイトル部分を <%= @title %> に変更しています
この変数を Sinatra アプリでいろいろと変更してみたいと思います

bulma に必要なファイルの設定

css と js が必要なものがあるので配置します

  • mkdir -p public/css
  • mkdir -p public/js
  • cd public/css
  • wget 'https://github.com/dansup/bulma-templates/raw/master/css/admin.css'
  • cd public/js
  • wget 'https://github.com/dansup/bulma-templates/raw/master/js/bulma.js'

これらもすでに用意されているテンプレートから取得します

コントローラ側の実装

先ほど作成した .erb のテンプレートファイルに埋め込んだ変数の値をコントローラ側で設定します

  • vim app.rb
require 'sinatra'

get '/' do
  @title = ['bulma admin', 'ぶるまあどみん', '管理画面'].sample
  erb :admin
end

ランダムでタイトル用の値を設定しているだけです
あとは erb メソッドを使って先ほどの admin.html をシンボルで参照します

動作確認

これで localhost:4567 にアクセスすると以下のように表示されると思います
sinatra_bulma1.png

アクセスするごとに左上のタイトル部分が変化するのがわかると思います

今回はタイトル部分しか弄りませんでしたが例えば数字の部分などは MySQL から取得したものを表示するなどの処理を app.rb に書き足してあげれば更に動的な値を表示してあげることもできます

ツリー

今回の成果物のツリー構造です

$ tree -I "vendor" .
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── public
│   ├── css
│   │   └── admin.css
│   └── js
│       └── bulma.js
└── views
    └── admin.erb

4 directories, 6 files

最後に

Sinatra + bulma で超簡単にダッシュボードを実装してみました

nodejs のように npm でインストールしてやる感じではなく静的ファイルをダウンロードしてやるので直感的に開発はできるかなと思います
あとは CSS や JS をカスタマイズすれば更に動的なページが作れると思います

自分はこのやり方であとは jQuery を使って開発しています
Ruby でというか Sinatra で WebUI を作る場合はどんな感じで進めるのが良いのかベストプラクティスを知りたいです、、、

参考サイト

2018年4月14日土曜日

sidekiq/api を使ってキューやジョブの操作をしてみる

概要

Sidekiq はワーカーがエラーをキャッチすると自動でリトライ処理をしてくれます
故にエラーが解消されない限りはワーカーが永遠にジョブを処理し続けてしまいます
そんな場合にはキューからジョブを削除しなければなりません
直接 redis を操作しても問題ないですが Sidekiq にはキューやジョブを操作する API が備わっているので使ってみました

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • Redis 3.2.1
  • sidekiq 5.1.3

Redis に接続する

今回の作業はすべて irb (bundle console) で行います

デフォルトだと localhost:6379 のデータベースインデックス 0 に接続します
今回はインデックスが 1 のデータベースに接続するため切り替える必要があります

irb(main):001:0> require 'sidekiq/api'
=> true
irb(main):002:0> Sidekiq.redis { |con| con.select 1 }
=> "OK"

これでインデックス 1 のデータベースに切り替えできました

キューの一覧を取得する

特にしていしなければ default のみあると思います

irb(main):003:0> Sidekiq::Queue.all
=> [#<Sidekiq::Queue:0x007fc13b3a0238 @name="default", @rname="queue:default">]
irb(main):004:0> q = Sidekiq::Queue.new
=> #<Sidekiq::Queue:0x007fc13b371d20 @name="default", @rname="queue:default">

Sidekiq::Queue.new でキューオブジェクトを作成します
引数に文字列でキュー名を指定することができます
指定しない場合は default になります
今回はこの default キュー内のジョブを削除してみます

キュー内に溜まったジョブの一覧を取得する

each で回すだけです

irb(main):008:0> q.each { |job|
irb(main):009:1* p job
irb(main):010:1> }
=> nil

特に何も入っていないので何も表示されませんでした

リトライ対象のジョブの一覧を取得する

リトライ対象のジョブは別のクラスから取得することができます
Sidekiq::RetrySet を使います

irb(main):014:0> rs = Sidekiq::RetrySet.new
=> #<Sidekiq::RetrySet:0x007fc13b83fc08 @name="retry", @_size=1>
irb(main):015:0> rs.each { |job|
irb(main):016:1* p job['jid']
irb(main):017:1> p job['retry_count']
irb(main):018:1> }
"85d82e45decb7474d5fbfbaf"
6
=> nil

長いので jidretry_count だけ表示しています
ここにジョブが入っていると default キューを見るワーカーがこのジョブを処理し続けます

リトライジョブを削除する

ジョブに対して delete メソッドをコールするだけです

irb(main):022:0> rs.each { |job|
irb(main):023:1* job.delete
irb(main):024:1> }
=> nil
irb(main):025:0> rs.each { |job|
irb(main):026:1* p job.jid
irb(main):027:1> }
=> nil
irb(main):028:0> rs.size
=> 0

もし特定のジョブだけを削除したい場合は一旦 jid のリストを取得して、指定の jid とマッチしたら delete するという感じの処理を追加すれば OK です
おそらくこの操作が一番運用で使うんじゃないかなと思います

最後に

sidekiq/api を使ってキューやジョブの操作をしてみました
前回紹介したダッシュボードの機能を使えば UI でもリトライジョブを削除することができると思います
1 つや 2 つであれば UI でもいいですが、何百何千という単位で処理したい場合はこれを使ってプログラムを組んだほうが良いかなと思います

参考サイト

2018年4月13日金曜日

Sidekiq でキューを指定してワーカーを起動する方法

概要

Sidekiq はデフォルトで default という名前のキューを使用します
用途の異なる複数のワーカーを作成する場合にキューを指定しないとすべてのワーカーが default を見てしまい本来掴みたくないジョブを掴んでしまうことがあります
その場合はワーカーごとにキューをわけましょう

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • Redis 3.2.1
  • sidekiq 5.1.3
  • sinatra 2.0.1

ワーカークラスでキューを指定する

これはエンキューするクライアントが参照します
当初ここに記載しておけばワーカー側も自動で参照してくれると思ったのですが、これはクライアント用の設定みたいです

require 'sidekiq'

Sidekiq.configure_server do |config|
  config.redis = { 'db' => 1 }
end

class MyWorker
  include Sidekiq::Worker
  sidekiq_options queue: 'test_queue'

  def perform(complexity)
    case complexity
    when 'super_hard'
      sleep 20
      puts 'super_hard'
    when 'hard'
      sleep 10
      puts 'hard'
    else
     sleep 1
     puts 'easy'
    end
  end
end

sidekiq_options queue: 'test_queue' の設定を 1 行追加しているだけです
これで MyWorker.perform_async したときに test_queue にエンキューされます

ワーカーを起動する時にキューを指定する

ワーカーは起動するときのオプションで参照するキューを指定します

  • bundle exec sidekiq -q test_queue -r ./worker.rb

これで default ではなく test_queue を参照するようになります
試しに redis-cli を使って確認したところちゃんと指定のキューが作成されていることがわかると思います

$ redis-cli -n 1
127.0.0.1:6379[1]> SMEMBERS queues
1) "test_queue"

最後に

Sidekiq でキューを指定してワーカーを動作させる方法を紹介しました
ワーカーを起動する時にオプションが必要になるのがポイントでしょうか

もう一つ方法として namespace を使う方法もあるらしいですがこれはまた別の方法かなと思います
新たにキューを作成するわけではなく更に上位の概念で分割しようという機能だと思います
また、namespace を使う場合には redis-namespace という gem も必要になります

参考サイト

2018年4月12日木曜日

Sinatra + Sidekiq で簡単非同期 Web アプリ

概要

前回 Sidekiq に入門してみました
今回は Sinatra と組み合わせてみました

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • Redis 3.2.1
  • sidekiq 5.1.3
  • sinatra 2.0.1

ワーカー

基本的には前回と同じです

  • vim worker.rb
require 'sidekiq'

Sidekiq.configure_server do |config|
  config.redis = { 'db' => 1 }
end

class MyWorker
  include Sidekiq::Worker

  def perform(complexity)
    case complexity
    when 'super_hard'
      sleep 20
      puts 'super_hard'
    when 'hard'
      sleep 10
      puts 'hard'
    else
     sleep 1
     puts 'easy'
    end
  end
end
  • bundle exec sidekiq -r ./worker.rb -P ./tmp/sidekiq.pid

とりあえず起動しておきましょう

アプリ

普通の Sinatra アプリに enqueue する処理をいれるだけです

require 'sinatra/base'
require './worker.rb'

Sidekiq.configure_client do |config|
        config.redis = { 'db' => 1 }
end

class MyApp < Sinatra::Base
        get '/enqueue/:value' do
                MyWorker.perform_async(params['value'])
                "enqueue #{params['value']}"
        end
end
  • bundle exec rackup config.ru

動作確認

  • curl 'localhost:9292/enqueue/super_hard'
  • curl 'localhost:9292/enqueue/hard'
  • curl 'localhost:9292/enqueue/easy'

と実行してみましょう
すぐにレスポンスが返ってくると思います

そしてワーカーのログを確認するとちゃんと非同期処理が実行されていることが確認できると思います

easy
2018-04-04T09:20:43.050Z 5683 TID-ovxtvzpkn MyWorker JID-170aa25c66fcdafb78e7c049 INFO: done: 1.001 sec
hard
2018-04-04T09:20:49.878Z 5683 TID-ovxtw03kf MyWorker JID-22663fbd98445bde4119a4d1 INFO: done: 10.006 sec
super_hard
2018-04-04T09:20:57.103Z 5683 TID-ovxtvzpbn MyWorker JID-895bd0adfb849652c0860eae INFO: done: 20.005 sec

最後に

Sinatra + Sidekiq で超かんたんな非同期 Web アプリを作成してみました
処理も切り離せて管理もしやすいと思います

サービスにする場合はちゃんとキューの状態などを追える仕組みを考えないとダメだと思います

2018年4月11日水曜日

Sidekiq で管理画面を表示する方法

概要

Sidekiq には管理画面がデフォルトでついており現在実行中のジョブなどを視覚的に確認することができます
デフォルトだと使えないようなので有効にする方法を紹介します

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • Redis 3.2.1
  • sidekiq 5.1.3

ダッシュボード用の Rack アプリの作成

すでにモジュールが用意されているのでそれを実行するだけです

  • vim config.ru
require 'sidekiq'

Sidekiq.configure_client do |config|
  config.redis = { :size => 1 }
end

require 'sidekiq/web'
run Sidekiq::Web

動作確認

実行してみましょう
Rack で動作します

  • bundle exec rackup config.ru

Rack なのでデフォルトは 9292 ポートで動作します
ブラウザでアクセスすると以下のような UI が表示されると思います

sidekiq_dashboard1.png

ちゃんとローカライズもされていました
もちろんワーカーを動作させるとグラフが描画されるのを確認することができます

最後に

Sidekiq のダッシュボードアプリを作成してみました
簡単に作れるのでとりあえず立てておくと運用も楽になるかなと思います

Heroku にデプロイできるボタンもあったので Redis が外部から参照できるのであれば Heroku にデプロイしちゃってもいいかなと思います

参考サイト

2018年4月10日火曜日

Sidekiq 超入門

概要

Sidekiq はキュー&ワーカー方式で非同期処理を実現するためのツールです
バックエンドに Redis を使っています
今回は動作させるところまでやってみました

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • Redis 3.2.1
  • sidekiq 5.1.3

インストール

  • bundle init
  • vim Gemfile
gem "sidekiq"
  • bundle install --path vendor

Redis も必要になるのでインストールしていない場合は brew などを使ってインストールしてください

ワーカー作成

vim worker.rb

require 'sidekiq'

Sidekiq.configure_server do |config|
  config.redis = { 'db' => 1 }
end

class MyWorker
  include Sidekiq::Worker

  def perform(complexity)
    case complexity
    when 'super_hard'
      sleep 20
      puts 'super_hard'
    when 'hard'
      sleep 10
      puts 'hard'
    else
     sleep 1
     puts 'easy'
    end
  end
end

include Sidekiq::Worker し perform メソッドを実装することでワーカーを作成します
クライアント側でキューイングする際に perform_async を呼び出すことでこの perform メソッドが自動的に呼ばれます

今回は非同期で実行されているか確認できるように sleep を入れています

クライアント作成

  • vim test_client.rb
require './worker.rb'

Sidekiq.configure_client do |config|
  config.redis = { 'db' => 1 }
end

class MyClient
  def enqueue(complexity)
    MyWorker.perform_async(complexity)
  end
end

cli = MyClient.new
cli.enqueue('super_hard')
cli.enqueue('hard')
cli.enqueue('easy')

クライアント側は worker クラスが必要になります
MyWorker クラスでは include Sidekiq::Worker することでエンキュー用の perform_async というメソッドが使えるようになっているのでこれをコールします

今回はワーカー側もクライアント側も localhost:6379 のデフォルトの Redis を参照しているため特にホストやポートの指定はしていませんが、ホストやポートが異なる場合は url というオプションがあるのでそれを使って指定してください (参考)

動作確認

まずはワーカーを起動します
ワーカーは Redis を必要するので Redis も起動しておいてください

  • redis-server
  • mkdir ./tmp
  • bundle exec sidekiq -r ./worker.rb -P ./tmp/sidekiq.pid

sidekiq というコマンドがあるのでこれを使ってワーカースクリプトを指定します
sidekiq コマンドの詳しいオプションは bundle exec sidekiq --help で確認できます
-P オプションで PID ファイルを作成することができます
ワーカーを停止する際に必要になるので指定しておきます
ずらずらーとログが流れてくると思います
キックしている AA とエラーログが流れなければ OK です

次にクライアントを起動します

  • bundle exec ruby test_client.rb

すると以下のようにログが流れると思います

2018-04-04T06:17:54.025Z 4252 TID-oxfu7p3gg MyWorker JID-4473b56c73ed2dd8cf1f89a7 INFO: start
easy
2018-04-04T06:17:55.029Z 4252 TID-oxfu7p3gg MyWorker JID-4473b56c73ed2dd8cf1f89a7 INFO: done: 1.004 sec
hard
2018-04-04T06:18:04.027Z 4252 TID-oxfu7zlbk MyWorker JID-b6be42cf2f3e5f34c7f47468 INFO: done: 10.002 sec
super_hard
2018-04-04T06:18:14.030Z 4252 TID-oxfu7zlh4 MyWorker JID-4ea3461949baefd67e58172c INFO: done: 20.005 sec

enqueue した順番は super_hard -> hard -> easy ですが出力されるのは easy -> hard -> super_hard で表示されており非同期でそれぞれ処理されているのがわかります

ワーカーを停止する場合は sidekiqctl というコマンドが用意されているのでこれを使うと良いです

  • bundle exec sidekiqctl stop ./tmp/sidekiq.pid

PID ファイルを指定すれば停止できます
全部のワーカーが完了するかタイムアウトの 10 秒後の終了します
タイムアウトの時間は設定で変更できます

最後に

SIdekiq の基本的な使い方を紹介しました
Redis を導入する必要はありますが比較的に簡単に導入できるので非同期処理を実現するにはオススメです

Web アプリではよく Rails に組み込む方法が紹介されていますが Sinatra などの軽量フレームワークでも使うことはできます
そのあたりも検証できたら紹介したいと思います

参考サイト