ここ数日やっていること

ここ数日RabbitMQとrabbitmq-delayed-message-exchangeとElixirを組み合わせたサンプルを書くということをしている

ようやっとサンプルが動いたのでブログを書く

やりたいこと

github.com

やりたいことは、このPluginを使って指定した秒数待ったあとにメッセージを送るというもの

ここのReadmeに使い方が書いてあるのだけど、初心者の僕には、Javaっぽい言語のソースコードが載っているだけでイマイチピンと来なかった。

この記事ではRabbitMQでdelayed-message-exchangeのpluginだけインストールした状態から、実際にElixirでメッセージ送信するためにやったことを紹介する。

Exchange設定

f:id:kytiken:20171114215537p:plain

こんな感じにRabbitMQ managementからExchangeを設定する。

Elixirプログラム

受信側

defmodule Consumer do
  use GenServer
  use AMQP

  def start_link do
    GenServer.start_link(__MODULE__, [], [])
  end

  @exchange    "my-exchange"
  @queue       "gen_server_test_queue"
  @queue_error "#{@queue}_error"

  def init(_opts) do
    {:ok, conn} = Connection.open
    {:ok, chan} = Channel.open(conn)
    # Limit unacknowledged messages to 10
    Basic.qos(chan, prefetch_count: 10)
    Queue.declare(chan, @queue_error, durable: true)
    # Messages that cannot be delivered to any consumer in the main queue will be routed to the error queue
    Queue.declare(chan, @queue, durable: true,
                                arguments: [{"x-dead-letter-exchange", :longstr, ""},
                                            {"x-dead-letter-routing-key", :longstr, @queue_error}])
    Queue.bind(chan, @queue, @exchange)
    # Register the GenServer process as a consumer
    {:ok, _consumer_tag} = Basic.consume(chan, @queue)
    {:ok, chan}
  end

  # Confirmation sent by the broker after registering this process as a consumer
  def handle_info({:basic_consume_ok, %{consumer_tag: consumer_tag}}, chan) do
    {:noreply, chan}
  end

  # Sent by the broker when the consumer is unexpectedly cancelled (such as after a queue deletion)
  def handle_info({:basic_cancel, %{consumer_tag: consumer_tag}}, chan) do
    {:stop, :normal, chan}
  end

  # Confirmation sent by the broker to the consumer process after a Basic.cancel
  def handle_info({:basic_cancel_ok, %{consumer_tag: consumer_tag}}, chan) do
    {:noreply, chan}
  end

  def handle_info({:basic_deliver, payload, %{delivery_tag: tag, redelivered: redelivered}}, chan) do
    spawn fn -> consume(chan, tag, redelivered, payload) end
    {:noreply, chan}
  end

  defp consume(channel, tag, redelivered, payload) do
    number = String.to_integer(payload)
    if number <= 10 do
      Basic.ack channel, tag
      IO.puts "Consumed a #{number}."
    else
      Basic.reject channel, tag, requeue: false
      IO.puts "#{number} is too big and was rejected."
    end

  rescue
    # Requeue unless it's a redelivered message.
    # This means we will retry consuming a message once in case of exception
    # before we give up and have it moved to the error queue
    #
    # You might also want to catch :exit signal in production code.
    # Make sure you call ack, nack or reject otherwise comsumer will stop
    # receiving messages.
    exception ->
      Basic.reject channel, tag, requeue: not redelivered
      IO.puts "Error converting #{payload} to integer"
  end
end

送信側

defmodule RabbitmqTutorials do
  @queue       "gen_server_test_queue"
  @exchange    "my-exchange"
  def hello do
    {:ok, connection} = AMQP.Connection.open
    {:ok, channel} = AMQP.Channel.open(connection)
    AMQP.Basic.publish(channel, @exchange, "", "5", headers: [{"x-delay", 5000}])
    AMQP.Connection.close(connection)
  end
end

あくまでサンプルなので、ここからもっと洗練する必要がある

今回の目的は「サンプルを書いて動かしてみる」なので以上

躓いた所

AMQP

RabbitMQではAMQPというプロトコルを使用するのですが、中途半端な理解なまま先に進んだためわけわからなくなった。

AMQPについてはGreeの技術記事がすごくわかりやすかった

AMQPによるメッセージング | GREE Engineers' Blog

AMQP.Exchange#declare/4

ElixirのAMQPクライアントには AMQP.Exchange#declare/4 という関数があって、これを使えばExchangeを登録できる

AMQP.Exchange – amqp v0.3.0

delayed-message-exchange を使うには typeに x-delayed-message を入れないといけない AMQP.Exchange#declare/4 のtypeはAtomを入れるようになっていて x-delayed-messageAtomで書きたいんだけど、 - が付いているもんだからAtomが作れない

仕方がないからRabbitMQ managementで登録することにした

RabbitMQ management exchange登録

なんもわからずに登録しようとしたらこんなこと言われた

f:id:kytiken:20171114222539p:plain

406 PRECONDITION_FAILED - Invalid argument, 'x-delayed-type' must be an existing exchange type 

argumentsx-delayed-type 入れろとのこと

感想

ちゃんと理解してから先に進むの大事だなと思った

今日やったこと(2017/11/07)

今日はRabbitMQをElixirで使うために入門をしていた

まず公式チュートリアルを進める

https://www.rabbitmq.com/tutorials/tutorial-one-elixir.html

amqpを使用する

https://hexdocs.pm/amqp/readme.html

ちなみにチュートリアルでは 0.2.1 なので古い 僕は今のところ Sending まで進めたが、動かないということはなかった

外部から接続したかったため、 open/1 のドキュメントを読む https://hexdocs.pm/amqp/AMQP.Connection.html#open/1

雑なサンプルを置いておく

defmodule RabbitmqTutorials do
  def hello do
    {:ok, connection} = AMQP.Connection.open(host: "localhost",
                                             port: 5672,
                                             username: "kytiken",
                                             password: "hogefugapiyo",
    )
    {:ok, channel} = AMQP.Channel.open(connection)
    AMQP.Queue.declare(channel, "hello")
    AMQP.Basic.publish(channel, "", "hello", "Hello World!")
    IO.puts " [x] Sent 'Hello World!'"
    AMQP.Connection.close(connection)
  end
end

ここまでやったところで時間切れ

お疲れ様でした

今日やったこと

RabbitMQにrabbitmq-delayed-message-exchangeを入れるというのをやりました

やった方法を簡単にまとめます

方法

Dockerを使って試します

1.RabbitMQのコンテナを作ります

docker run --rm -p 15672 --name some-rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password rabbitmq:3-management

2.コンテナの中に入ります

docker exec -it some-rabbit bash

3.rabbitmq-delayed-message-exchangeのファイルを取ってきます

apt-get update && apt-get install -y curl
curl -O http://www.rabbitmq.com/community-plugins/v3.6.x/rabbitmq_delayed_message_exchange-0.0.1.ez 

4.pluginsフォルダにぶち込みます

mv rabbitmq_delayed_message_exchange-0.0.1.ez $RABBITMQ_HOME/plugins

5.有効にします

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

以上

今日やったこと

プログラミングElixirという本の16章 OTP:サーバというのを読んで写経した

GenServer の使い方が書いてあるところまでやった。

勉強が進んだら練習用にタイマーアプリを書いてみたい。

余談

帰宅してから晩御飯食べて、洗濯物を干してから始めたので、あんまり時間が取れなかった。

仕事が終わってからもプログラミングをするのは久しぶりだった。

家に帰ってから5分でもいいから、毎日Elixirを触るというのを習慣化したい

以上

iPhoneの音声入力を使ってみた感想

今まで気恥ずかしくて音声入力を使っていなかったんだけど試しに使ってみたら案外悪くなかった

この記事では私が音声入力を使ってみたところでの感想を書く

便利だと感じたところ

  • 入力インターフェースが貧弱なときに便利だと感じた(フリック入力しかできないときとか)
  • キーボードがあったらキーボードのほうが早いのだけどフリック入力ってどんだけがんばっても速度が出ない

不満に思ったところ

  • 外で使うには周りの人に聞かれてしまうから使いづらいところ
  • いい間違えた時に修正するのがめんどう
  • ネットスラングを打とうとしても自分の思った通りに変換されない
  • 技術記事を書く場合特殊な固有名詞が出てくるため使いにくい感じがする

副次的な効果

  • 家の中でしゃべることが増えた
  • 正しい日本語を考えるようになった(文章的におかしいと認識の精度が悪くなるため)

今まで使っていなかった理由

  • 今まで使っていなかったのはそもそも認識の精度が悪いと思っていた
  • 仮に認識の精度が悪かったら、せっかくしゃべったのに認識の精度が悪いともう一回同じことをしゃべり直すことになるから、手間が増えそうというのがあった

 感想

  • 音声入力していてそういう事はなくはないけれど思っていたより精度が高くて、ちょっとブログ記事を書く分には悪くないと思った
  • 家の中で使うのはすごく便利だと思う
  • 外で使うのはやっぱり抵抗がある・・・

活用場面 

  • ブログ記事の下書きを書く時
  • チャット返信
  • ちょっと思いついたメモをする

以上、感想でした

 

 

1週間でやったこと(2017/08/28~2017/09/03)

やったこと

なんとなくConohaのiOSクライアントを自分用に作り始めている

github.com

  • Moya
  • RxSwift
  • Realm

このあたりのライブラリを使っているのだけど、iOSの開発をするのも久しぶりだし、Moya RxSwift は初めて触るので全然進まない

あと画面構成がほとんどTableViewになってしまった。とりあえず自分用だしいいか。

実家に帰った

法事で実家に帰った

ちなみに 9/9 にも法事で実家に帰る予定

法事だから仕方がない

仕事用にキーボードを購入した

www.instagram.com

一緒に働いている人がもう使わないというので、ちょっと触らせてもらった。 使ってみると結構いい感じだったので格安で譲ってもらった。

プリンセスプリンシパルというアニメにハマった

www.pripri-anime.jp

ネット配信がことごとく有料だったので見ていなかったのだけど、ニコニコ動画で期間限定無料配信だったので見てみたらハマってしまった

プリンセスの食えない女感が非常に好きです

SHIROBAKOというアニメを見始める

アニメ制作会社の制作進行の女の子が主人公の物語。

「納品に間に合うのか、間に合わないのか」というようなカタルシスがあって面白くもあり、胃が痛くもなる。

まだ5話くらいまでしか見ていないので、ネタバレはご遠慮ください。

雑感

やっぱり進捗だめですといった感じがする。

帰ったらアニメを見るのを辞めたらたぶんもうちょっと進捗が出ると思うのだけど、気づいたら見ているんだよなぁ・・・

capybaraコードリーディング(find, synchronize)

はじめに

capybaraでは find メソッドを使うとある一定の時間が経過するまでリトライし続けます。

今回は find メソッドをターゲットに、どうやってリトライし続ける動作を実現しているのかを見ていこうと思います。

ちなみに私は最初それを知らずに 「ページのロードが終わってないからかな?(適当)」なんてアタリをつけて sleep 1 をテストコードに埋めてたりしていました。

しかしそんなことをするくらいだったら Capybara.default_max_wait_time の時間を伸ばしたほうがいいです。 Capybara.default_max_wait_time とは findall メソッドを使用したときに探し続けるのに使用する最大の待ち時間です。

ちなみに調べた時点での capybara のバージョンは2.14.3です

コードリーディング

まずはエントリーポイントとなるfindメソッドを見ます

31 def find(*args, &optional_filter_block)
32   query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
33   synchronize(query.wait) do
34     if (query.match == :smart or query.match == :prefer_exact) and query.supports_exact?
35       result = query.resolve_for(self, true)
36       result = query.resolve_for(self, false) if result.empty? && !query.exact?
37     else
38       result = query.resolve_for(self)
39     end
40     if query.match == :one or query.match == :smart and result.size > 1
41       raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
42     end
43     if result.empty?
44       raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
45     end
46     result.first
47   end.tap(&:allow_reload!)
48 end

https://github.com/teamcapybara/capybara/blob/2.14.3/lib/capybara/node/finders.rb#L31

(ドキュメント) http://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Finders#find-instance_method

要素が見つからないシチュエーションを考える

要素が見つからない場合、

query.resolve_for

したあとに、下記の行が実行されて Capybara::ElementNotFound が発生します

43     if result.empty?
44       raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
45     end

じゃあどこでリトライの処理をしているかというと

        synchronize

にリトライの実装があります。

synchronizeをコードリーディング

 77 def synchronize(seconds=Capybara.default_max_wait_time, options = {})
 78   start_time = Capybara::Helpers.monotonic_time
 79
 80   if session.synchronized
 81     yield
 82   else
 83     session.synchronized = true
 84     begin
 85       yield
 86     rescue => e
 87       session.raise_server_error!
 88       raise e unless driver.wait?
 89       raise e unless catch_error?(e, options[:errors])
 90       raise e if (Capybara::Helpers.monotonic_time - start_time) >= seconds
 91       sleep(0.05)
 92       raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Capybara::Helpers.monotonic_time == start_time
 93       reload if Capybara.automatic_reload
 94       retry
 95     ensure
 96       session.synchronized = false
 97     end
 98   end
 99 end

https://github.com/teamcapybara/capybara/blob/2.14.3/lib/capybara/node/base.rb#L77

(ドキュメント) http://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Base#synchronize-instance_methodhttp://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Base#synchronize-instance_method

Capybara::ElementNotFoundときには

 84     begin
 85       yield
 86     rescue => e

yield を実行中のはずなので、87 ~ 93行目の条件に引っかからない場合には

 94       retry

が実行され yield が再び実行されます。 これが Capybara::ElementNotFound が発生されなくなるまでループするという実装になっているということがわかりました。

おわりに

余談ですが、 click_link などのメソッドでも内部的には findメソッドを呼び出しているので結局要素が現れるまで待ってくれてるようです