├── .drone.yml ├── .erlang ├── .gitignore ├── CHANGES.md ├── Gihufile ├── LICENSE ├── Makefile ├── README.md ├── bump-version ├── include ├── lmq.hrl └── lmq_test.hrl ├── lmq.sublime-project ├── rebar ├── rebar.config ├── rel ├── files │ ├── app.config │ ├── erl │ ├── install_upgrade.escript │ ├── lmq │ ├── lmq-admin │ ├── lmq.cmd │ ├── nodetool │ ├── start_erl.cmd │ └── vm.args └── reltool.config ├── src ├── influxdb_client.erl ├── lmq.app.src ├── lmq.erl ├── lmq_api.erl ├── lmq_app.erl ├── lmq_console.erl ├── lmq_cow_msg.erl ├── lmq_cow_prop.erl ├── lmq_cow_queue.erl ├── lmq_cow_reply.erl ├── lmq_cow_stats.erl ├── lmq_event.erl ├── lmq_handler.erl ├── lmq_handler_dist.erl ├── lmq_hook.erl ├── lmq_hook_preserve_header.erl ├── lmq_lib.erl ├── lmq_metrics.erl ├── lmq_misc.erl ├── lmq_mpull.erl ├── lmq_mpull_sup.erl ├── lmq_queue.erl ├── lmq_queue_mgr.erl ├── lmq_queue_sup.erl ├── lmq_queue_supersup.erl └── lmq_sup.erl └── test ├── lmq_SUITE.erl ├── lmq_api_http_SUITE.erl ├── lmq_event_SUITE.erl ├── lmq_hook_crash.erl ├── lmq_hook_invalid.erl ├── lmq_hook_sample1.erl ├── lmq_hook_sample2.erl ├── lmq_hook_test.erl ├── lmq_lib_SUITE.erl ├── lmq_mpull_SUITE.erl ├── lmq_msgpack_SUITE.erl ├── lmq_queue_SUITE.erl ├── lmq_queue_mgr_SUITE.erl └── lmq_test_handler.erl /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | image: erlangR16B02 3 | script: 4 | - make test 5 | -------------------------------------------------------------------------------- /.erlang: -------------------------------------------------------------------------------- 1 | lists:foreach(fun(Path) -> 2 | code:add_patha(filename:join(Path, "ebin")) 3 | end, filelib:wildcard("./deps/*")). 4 | code:add_patha("./ebin"). 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deps 2 | ebin 3 | log 4 | logs 5 | *.beam 6 | Mnesia.* 7 | .eunit 8 | rel/lmq* 9 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 更新履歴 2 | ### 0.7.0 (Unreleased) 3 | * 残りリトライ数を API レスポンスに追加 4 | * プロパティに差分が無い時は更新処理をスキップ 5 | * 大きなデータ (>7MB) を push した時のエラーを修正 6 | 7 | ### 0.6.4 (2014-04-22) 8 | * StatsD をサポート 9 | 10 | ### 0.6.3 (2014-02-18) 11 | * 複数キューが対象の API (``/messages?qre=``) で ``t`` が0以外だとクラッシュするバグを修正 12 | 13 | ### 0.6.2 (2014-01-28) 14 | * HTTP API を変更 15 | 16 | ### 0.6.1 (2013-12-26) 17 | * msgpack の str サポートを有効化 18 | 19 | ### 0.6.0 (2013-12-02) 20 | * REST ベースの HTTP API を実装 21 | * MessagePack-RPC API と同等の機能を実装 22 | * 今後は HTTP API が標準になる予定 23 | * nack/release 時もリトライ回数を消費するように変更 24 | * 安定性の向上とバグ修正 25 | * pull 待ちクライアント切断時の挙動を改善 26 | * キューのプロパティを初期化できないバグを修正 27 | 28 | ### 0.5.0 (2013-10-10) 29 | * クラスタリング機能を実装 30 | * マスターレス方式を採用 31 | * キューは全てのノードにミラーされる 32 | * 統計情報を収集 33 | * `lmq-admin` コマンドを追加 34 | * クラスタ操作 35 | * LMQ の状態と統計情報の表示 36 | * `pull_any` を再実装 37 | * リクエストを受けてから作成されたキューもチェック対象にするように変更 38 | * 安定性の向上とバグ修正 39 | * システムテーブルをオンメモリに変更 40 | * 多数のクライアントが `pull` した時に、一部に異常なレスポンスが返るバグを修正 41 | * リトライ超過で破棄するメッセージが大量にある時に、応答が無くなるバグを修正 42 | 43 | ### 0.4.0 (2013-09-10) 44 | * 明示的に `create` をしなくても、`push`/`pull` をした時に自動的にキューを作成するように変更 45 | * プロパティの実装を強化 46 | * 稼働中のキューのプロパティを更新する機能を追加 47 | * 正規表現でデフォルト値を登録する機能を追加 48 | * API の変更 49 | * `create` を廃止 50 | * `update_props` を追加 51 | * `set_default_props`/`get_default_props` を追加 52 | * `push` のレスポンスを変更 53 | * `pull`/`pull_any` のレスポンスを変更 54 | * 安定性の向上とバグ修正 55 | * `release` でリトライ回数が減るのを修正 56 | * `pull_any` を timeout: 0 で呼ぶとメッセージを取得できないことがあるのを修正 57 | * その他の修正 58 | 59 | ### 0.3.0 (2013-08-05) 60 | * 大幅なパフォーマンスの向上 61 | * push: 6.1 倍 62 | * pull: 1.9 倍 63 | * pull+done: 47倍 64 | * 起動時に自動的に DB を初期化するように変更 65 | * 設定ファイルのパスを変更 66 | * その他バグ修正 67 | 68 | ### 0.2.0 (2013-07-24) 69 | * キューのプロパティ機能を実装 70 | * 再送までの時間を設定可能 71 | * 再送回数を設定可能 72 | * パックする時間を設定可能 73 | * パック機能を実装 74 | * メッセージを push してから一定期間内に push されたメッセージをまとめる 75 | * メッセージがまとめられるまでは pull できない 76 | * 一度まとめられたメッセージは、その単位で再送される 77 | * 新しい API を追加 78 | * delete() 79 | * push_all() 80 | * pull_any() 81 | * 既存の API にパラメータを追加 82 | * create() に property を追加 83 | * pull() に timeout を追加 84 | * 安定性の向上とバグ修正 85 | * 起動時に既存のキューを読み込むように変更 86 | * 複数のクライアントから同時にアクセスした際の安定性を向上 87 | * ロギングを追加 88 | * その他バグ修正 89 | 90 | ### 0.1.0 (2013-06-24) 91 | * 初期リリース 92 | -------------------------------------------------------------------------------- /Gihufile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime 3 | RUN sed -i -e 's,archive.ubuntu.com,ftp.jaist.ac.jp/pub/Linux,' /etc/apt/sources.list 4 | RUN apt-get update 5 | RUN apt-get install -y build-essential curl 6 | RUN apt-get install -y libncurses5-dev openssl libssl-dev fop xsltproc unixodbc-dev 7 | RUN apt-get install -y git 8 | RUN curl -s -O http://erlang.org/download/otp_src_R16B01.tar.gz 9 | RUN tar xzf otp_src_R16B01.tar.gz 10 | RUN cd otp_src_R16B01 && ./configure && make && make install 11 | WORKDIR /gihuci 12 | CMD make test 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Internet Initiative Japan Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = $(shell pwd)/rebar 2 | 3 | .PHONY: deps test rel eunit 4 | 5 | all: deps compile 6 | 7 | compile: 8 | $(REBAR) compile 9 | 10 | deps: 11 | $(REBAR) get-deps 12 | 13 | clean: 14 | $(REBAR) clean 15 | -rm test/*.beam 16 | 17 | distclean: clean 18 | $(REBAR) delete-deps 19 | 20 | eunit: all 21 | $(REBAR) skip_deps=true eunit 22 | 23 | test: all 24 | $(REBAR) skip_deps=true eunit ct 25 | 26 | testclean: 27 | -rm -r .eunit 28 | -rm -r logs 29 | 30 | generate: 31 | $(REBAR) generate 32 | 33 | rel: deps compile generate 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LMQ - Lightweight Message Queue 2 | [![Build Status](https://drone.io/github.com/iij/lmq/status.png)](https://drone.io/github.com/iij/lmq/latest) 3 | 4 | LMQ は Erlang 製のメッセージキューです。 5 | HTTP API を使ってキューに任意のデータを投入し、任意のタイミングで取り出すことができます。 6 | 7 | LMQ は使いやすく、運用しやすく、十分に早い MQ を目指して開発されました。 8 | その上で、流れ込むメッセージを処理するための特徴的な機能をいくつか有しています。 9 | 10 | LMQ は次のような特徴を持ちます。 11 | 12 | * REST ベースの HTTP API で簡単に利用できる 13 | * 1台で動作し、2台をつなげば冗長化できる 14 | * in memory で十分に早く動作する 15 | * 名前を付けて複数のキューを作成できる 16 | * タイムアウトに基づくメッセージの再送 17 | * キュー毎にタイムアウト時間等のプロパティを個別に設定できる 18 | * キューの自動作成 19 | * 投入されたメッセージは常にキューに蓄積される 20 | * コンシューマがいなくてもロストしない 21 | * LMQ 内で複数のキューへメッセージを複製 22 | * 投入されたメッセージの時間単位での集約 23 | * 統計情報の出力 24 | 25 | **※仕様は開発中のもので、将来のバージョンで非互換の変更が加わる可能性があります。** 26 | 27 | ## インストール 28 | Erlang R16B01 以上が必要になりますので、あらかじめインストールしておいてください。 29 | 30 | リポジトリを取得し、以下のコマンドを実行します。 31 | 32 | $ cd $REPO_ROOT 33 | $ make rel 34 | 35 | これにより、`$REPO_ROOT/rel/lmq` ディレクトリに必要なファイルが全てコピーされます。 36 | 37 | ## 使い方 38 | 39 | 起動するには以下のコマンドを実行します。 40 | 41 | $ bin/lmq start 42 | 43 | 起動しているか確認するには以下のようにします。 44 | 45 | $ bin/lmq ping 46 | pong 47 | 48 | これでシングル構成で動作するようになります。 49 | 50 | メッセージの投入は HTTP 経由で行います。以下では重要でないヘッダは省略しています。 51 | 52 | $ curl -i -XPOST localhost:9980/messages/myqueue -H 'content-type: text/plain' -d 'hello world!' 53 | HTTP/1.1 200 OK 54 | content-length: 15 55 | content-type: application/json 56 | 57 | {"accum":"no"} 58 | 59 | `{"accum":"no"}` は、キューがメッセージをどのように処理したかを示しています。詳細は[メッセージの集約](#accumlate)を参照してください。 60 | 61 | キューに投入したメッセージを取り出します。 62 | 63 | $ curl -i localhost:9980/messages/myqueue 64 | HTTP/1.1 200 OK 65 | content-length: 12 66 | content-type: text/plain 67 | x-lmq-queue-name: myqueue 68 | x-lmq-message-id: f0eca12e-19f2-4922-bcc9-6e42bd585937 69 | x-lmq-message-type: normal 70 | 71 | hello world! 72 | 73 | レスポンスボディは投入したメッセージがそのまま返ってきます。`content-type` も同様です。 74 | 75 | その他、メッセージを処理するために重要な情報が HTTP ヘッダに含まれます。`x-lmq-message-id` は各メッセージに割り当てられたユニークな ID です。LMQ はメッセージの再送処理のために、処理が完了したら ack を返すことになっています。この時にメッセージ ID を用います。 76 | 77 | $ curl -i -XPOST 'localhost:9980/messages/myqueue/f0eca12e-19f2-4922-bcc9-6e42bd585937?reply=ack' 78 | HTTP/1.1 204 No Content 79 | 80 | `404 Not Found` が返ってきた場合は、既にメッセージがタイムアウトして再送待ちになっている可能性があります。メッセージを取り出すところからやり直してみてください。デフォルトでは、再送までの時間は30秒に設定されています。 81 | 82 | LMQ には他にも API が用意されています。詳細は [HTTP API](#http_api) を参照してください。 83 | 84 | ## キューのプロパティ 85 | キューにはプロパティを設定することができ、その値により各キューの動作をカスタマイズすることができます。利用可能なプロパティと、デフォルト値は以下の通りです。 86 | 87 | name | type | default | description 88 | --- | --- | ---: | --- 89 | accum (pack) | float | 0 | メッセージを集約する期間(秒)、0 で無効 90 | retry | integer | 2 | メッセージの再送回数 91 | timeout | float | 30 | メッセージが再送されるまでの時間(秒) 92 | 93 | LMQ はキューのプロパティを設定するための複数の方法を用意しています。プロパティを設定する全ての API は、変更したいプロパティを投入することが可能です。設定されなかったプロパティにはデフォルト値が使われます。 94 | 95 | プロパティは以下の順に優先されます。 96 | 97 | 1. キューに設定されたプロパティ 98 | 2. デフォルトプロパティリストの最初にマッチしたもの 99 | 3. システムのデフォルト値(変更不可) 100 | 101 | 例えば、キュー `foo` に `{"accum": 15}` というプロパティを設定し、デフォルトプロパティに `[[".*", {"timeout": 60}]]` を設定した場合、キュー `foo` の実際のプロパティは以下のようになります。 102 | 103 | {"accum": 15, "retry": 2, "timeout": 60} 104 | 105 | プロパティの変更はいつでも可能ですが、新しいプロパティが反映されるのは、変更後に Put したメッセージだけです。 106 | 107 | ## タイムアウトと再送 108 | LMQ はメッセージが Get されてからの経過時間を管理しており、これが `timeout` 値を超えると、そのメッセージは正しく処理されなかったと見なしてメッセージをキューに戻します。これにより、他のクライアントがメッセージを Get できるようになります。これが LMQ における再送処理です。 109 | 110 | LMQ はメッセージを再送する際にメッセージ ID を変更します。そのため、メッセージ ID を指定する必要のある Reply は、再送された元のメッセージに対して呼び出しても失敗します。 111 | 112 | メッセージは `retry` 回数だけ再送されます。そして、この回数を超過したメッセージは破棄されます。`retry` を 0 に設定したキューでは、メッセージは決して自動的には再送されませんが、タイムアウトの発生によって破棄される点は同一なので注意してください。メッセージ ID を指定する必要のある Reply は、破棄されたメッセージに対して呼び出しても失敗します。 113 | 114 | タイムアウトは `ext` を Reply することでリセットできます。時間のかかる処理をする前に `ext` を Reply すれば、処理がタイムアウトする可能性を低くすることができます。 115 | 116 | また、`nack` を Reply する事でメッセージを意図的にキューに戻すことが可能です。結果として、メッセージは即座に再送されます。この場合でもリトライ回数としてカウントされるので注意してください。クライアントがメッセージを処理できない状態になった時は、明示的に `nack` を Reply することで、LMQ による再送を待たずに他のクライアントにメッセージを渡すことができます。 117 | 118 | ## メッセージの集約 119 | LMQ には、一定時間内に特定のキューに Put された全てのメッセージを集約して、時間経過後に一つのメッセージとしてキューに入れる集約機能があります。この集約されたメッセージのことを複合メッセージと呼びます。複合メッセージは通常のメッセージと同様に処理されます。すなわち、再送処理や API による操作は複合メッセージ単位になります。 120 | 121 | この機能を有効にするには、キューのプロパティの `accum` を 0 より大きくしてください。なお、集約が有効なキューでは、Put されたメッセージは `accum` 時間が経過するまで取り出せないことに注意してください。 122 | 123 | ## クラスタリング 124 | 耐障害性を向上させるために、2つ以上の LMQ ノードを使ってクラスタを組むことができます。LMQ クラスタは P2P 方式で実装され、マスターレスモデルになっています。キューのデータはクラスタ内の全ノードで同期されるため、クラスタを構成するノードが落ちたとしても影響はほとんどありません。また、クライアントはクラスタ内のどのノードに対してもリクエストを送ることができます。 125 | 126 | クラスタを組むには以下のようにします。説明上は *lmq1.example.com* と *lmq2.example.com* でクラスタを組むことにします。 127 | 128 | 1. `etc/vm.args` ファイルを編集します。 129 | 130 | `-name lmq@127.0.0.1` という行の *127.0.0.1* を他のノードからアクセスできる IP アドレス、もしくはホスト名に変更します。例として、それぞれ *lmq1.example.com* と *lmq2.example.com* にします。 131 | 132 | 2. それぞれのホストで LMQ を起動します。 133 | 134 | lmq1.example.com$ bin/lmq start 135 | lmq2.example.com$ bin/lmq start 136 | 137 | 3. クラスタを組みます。 138 | 139 | lmq2.example.com$ bin/lmq-admin join lmq@lmq1.example.com 140 | 141 | *lmq2.example.com* をクラスタから抜くには以下のようにします。 142 | 143 | lmq2.example.com$ bin/lmq-admin leave 144 | 145 | ## ステータス表示 146 | LMQ の現在のステータスを表示するには、以下のコマンドを実行します。 147 | 148 | $ bin/lmq-admin status 149 | 150 | ## StatsD / Graphite 151 | StatsD 経由で Graphite に metrics を送信することができます。デフォルトで localhost:8125 に送るので、別のサーバに送るには `app.config` の以下の項目を変更してください。 152 | 153 | ```erlang 154 | {statsderl, [ 155 | {hostname, "localhost"}, 156 | {port, 8125} 157 | ]}, 158 | ``` 159 | 160 | また、`stats_interval` を1以上にすると、指定した間隔で各キューのメッセージ数を送信します。単位は ms です。パフォーマンスに影響するため、設定するなら 30,000 程度を推奨します。 161 | 162 | ```erlang 163 | {lmq, [ 164 | {stats_interval, 0} 165 | ]}, 166 | ``` 167 | 168 | ## パフォーマンスチューニング 169 | 高いパフォーマンスが必要な環境では、API のアクセスログを無効化することで 15-30% ほど多くの API リクエストを処理できるようになります。 170 | 171 | API のアクセスログは `info` レベルで吐かれるので、lager の全てのハンドラのログレベルを `notice` 以上にすればアクセスログによるオーバーヘッドが無くなります。 172 | 173 | 例えば、以下のように `app.config` を設定できます。 174 | 175 | ```erlang 176 | {lager, [ 177 | {handlers, [ 178 | {lager_file_backend, [ 179 | [{file, "./log/console.log"}, {level, notice}, {size, 0}, {date, "$D0"}, {count, 14}] 180 | ]} 181 | ]}, 182 | ``` 183 | 184 | ## HTTP API 185 | LMQ は HTTP インタフェースを 9980 番ポートで提供しています(ポートは変更可能)。URL に渡す値は全て URL エンコードしてください。 186 | 187 | リクエスト/レスポンスは JSON 形式です。 188 | 189 | ### メッセージ操作 190 | #### GET /messages/:name 191 | * 概要: メッセージをキューから1つ取得する 192 | * パラメータ: 193 | * name: キュー名 194 | * クエリパラメータ(オプション) 195 | * t: Timeout, メッセージを取得できるまで待つ時間(秒) 196 | * cf: Compound Format, 複合メッセージのフォーマット 197 | * multipart: `multipart/mixed` 形式。デフォルト 198 | * msgpack: `msgpack` 形式 199 | * レスポンスコード: `200 OK` 200 | * レスポンスヘッダ: 201 | * content-type: POST 時の content-type 202 | * x-lmq-queue-name: メッセージを取り出したキュー名 203 | * x-lmq-message-id: メッセージの ID 204 | * x-lmq-message-type: メッセージの種類 205 | * normal: 通常のメッセージ 206 | * compound: 複合メッセージ 207 | * レスポンスボディ: POST されたデータ 208 | 209 | #### POST /messages/:name 210 | * 概要: メッセージをキューに追加する 211 | * パラメータ: 212 | * name: キュー名 213 | * リクエストボディ: メッセージの内容となるデータ 214 | * レスポンスコード: `200 OK` 215 | * レスポンスボディ: 216 | 217 | ```json 218 | {"accum": "FLAG"} 219 | ``` 220 | 221 | * FLAG: 集約処理が行われたかどうか 222 | * new: 新たに集約が開始された 223 | * yes: 既存のメッセージと集約された 224 | * no: 集約されなかった 225 | 226 | #### GET /messages?qre=:regexp 227 | * 概要: メッセージを regexp にマッチするキューの**いずれか**から1つ取得する 228 | * パラメータ: 229 | * qre: 対象のキューを絞り込む正規表現 230 | * クエリパラメータ(オプション): `GET /messages/:name` と同様 231 | * レスポンス: `GET /messages/:name` と同様 232 | 233 | #### POST /messages?qre=:regexp 234 | * 概要: メッセージを regexp にマッチする**全ての**キューに追加する 235 | * パラメータ: 236 | * qre: 対象のキューを絞り込む正規表現 237 | * リクエストボディ: メッセージの内容となるデータ 238 | * レスポンスコード: `200 OK` 239 | * レスポンスボディ: 240 | 241 | ```json 242 | { 243 | "QUEUE NAME 1": {"accum": "FLAG 1"}, 244 | "QUEUE NAME 2": {"accum": "FLAG 2"}, 245 | ... 246 | } 247 | ``` 248 | 249 | * QUEUE NAME N: メッセージを追加したキュー名 250 | * FLAG N: 集約処理が行われたかどうか 251 | * new: 新たに集約が開始された 252 | * yes: 既存のメッセージと集約された 253 | * no: 集約されなかった 254 | 255 | #### POST /messages/:name/:msgid?reply=:type 256 | * 概要: 取得したメッセージの処理結果を通知する 257 | * パラメータ: 258 | * name: キュー名 259 | * msgid: 対象メッセージの ID 260 | * type: 処理結果 261 | * ack: 処理が正常に終了 -> メッセージをキューから削除 262 | * nack: 処理が継続できなくなった -> メッセージをキューに戻す 263 | * ext: 処理に時間がかかっている -> メッセージの処理可能時間を延長 264 | * レスポンスコード: `204 No Content` 265 | 266 | ### キュー操作 267 | #### DELETE /queues/:name 268 | * 概要: キューを削除する。キュー内のメッセージは全て破棄される 269 | * パラメータ: 270 | * name: キュー名 271 | * レスポンスコード: `204 No Content` 272 | 273 | ### プロパティ操作 274 | #### GET /properties 275 | * 概要: デフォルトプロパティを取得する 276 | * レスポンスコード: `200 OK` 277 | * レスポンスボディ: 278 | 279 | ```json 280 | [[REGEXP, PROPERTIES], ...] 281 | ``` 282 | 283 | * REGEXP: 対象のキューを絞り込む正規表現 284 | * PROPERTIES: REGEXP にマッチしたキューに設定するプロパティ 285 | 286 | #### PUT /properties 287 | * 概要: デフォルトプロパティを設定する。既存の設定があれば上書きされる 288 | * リクエストボディ: `GET /properties` のレスポンスと同様 289 | * レスポンスコード: `204 No Content` 290 | 291 | #### DELETE /properties 292 | * 概要: デフォルトプロパティを初期化する 293 | * レスポンスコード: `204 No Content` 294 | 295 | #### GET /properties/:name 296 | * 概要: キューのプロパティを取得する 297 | * パラメータ: 298 | * name: キュー名 299 | * レスポンスコード: `200 OK` 300 | * レスポンスボディ: 301 | 302 | ```json 303 | { 304 | "accum": ACCUM, 305 | "retry": RETRY, 306 | "timeout": TIMEOUT 307 | } 308 | ``` 309 | 310 | 詳細は[キューのプロパティ](#properties)を参照 311 | 312 | #### PATCH /properties/:name 313 | * 概要: キューのプロパティを部分的に更新する。指定しなかったプロパティは既存の値を踏襲する 314 | * パラメータ: 315 | * name: キュー名 316 | * リクエストボディ: 317 | プロパティの内、変更したいものだけを記したもの 318 | 319 | 例: 320 | 321 | ```json 322 | {"accum": 30} 323 | {"accum": 30, "retry": 5} 324 | ``` 325 | 326 | * レスポンスコード: `204 No Content` 327 | 328 | #### DELETE /properties/:name 329 | * 概要: キューのプロパティを初期化する 330 | * パラメータ: 331 | * name: キュー名 332 | * レスポンスコード: `204 No Content` 333 | 334 | ## MessagePack-RPC API 335 | 336 | LMQ は MessagePack-RPC インタフェースを 18800 番ポートで提供しています。ここでは、各 API について `method(args) -> return_value` の形式で説明します。 337 | 338 | 戻り値は正常に処理された時のものについて記載します。各 API はエラーを返す可能性がある点に留意してください。 339 | 340 | *MessagePack-RPC API は、後方互換性のために 0.6 でのメッセージ集約プロパティの変更の影響を受けていません。そのため、HTTP API とは一部異なります。* 341 | 342 | ### Request Message 343 | 344 | #### create(name[, property]) -> "ok" 345 | **この API は 0.4.0 で削除されました。代わりに `update_props` を使用してください。** 346 | 347 | #### delete(name) -> "ok" 348 | 349 | 指定したキューを削除します。 350 | 351 |
352 |
name (string)
削除するキューの名前
353 |
354 | 355 | #### push(name, content) -> "ok" 356 | 指定したキューにメッセージを投入します。キューがなければ作成します。 357 | 358 |
359 |
name (string)
キューの名前
360 |
content (object)
投入するデータで、形式は問わない
361 |
362 | 363 | #### pull(name[, timeout]) -> {"queue": name, "id": id, "type": type, "content": content} | "empty" 364 | 指定したキューからメッセージを取り出します。キューがなければ作成します。 365 | 366 | timeout を指定し、タイムアウトした時は `empty` 文字列が返ります。 367 | 368 | `type` が "package" の場合、`content` は `push` されたデータの配列になります。 369 | 370 |
371 |
name (string)
キューの名前
372 |
timeout (float)
タイムアウトまでの秒数
373 |
id (string)
メッセージの ID
374 |
type (string)
メッセージのタイプ。normal: 通常、package: パッケージ
375 |
content
push() により投入されたデータ
376 |
377 | 378 | #### push_all(regexp, content) -> "ok" 379 | 380 | 正規表現にマッチする全てのキューにメッセージを投入します。 381 | 382 |
383 |
regexp (string)
正規表現
384 |
content (object)
投入するデータで、形式は問わない
385 |
386 | 387 | #### pull_any(regexp[, timeout]) -> {"queue": name, "id": id, "type": type, "content": content} | "empty" 388 | 389 | 正規表現にマッチするキューの中から、最も早く取り出せたメッセージを取得します。 390 | 391 | timeout を指定し、タイムアウトした時は `empty` 文字列が返ります。 392 | 393 | `type` が "package" の場合、`content` は `push` されたデータの配列になります。 394 | 395 |
396 |
regexp (string)
正規表現
397 |
timeout (float)
タイムアウトまでの秒数
398 |
name (string)
キューの名前
399 |
id (string)
メッセージの ID
400 |
type (string)
メッセージのタイプ。normal: 通常、package: パッケージ
401 |
content
push() により投入されたデータ
402 |
403 | 404 | #### done(name, id) -> "ok" 405 | 406 | メッセージの完了報告をし、キューからメッセージを取り除きます。pull() により取り出されたメッセージが一定時間内に完了しなかった場合、LMQ は自動的にメッセージを再送します。 407 | 408 | メッセージが再送されると、古い ID は無効になり、完了報告が失敗します。 409 | 410 |
411 |
name (string)
キューの名前
412 |
id (string)
完了報告するメッセージの ID
413 |
414 | 415 | #### retain(name, id) -> "ok" 416 | 417 | メッセージの再送タイマーをリセットします。 418 | これにより、再送までの時間を延長することができます。 419 | 420 | メッセージが既に再送されていた場合、呼び出しは失敗します。 421 | 422 |
423 |
name (string)
キューの名前
424 |
id (string)
再送をリセットするメッセージの ID
425 |
426 | 427 | #### release(name, id) -> "ok" 428 | 429 | メッセージをキューに戻します。メッセージは即座に再送されます。 430 | 431 |
432 |
name (string)
キューの名前
433 |
id (string)
キューに戻すメッセージの ID
434 |
435 | 436 | #### update_props(name[, property]) -> "ok" 437 | キューのプロパティを更新します。キューがなければ作成します。 438 | 439 | 変更したいプロパティだけ指定すれば、残りはデフォルト値が使用されます。 440 | また、`property` 自体を省略すると、全てデフォルト値に設定されます。 441 | 442 |
443 |
name (string)
キューの名前
444 |
property (dict)
キューの動作に関わるプロパティ
445 |
446 | 447 | ### set_default_props(props_list) -> "ok" 448 | デフォルトプロパティを設定します。既存の内容は完全に上書きされます。 449 | 450 | props_list は [[regexp, property], ...] の構造を持つリストです。 451 | キューの作成時に、新しいキューの名前が regexp にマッチするかを先頭から評価していき、初めてマッチした property を使用します。どのルールにもマッチしなければ、システムのデフォルトを使用します。 452 | 453 | ### get_default_props() -> props_list 454 | 設定されているデフォルトプロパティを取得する。 455 | -------------------------------------------------------------------------------- /bump-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $# -ne 1 ]; then 6 | echo "usage: $0 VERSION" 7 | exit 1 8 | fi 9 | 10 | sed -i .bak -e "s/{vsn, \".*\"}/{vsn, \"$1\"}/" src/lmq.app.src 11 | rm src/lmq.app.src.bak 12 | 13 | sed -i .bak -e "s/{rel, \"lmq\", \".*\"/{rel, \"lmq\", \"$1\"/" rel/reltool.config 14 | rm rel/reltool.config.bak 15 | 16 | echo Files updated successfully, version bumped to $1 17 | exit 0 18 | -------------------------------------------------------------------------------- /include/lmq.hrl: -------------------------------------------------------------------------------- 1 | -record(message, {id={lmq_misc:unixtime(), uuid:get_v4()}, 2 | state=available, type=normal, retry=0, content}). 3 | -record(queue_info, {name, props}). 4 | -record(lmq_info, {key, value}). 5 | 6 | -define(DEFAULT_QUEUE_PROPS, [{accum, 0}, {retry, 2}, {timeout, 30}]). 7 | -define(LMQ_INFO_TABLE, '__lmq_info__'). 8 | -define(LMQ_INFO_TABLE_DEFS, [{type, set}, 9 | {attributes, record_info(fields, lmq_info)}, 10 | {record_name, lmq_info}]). 11 | -define(QUEUE_INFO_TABLE, '__lmq_queue_info__'). 12 | -define(QUEUE_INFO_TABLE_DEFS, [{type, set}, 13 | {attributes, record_info(fields, queue_info)}, 14 | {record_name, queue_info}]). 15 | 16 | -define(LMQ_ALL_METRICS, all). 17 | -define(STATSD_SAMPLERATE, 0.5). 18 | -------------------------------------------------------------------------------- /include/lmq_test.hrl: -------------------------------------------------------------------------------- 1 | -define(LMQ_EVENT, lmq_event). 2 | 3 | -define(EVENT_OR_FAIL(Event), receive {test_handler, Event} -> ok 4 | after 50 -> ct:fail(no_response) 5 | end). 6 | -define(EVENT_AND_FAIL(Event), receive {test_handler, Event} -> 7 | ct:fail(invalid_event) 8 | after 50 -> ok 9 | end). 10 | -------------------------------------------------------------------------------- /lmq.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | {"path": ".", 4 | "folder_exclude_patterns": [".eunit", "logs", "log", "Mnesia.*", "deps"], 5 | "file_exclude_patterns": ["*.beam"]} 6 | ], 7 | "settings": { 8 | "tab_size": 4 9 | }, 10 | "build_systems": [ 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iij/lmq/3f01c555af973a07a3f2b22ff95a2bc1c7930bc2/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {sub_dirs, ["rel"]}. 2 | {erl_opts, [warnings_as_errors, 3 | {parse_transform, lager_transform}, 4 | debug_info]}. 5 | {cover_enabled, true}. 6 | {eunit_opts, [verbose]}. 7 | 8 | {deps, [ 9 | {lager, "2.0.0", 10 | {git, "git://github.com/basho/lager", {tag, "2.0.0"}}}, 11 | {uuid, ".*", 12 | {git, "git://github.com/okeuday/uuid.git", {branch, "master"}}}, 13 | {msgpack_rpc, "0.6.3", 14 | {git, "git://github.com/msgpack-rpc/msgpack-rpc-erlang.git", {branch, "master"}}}, 15 | {folsom, "0.7.4", 16 | {git, "git://github.com/boundary/folsom.git", {tag, "0.7.4"}}}, 17 | {cowboy, ".*", 18 | {git, "git://github.com/ninenines/cowboy.git", {tag, "1.0.0"}}}, 19 | {ibrowse, ".*", 20 | {git, "git://github.com/cmullaparthi/ibrowse.git", {branch, "master"}}}, 21 | {jsonx, ".*", 22 | {git, "git://github.com/iskra/jsonx.git", {branch, "master"}}}, 23 | {statsderl, ".*", 24 | {git, "git://github.com/lpgauth/statsderl.git", {branch, "master"}}}, 25 | {eper, ".*", 26 | {git, "git://github.com/massemanet/eper.git", {branch, "master"}}} 27 | ]}. 28 | -------------------------------------------------------------------------------- /rel/files/app.config: -------------------------------------------------------------------------------- 1 | [ 2 | %% LMQ config 3 | {lmq, [ 4 | {port, 18800}, 5 | {stats_interval, 20000}, 6 | {influxdb, [ 7 | {host, "localhost"}, 8 | {port, 4444} 9 | ]} 10 | ]}, 11 | 12 | %% Lager config 13 | {lager, [ 14 | {handlers, [ 15 | {lager_file_backend, [ 16 | [{file, "./log/error.log"}, {level, error}, 17 | {size, 0}, %% do not rotate by size 18 | {date, "$D0"}, %% rotate at midnight 19 | {count, 14}], %% save recent 2 weeks log 20 | [{file, "./log/console.log"}, {level, info}, 21 | {size, 0}, {date, "$D0"}, {count, 14}] 22 | ]} 23 | ]}, 24 | {crash_log, "./log/crash.log"}, 25 | {crash_log_msg_size, 65536}, 26 | {crash_log_size, 0}, 27 | {crash_log_date, "$D0"}, 28 | {crash_log_count, 14}, 29 | {error_logger_redirect, true} 30 | ]}, 31 | 32 | %% StatsD config 33 | {statsderl, [ 34 | {hostname, "localhost"}, 35 | {port, 8125} 36 | ]}, 37 | 38 | %% SASL config 39 | {sasl, [ 40 | {sasl_error_logger, false} 41 | ]} 42 | ]. 43 | -------------------------------------------------------------------------------- /rel/files/erl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script replaces the default "erl" in erts-VSN/bin. This is necessary 4 | ## as escript depends on erl and in turn, erl depends on having access to a 5 | ## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect 6 | ## of running escript -- the embedded node bypasses erl and uses erlexec directly 7 | ## (as it should). 8 | ## 9 | ## Note that this script makes the assumption that there is a start_clean.boot 10 | ## file available in $ROOTDIR/release/VSN. 11 | 12 | # Determine the abspath of where this script is executing from. 13 | ERTS_BIN_DIR=$(cd ${0%/*} && pwd) 14 | 15 | # Now determine the root directory -- this script runs from erts-VSN/bin, 16 | # so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR 17 | # path. 18 | ROOTDIR=${ERTS_BIN_DIR%/*/*} 19 | 20 | # Parse out release and erts info 21 | START_ERL=`cat $ROOTDIR/releases/start_erl.data` 22 | ERTS_VSN=${START_ERL% *} 23 | APP_VSN=${START_ERL#* } 24 | 25 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 26 | EMU=beam 27 | PROGNAME=`echo $0 | sed 's/.*\\///'` 28 | CMD="$BINDIR/erlexec" 29 | export EMU 30 | export ROOTDIR 31 | export BINDIR 32 | export PROGNAME 33 | 34 | exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} 35 | -------------------------------------------------------------------------------- /rel/files/install_upgrade.escript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -noshell -noinput 3 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 4 | %% ex: ft=erlang ts=4 sw=4 et 5 | 6 | -define(TIMEOUT, 60000). 7 | -define(INFO(Fmt,Args), io:format(Fmt,Args)). 8 | 9 | main([NodeName, Cookie, ReleasePackage]) -> 10 | TargetNode = start_distribution(NodeName, Cookie), 11 | {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release, 12 | [ReleasePackage], ?TIMEOUT), 13 | ?INFO("Unpacked Release ~p~n", [Vsn]), 14 | {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, 15 | check_install_release, [Vsn], ?TIMEOUT), 16 | {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, 17 | install_release, [Vsn], ?TIMEOUT), 18 | ?INFO("Installed Release ~p~n", [Vsn]), 19 | ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), 20 | ?INFO("Made Release ~p Permanent~n", [Vsn]); 21 | main(_) -> 22 | init:stop(1). 23 | 24 | start_distribution(NodeName, Cookie) -> 25 | MyNode = make_script_node(NodeName), 26 | {ok, _Pid} = net_kernel:start([MyNode, shortnames]), 27 | erlang:set_cookie(node(), list_to_atom(Cookie)), 28 | TargetNode = make_target_node(NodeName), 29 | case {net_kernel:hidden_connect_node(TargetNode), 30 | net_adm:ping(TargetNode)} of 31 | {true, pong} -> 32 | ok; 33 | {_, pang} -> 34 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 35 | init:stop(1) 36 | end, 37 | TargetNode. 38 | 39 | make_target_node(Node) -> 40 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 41 | list_to_atom(lists:concat([Node, "@", Host])). 42 | 43 | make_script_node(Node) -> 44 | list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])). 45 | -------------------------------------------------------------------------------- /rel/files/lmq: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- tab-width:4;indent-tabs-mode:nil -*- 3 | # ex: ts=4 sw=4 et 4 | 5 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) 6 | 7 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} 8 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc 9 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log 10 | # Note the trailing slash on $PIPE_DIR/ 11 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ 12 | RUNNER_USER= 13 | 14 | # Make sure this script is running as the appropriate user 15 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then 16 | exec sudo -u $RUNNER_USER -i $0 $@ 17 | fi 18 | 19 | # Make sure CWD is set to runner base dir 20 | cd $RUNNER_BASE_DIR 21 | 22 | # Make sure log directory exists 23 | mkdir -p $RUNNER_LOG_DIR 24 | # Identify the script name 25 | SCRIPT=`basename $0` 26 | 27 | # Parse out release and erts info 28 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 29 | ERTS_VSN=${START_ERL% *} 30 | APP_VSN=${START_ERL#* } 31 | 32 | # Use releases/VSN/vm.args if it exists otherwise use etc/vm.args 33 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" ]; then 34 | VMARGS_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" 35 | else 36 | VMARGS_PATH="$RUNNER_ETC_DIR/vm.args" 37 | fi 38 | 39 | # Use releases/VSN/sys.config if it exists otherwise use etc/app.config 40 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" ]; then 41 | CONFIG_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" 42 | else 43 | CONFIG_PATH="$RUNNER_ETC_DIR/app.config" 44 | fi 45 | 46 | # Extract the target node name from node.args 47 | NAME_ARG=`egrep '^-s?name' $VMARGS_PATH` 48 | if [ -z "$NAME_ARG" ]; then 49 | echo "vm.args needs to have either -name or -sname parameter." 50 | exit 1 51 | fi 52 | 53 | # Extract the name type and name from the NAME_ARG for REMSH 54 | REMSH_TYPE=`echo $NAME_ARG | awk '{print $1}'` 55 | REMSH_NAME=`echo $NAME_ARG | awk '{print $2}'` 56 | 57 | # Note the `date +%s`, used to allow multiple remsh to the same node transparently 58 | REMSH_NAME_ARG="$REMSH_TYPE remsh`date +%s`@`echo $REMSH_NAME | awk -F@ '{print $2}'`" 59 | REMSH_REMSH_ARG="-remsh $REMSH_NAME" 60 | 61 | # Extract the target cookie 62 | COOKIE_ARG=`grep '^-setcookie' $VMARGS_PATH` 63 | if [ -z "$COOKIE_ARG" ]; then 64 | echo "vm.args needs to have a -setcookie parameter." 65 | exit 1 66 | fi 67 | 68 | # Add ERTS bin dir to our path 69 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 70 | 71 | # Setup command to control the node 72 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 73 | 74 | # Setup remote shell command to control node 75 | REMSH="$ERTS_PATH/erl $REMSH_NAME_ARG $REMSH_REMSH_ARG $COOKIE_ARG" 76 | 77 | # Check the first argument for instructions 78 | case "$1" in 79 | start) 80 | # Make sure there is not already a node running 81 | RES=`$NODETOOL ping` 82 | if [ "$RES" = "pong" ]; then 83 | echo "Node is already running!" 84 | exit 1 85 | fi 86 | shift # remove $1 87 | RUN_PARAM=$(printf "\'%s\' " "$@") 88 | HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start $RUN_PARAM" 89 | export HEART_COMMAND 90 | mkdir -p $PIPE_DIR 91 | $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console $RUN_PARAM" 2>&1 92 | ;; 93 | 94 | stop) 95 | # Wait for the node to completely stop... 96 | case `uname -s` in 97 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) 98 | # PID COMMAND 99 | PID=`ps ax -o pid= -o command=|\ 100 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 101 | ;; 102 | SunOS) 103 | # PID COMMAND 104 | PID=`ps -ef -o pid= -o args=|\ 105 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 106 | ;; 107 | CYGWIN*) 108 | # UID PID PPID TTY STIME COMMAND 109 | PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'` 110 | ;; 111 | esac 112 | $NODETOOL stop 113 | ES=$? 114 | if [ "$ES" -ne 0 ]; then 115 | exit $ES 116 | fi 117 | while `kill -0 $PID 2>/dev/null`; 118 | do 119 | sleep 1 120 | done 121 | ;; 122 | 123 | restart) 124 | ## Restart the VM without exiting the process 125 | $NODETOOL restart 126 | ES=$? 127 | if [ "$ES" -ne 0 ]; then 128 | exit $ES 129 | fi 130 | ;; 131 | 132 | reboot) 133 | ## Restart the VM completely (uses heart to restart it) 134 | $NODETOOL reboot 135 | ES=$? 136 | if [ "$ES" -ne 0 ]; then 137 | exit $ES 138 | fi 139 | ;; 140 | 141 | ping) 142 | ## See if the VM is alive 143 | $NODETOOL ping 144 | ES=$? 145 | if [ "$ES" -ne 0 ]; then 146 | exit $ES 147 | fi 148 | ;; 149 | 150 | attach) 151 | # Make sure a node IS running 152 | RES=`$NODETOOL ping` 153 | ES=$? 154 | if [ "$ES" -ne 0 ]; then 155 | echo "Node is not running!" 156 | exit $ES 157 | fi 158 | 159 | shift 160 | exec $ERTS_PATH/to_erl $PIPE_DIR 161 | ;; 162 | 163 | remote_console) 164 | # Make sure a node IS running 165 | RES=`$NODETOOL ping` 166 | ES=$? 167 | if [ "$ES" -ne 0 ]; then 168 | echo "Node is not running!" 169 | exit $ES 170 | fi 171 | 172 | shift 173 | exec $REMSH 174 | ;; 175 | 176 | upgrade) 177 | if [ -z "$2" ]; then 178 | echo "Missing upgrade package argument" 179 | echo "Usage: $SCRIPT upgrade {package base name}" 180 | echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" 181 | exit 1 182 | fi 183 | 184 | # Make sure a node IS running 185 | RES=`$NODETOOL ping` 186 | ES=$? 187 | if [ "$ES" -ne 0 ]; then 188 | echo "Node is not running!" 189 | exit $ES 190 | fi 191 | 192 | node_name=`echo $NAME_ARG | awk '{print $2}'` 193 | erlang_cookie=`echo $COOKIE_ARG | awk '{print $2}'` 194 | 195 | $ERTS_PATH/escript $RUNNER_BASE_DIR/bin/install_upgrade.escript $node_name $erlang_cookie $2 196 | ;; 197 | 198 | console|console_clean) 199 | # .boot file typically just $SCRIPT (ie, the app name) 200 | # however, for debugging, sometimes start_clean.boot is useful: 201 | case "$1" in 202 | console) BOOTFILE=$SCRIPT ;; 203 | console_clean) BOOTFILE=start_clean ;; 204 | esac 205 | # Setup beam-required vars 206 | ROOTDIR=$RUNNER_BASE_DIR 207 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 208 | EMU=beam 209 | PROGNAME=`echo $0 | sed 's/.*\\///'` 210 | CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -mode embedded -config $CONFIG_PATH -args_file $VMARGS_PATH" 211 | export EMU 212 | export ROOTDIR 213 | export BINDIR 214 | export PROGNAME 215 | 216 | # Dump environment info for logging purposes 217 | echo "Exec: $CMD" -- ${1+"$@"} 218 | echo "Root: $ROOTDIR" 219 | 220 | # Log the startup 221 | logger -t "$SCRIPT[$$]" "Starting up" 222 | 223 | # Start the VM 224 | exec $CMD -- ${1+"$@"} 225 | ;; 226 | 227 | foreground) 228 | # start up the release in the foreground for use by runit 229 | # or other supervision services 230 | 231 | BOOTFILE=$SCRIPT 232 | FOREGROUNDOPTIONS="-noinput +Bd" 233 | 234 | # Setup beam-required vars 235 | ROOTDIR=$RUNNER_BASE_DIR 236 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 237 | EMU=beam 238 | PROGNAME=`echo $0 | sed 's/.*\///'` 239 | CMD="$BINDIR/erlexec $FOREGROUNDOPTIONS -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -config $CONFIG_PATH -args_file $VMARGS_PATH" 240 | export EMU 241 | export ROOTDIR 242 | export BINDIR 243 | export PROGNAME 244 | 245 | # Dump environment info for logging purposes 246 | echo "Exec: $CMD" -- ${1+"$@"} 247 | echo "Root: $ROOTDIR" 248 | 249 | # Start the VM 250 | exec $CMD -- ${1+"$@"} 251 | ;; 252 | *) 253 | echo "Usage: $SCRIPT {start|foreground|stop|restart|reboot|ping|console|console_clean|attach|remote_console|upgrade}" 254 | exit 1 255 | ;; 256 | esac 257 | 258 | exit 0 259 | -------------------------------------------------------------------------------- /rel/files/lmq-admin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- tab-width:4;indent-tabs-mode:nil -*- 3 | # ex: ts=4 sw=4 et 4 | 5 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) 6 | 7 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} 8 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc 9 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log 10 | # Note the trailing slash on $PIPE_DIR/ 11 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ 12 | RUNNER_USER= 13 | 14 | # Make sure this script is running as the appropriate user 15 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then 16 | exec sudo -u $RUNNER_USER -i $0 $@ 17 | fi 18 | 19 | # Make sure CWD is set to runner base dir 20 | cd $RUNNER_BASE_DIR 21 | 22 | # Make sure log directory exists 23 | mkdir -p $RUNNER_LOG_DIR 24 | # Identify the script name 25 | SCRIPT=`basename $0` 26 | 27 | # Parse out release and erts info 28 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 29 | ERTS_VSN=${START_ERL% *} 30 | APP_VSN=${START_ERL#* } 31 | 32 | # Use releases/VSN/vm.args if it exists otherwise use etc/vm.args 33 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" ]; then 34 | VMARGS_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" 35 | else 36 | VMARGS_PATH="$RUNNER_ETC_DIR/vm.args" 37 | fi 38 | 39 | # Use releases/VSN/sys.config if it exists otherwise use etc/app.config 40 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" ]; then 41 | CONFIG_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" 42 | else 43 | CONFIG_PATH="$RUNNER_ETC_DIR/app.config" 44 | fi 45 | 46 | # Extract the target node name from node.args 47 | NAME_ARG=`egrep '^-s?name' $VMARGS_PATH` 48 | if [ -z "$NAME_ARG" ]; then 49 | echo "vm.args needs to have either -name or -sname parameter." 50 | exit 1 51 | fi 52 | 53 | # Extract the name type and name from the NAME_ARG for REMSH 54 | REMSH_TYPE=`echo $NAME_ARG | awk '{print $1}'` 55 | REMSH_NAME=`echo $NAME_ARG | awk '{print $2}'` 56 | 57 | # Note the `date +%s`, used to allow multiple remsh to the same node transparently 58 | REMSH_NAME_ARG="$REMSH_TYPE remsh`date +%s`@`echo $REMSH_NAME | awk -F@ '{print $2}'`" 59 | REMSH_REMSH_ARG="-remsh $REMSH_NAME" 60 | 61 | # Extract the target cookie 62 | COOKIE_ARG=`grep '^-setcookie' $VMARGS_PATH` 63 | if [ -z "$COOKIE_ARG" ]; then 64 | echo "vm.args needs to have a -setcookie parameter." 65 | exit 1 66 | fi 67 | 68 | # Add ERTS bin dir to our path 69 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 70 | 71 | # Setup command to control the node 72 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 73 | 74 | # Setup remote shell command to control node 75 | REMSH="$ERTS_PATH/erl $REMSH_NAME_ARG $REMSH_REMSH_ARG $COOKIE_ARG" 76 | 77 | ensure_node_is_up() { 78 | if ! $NODETOOL ping > /dev/null; then 79 | echo "Node is not running!" 80 | exit 1 81 | fi 82 | } 83 | 84 | # Check the first argument for instructions 85 | case "$1" in 86 | join) 87 | if [ -z $2 ]; then 88 | echo "Usage: $SCRIPT join " 89 | exit 1 90 | fi 91 | ensure_node_is_up 92 | $NODETOOL rpc lmq_console join "$2" 93 | ;; 94 | leave) 95 | ensure_node_is_up 96 | $NODETOOL rpc lmq_console leave 97 | ;; 98 | status) 99 | ensure_node_is_up 100 | $NODETOOL rpc lmq_console status 101 | ;; 102 | stats) 103 | ensure_node_is_up 104 | $NODETOOL rpc lmq_console stats 105 | ;; 106 | *) 107 | echo "Usage: $SCRIPT {join|leave|status|stats}" 108 | exit 1 109 | ;; 110 | esac 111 | 112 | exit 0 113 | -------------------------------------------------------------------------------- /rel/files/lmq.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @set node_name=lmq 4 | 5 | @rem Get the absolute path to the parent directory, 6 | @rem which is assumed to be the node root. 7 | @for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI 8 | 9 | @set releases_dir=%node_root%\releases 10 | 11 | @rem Parse ERTS version and release version from start_erl.data 12 | @for /F "tokens=1,2" %%I in (%releases_dir%\start_erl.data) do @( 13 | @call :set_trim erts_version %%I 14 | @call :set_trim release_version %%J 15 | ) 16 | 17 | @rem extract erlang cookie from vm.args 18 | @set vm_args=%releases_dir%\%release_version%\vm.args 19 | @for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie %vm_args%`) do @set erlang_cookie=%%J 20 | 21 | @set erts_bin=%node_root%\erts-%erts_version%\bin 22 | 23 | @set service_name=%node_name%_%release_version% 24 | 25 | @if "%1"=="usage" @goto usage 26 | @if "%1"=="install" @goto install 27 | @if "%1"=="uninstall" @goto uninstall 28 | @if "%1"=="start" @goto start 29 | @if "%1"=="stop" @goto stop 30 | @if "%1"=="restart" @call :stop && @goto start 31 | @if "%1"=="console" @goto console 32 | @if "%1"=="query" @goto query 33 | @if "%1"=="attach" @goto attach 34 | @if "%1"=="upgrade" @goto upgrade 35 | @echo Unknown command: "%1" 36 | 37 | :usage 38 | @echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade] 39 | @goto :EOF 40 | 41 | :install 42 | @%erts_bin%\erlsrv.exe add %service_name% -c "Erlang node %node_name% in %node_root%" -sname %node_name% -w %node_root% -m %node_root%\bin\start_erl.cmd -args " ++ %node_name% ++ %node_root%" -stopaction "init:stop()." 43 | @goto :EOF 44 | 45 | :uninstall 46 | @%erts_bin%\erlsrv.exe remove %service_name% 47 | @%erts_bin%\epmd.exe -kill 48 | @goto :EOF 49 | 50 | :start 51 | @%erts_bin%\erlsrv.exe start %service_name% 52 | @goto :EOF 53 | 54 | :stop 55 | @%erts_bin%\erlsrv.exe stop %service_name% 56 | @goto :EOF 57 | 58 | :console 59 | @start %erts_bin%\werl.exe -boot %releases_dir%\%release_version%\%node_name% -config %releases_dir%\%release_version%\sys.config -args_file %vm_args% -sname %node_name% 60 | @goto :EOF 61 | 62 | :query 63 | @%erts_bin%\erlsrv.exe list %service_name% 64 | @exit /b %ERRORLEVEL% 65 | @goto :EOF 66 | 67 | :attach 68 | @for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I 69 | start %erts_bin%\werl.exe -boot %releases_dir%\%release_version%\start_clean -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie% 70 | @goto :EOF 71 | 72 | :upgrade 73 | @if "%2"=="" ( 74 | @echo Missing upgrade package argument 75 | @echo Usage: %~n0 upgrade {package base name} 76 | @echo NOTE {package base name} MUST NOT include the .tar.gz suffix 77 | @goto :EOF 78 | ) 79 | @%erts_bin%\escript.exe %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2 80 | @goto :EOF 81 | 82 | :set_trim 83 | @set %1=%2 84 | @goto :EOF 85 | -------------------------------------------------------------------------------- /rel/files/nodetool: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ft=erlang ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% nodetool: Helper Script for interacting with live nodes 6 | %% 7 | %% ------------------------------------------------------------------- 8 | 9 | main(Args) -> 10 | ok = start_epmd(), 11 | %% Extract the args 12 | {RestArgs, TargetNode} = process_args(Args, [], undefined), 13 | 14 | %% See if the node is currently running -- if it's not, we'll bail 15 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of 16 | {true, pong} -> 17 | ok; 18 | {_, pang} -> 19 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 20 | halt(1) 21 | end, 22 | 23 | case RestArgs of 24 | ["ping"] -> 25 | %% If we got this far, the node already responsed to a ping, so just dump 26 | %% a "pong" 27 | io:format("pong\n"); 28 | ["stop"] -> 29 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); 30 | ["restart"] -> 31 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); 32 | ["reboot"] -> 33 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); 34 | ["rpc", Module, Function | RpcArgs] -> 35 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 36 | [RpcArgs], 60000) of 37 | ok -> 38 | ok; 39 | {badrpc, Reason} -> 40 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 41 | halt(1); 42 | _ -> 43 | halt(1) 44 | end; 45 | ["rpcterms", Module, Function, ArgsAsString] -> 46 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 47 | consult(ArgsAsString), 60000) of 48 | {badrpc, Reason} -> 49 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 50 | halt(1); 51 | Other -> 52 | io:format("~p\n", [Other]) 53 | end; 54 | Other -> 55 | io:format("Other: ~p\n", [Other]), 56 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n") 57 | end, 58 | net_kernel:stop(). 59 | 60 | process_args([], Acc, TargetNode) -> 61 | {lists:reverse(Acc), TargetNode}; 62 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> 63 | erlang:set_cookie(node(), list_to_atom(Cookie)), 64 | process_args(Rest, Acc, TargetNode); 65 | process_args(["-name", TargetName | Rest], Acc, _) -> 66 | ThisNode = append_node_suffix(TargetName, "_maint_"), 67 | {ok, _} = net_kernel:start([ThisNode, longnames]), 68 | process_args(Rest, Acc, nodename(TargetName)); 69 | process_args(["-sname", TargetName | Rest], Acc, _) -> 70 | ThisNode = append_node_suffix(TargetName, "_maint_"), 71 | {ok, _} = net_kernel:start([ThisNode, shortnames]), 72 | process_args(Rest, Acc, nodename(TargetName)); 73 | process_args([Arg | Rest], Acc, Opts) -> 74 | process_args(Rest, [Arg | Acc], Opts). 75 | 76 | 77 | start_epmd() -> 78 | [] = os:cmd(epmd_path() ++ " -daemon"), 79 | ok. 80 | 81 | epmd_path() -> 82 | ErtsBinDir = filename:dirname(escript:script_name()), 83 | Name = "epmd", 84 | case os:find_executable(Name, ErtsBinDir) of 85 | false -> 86 | case os:find_executable(Name) of 87 | false -> 88 | io:format("Could not find epmd.~n"), 89 | halt(1); 90 | GlobalEpmd -> 91 | GlobalEpmd 92 | end; 93 | Epmd -> 94 | Epmd 95 | end. 96 | 97 | 98 | nodename(Name) -> 99 | case string:tokens(Name, "@") of 100 | [_Node, _Host] -> 101 | list_to_atom(Name); 102 | [Node] -> 103 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 104 | list_to_atom(lists:concat([Node, "@", Host])) 105 | end. 106 | 107 | append_node_suffix(Name, Suffix) -> 108 | case string:tokens(Name, "@") of 109 | [Node, Host] -> 110 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); 111 | [Node] -> 112 | list_to_atom(lists:concat([Node, Suffix, os:getpid()])) 113 | end. 114 | 115 | 116 | %% 117 | %% Given a string or binary, parse it into a list of terms, ala file:consult/0 118 | %% 119 | consult(Str) when is_list(Str) -> 120 | consult([], Str, []); 121 | consult(Bin) when is_binary(Bin)-> 122 | consult([], binary_to_list(Bin), []). 123 | 124 | consult(Cont, Str, Acc) -> 125 | case erl_scan:tokens(Cont, Str, 0) of 126 | {done, Result, Remaining} -> 127 | case Result of 128 | {ok, Tokens, _} -> 129 | {ok, Term} = erl_parse:parse_term(Tokens), 130 | consult([], Remaining, [Term | Acc]); 131 | {eof, _Other} -> 132 | lists:reverse(Acc); 133 | {error, Info, _} -> 134 | {error, Info} 135 | end; 136 | {more, Cont1} -> 137 | consult(Cont1, eof, Acc) 138 | end. 139 | -------------------------------------------------------------------------------- /rel/files/start_erl.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @rem Parse arguments. erlsrv.exe prepends erl arguments prior to first ++. 4 | @rem Other args are position dependent. 5 | @set args="%*" 6 | @for /F "delims=++ tokens=1,2,3" %%I in (%args%) do @( 7 | @set erl_args=%%I 8 | @call :set_trim node_name %%J 9 | @call :set_trim node_root %%K 10 | ) 11 | 12 | @set releases_dir=%node_root%\releases 13 | 14 | @rem parse ERTS version and release version from start_erl.dat 15 | @for /F "tokens=1,2" %%I in (%releases_dir%\start_erl.data) do @( 16 | @call :set_trim erts_version %%I 17 | @call :set_trim release_version %%J 18 | ) 19 | 20 | @set erl_exe=%node_root%\erts-%erts_version%\bin\erl.exe 21 | @set boot_file=%releases_dir%\%release_version%\%node_name% 22 | 23 | @if exist %releases_dir%\%release_version%\sys.config ( 24 | @set app_config=%releases_dir%\%release_version%\sys.config 25 | ) else ( 26 | @set app_config=%node_root%\etc\app.config 27 | ) 28 | 29 | @if exist %releases_dir%\%release_version%\vm.args ( 30 | @set vm_args=%releases_dir%\%release_version%\vm.args 31 | ) else ( 32 | @set vm_args=%node_root%\etc\vm.args 33 | ) 34 | 35 | @%erl_exe% %erl_args% -boot %boot_file% -config %app_config% -args_file %vm_args% 36 | 37 | :set_trim 38 | @set %1=%2 39 | @goto :EOF 40 | -------------------------------------------------------------------------------- /rel/files/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name lmq@127.0.0.1 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie lmq 6 | 7 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive 8 | ## (Disabled by default..use with caution!) 9 | ##-heart 10 | 11 | ## Enable kernel poll and a few async threads 12 | ##+K true 13 | ##+A 5 14 | 15 | ## Increase number of concurrent ports/sockets 16 | ##-env ERL_MAX_PORTS 4096 17 | 18 | ## Tweak GC to run more often 19 | ##-env ERL_FULLSWEEP_AFTER 10 20 | -------------------------------------------------------------------------------- /rel/reltool.config: -------------------------------------------------------------------------------- 1 | {sys, [ 2 | {lib_dirs, ["../deps"]}, 3 | {erts, [{mod_cond, derived}, {app_file, strip}]}, 4 | {app_file, strip}, 5 | {rel, "lmq", "0.6.4", 6 | [ 7 | kernel, 8 | stdlib, 9 | sasl, 10 | mnesia, 11 | lager, 12 | ranch, 13 | msgpack, 14 | msgpack_rpc, 15 | uuid, 16 | lmq 17 | ]}, 18 | {rel, "start_clean", "", 19 | [ 20 | kernel, 21 | stdlib 22 | ]}, 23 | {boot_rel, "lmq"}, 24 | {profile, embedded}, 25 | {incl_cond, exclude}, 26 | {excl_archive_filters, [".*"]}, %% Do not archive built libs 27 | {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)", 28 | "^erts.*/(doc|info|include|lib|man|src)"]}, 29 | {excl_app_filters, ["\.gitignore"]}, 30 | {app, sasl, [{incl_cond, include}]}, 31 | {app, stdlib, [{incl_cond, include}]}, 32 | {app, kernel, [{incl_cond, include}]}, 33 | {app, crypto, [{incl_cond, include}]}, 34 | {app, compiler, [{incl_cond, include}]}, 35 | {app, goldrush, [{incl_cond, include}]}, 36 | {app, syntax_tools, [{incl_cond, include}]}, 37 | {app, quickrand, [{incl_cond, include}]}, 38 | {app, uuid, [{incl_cond, include}]}, 39 | {app, lager, [{incl_cond, include}]}, 40 | {app, mnesia, [{incl_cond, include}]}, 41 | {app, ranch, [{incl_cond, include}]}, 42 | {app, cowlib, [{incl_cond, include}]}, 43 | {app, cowboy, [{incl_cond, include}]}, 44 | {app, jsonx, [{incl_cond, include}]}, 45 | {app, msgpack, [{incl_cond, include}]}, 46 | {app, msgpack_rpc, [{incl_cond, include}]}, 47 | {app, bear, [{incl_cond, include}]}, 48 | {app, folsom, [{incl_cond, include}]}, 49 | {app, statsderl, [{incl_cond, include}]}, 50 | {app, eper, [{incl_cond, include}]}, 51 | {app, lmq, [{incl_cond, include}, {lib_dir, ".."}]} 52 | ]}. 53 | 54 | {target_dir, "lmq"}. 55 | 56 | {overlay, [ 57 | {mkdir, "log/sasl"}, 58 | {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, 59 | {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"}, 60 | {copy, "files/lmq", "bin/lmq"}, 61 | {copy, "files/lmq-admin", "bin/lmq-admin"}, 62 | {copy, "files/lmq.cmd", "bin/lmq.cmd"}, 63 | {copy, "files/start_erl.cmd", "bin/start_erl.cmd"}, 64 | {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"}, 65 | {copy, "files/app.config", "etc/app.config"}, 66 | {copy, "files/vm.args", "etc/vm.args"} 67 | ]}. 68 | -------------------------------------------------------------------------------- /src/influxdb_client.erl: -------------------------------------------------------------------------------- 1 | -module(influxdb_client). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/2, write/1]). 6 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 7 | code_change/3, terminate/2]). 8 | 9 | -record(state, {enabled, socket, host, port}). 10 | 11 | %% ================================================================== 12 | %% Public API 13 | %% ================================================================== 14 | start_link(Host, Port) -> 15 | gen_server:start_link({local, ?MODULE}, ?MODULE, {Host, Port}, []). 16 | 17 | write(Series) -> 18 | gen_server:cast(?MODULE, {write, Series}). 19 | 20 | %% ================================================================== 21 | %% gen_server callbacks 22 | %% ================================================================== 23 | init({Host, Port}) -> 24 | case resolve_hostname(Host) of 25 | {ok, Addr} -> 26 | {ok, Socket} = gen_udp:open(0, [{active, false}]), 27 | {ok, #state{enabled=true, socket=Socket, host=Addr, port=Port}}; 28 | _ -> 29 | {ok, #state{enabled=false}} 30 | end. 31 | 32 | handle_call(_, _, State) -> 33 | {reply, ok, State}. 34 | 35 | handle_cast(_, #state{enabled=false}=State) -> 36 | {noreply, State}; 37 | handle_cast({write, Series}, #state{socket=Socket, host=Host, port=Port}=State) -> 38 | Data = jsonx:encode(Series), 39 | gen_udp:send(Socket, Host, Port, Data), 40 | {noreply, State}. 41 | 42 | handle_info(_, State) -> 43 | {noreply, State}. 44 | 45 | code_change(_OldVsn, State, _Extra) -> 46 | {ok, State}. 47 | 48 | terminate(_Reason, _State) -> 49 | ok. 50 | 51 | %% ================================================================== 52 | %% Private functions 53 | %% ================================================================== 54 | resolve_hostname(Addr) when is_tuple(Addr) -> 55 | Addr; 56 | resolve_hostname(Hostname) -> 57 | case inet:gethostbyname(Hostname) of 58 | {ok, {_, _, _, _, _, [Addr|_]}} -> {ok, Addr}; 59 | _ -> {error, invalid} 60 | end. 61 | -------------------------------------------------------------------------------- /src/lmq.app.src: -------------------------------------------------------------------------------- 1 | {application, lmq, 2 | [ 3 | {description, ""}, 4 | {vsn, "0.6.4"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | lager, 10 | crypto, 11 | uuid, 12 | mnesia, 13 | ranch, 14 | cowlib, 15 | cowboy, 16 | jsonx, 17 | msgpack, 18 | msgpack_rpc, 19 | folsom, 20 | statsderl 21 | ]}, 22 | {mod, { lmq_app, []}}, 23 | {env, [ 24 | {port, 18800}, 25 | {http, {"0.0.0.0", 9980}}, 26 | {stats_interval, 20000}, 27 | {influxdb, [ 28 | {host, "localhost"}, 29 | {port, 4444} 30 | ]} 31 | ]} 32 | ]}. 33 | -------------------------------------------------------------------------------- /src/lmq.erl: -------------------------------------------------------------------------------- 1 | -module(lmq). 2 | 3 | -include("lmq.hrl"). 4 | -export([start/0, stop/0]). 5 | -export([push/2, push/3, pull/1, pull/2, pull/3, ack/2, abort/2, keep/2, 6 | push_all/2, push_all/3, pull_any/1, pull_any/2, pull_any/3, delete/1, 7 | get_props/1, update_props/1, update_props/2, 8 | set_default_props/1, get_default_props/0, 9 | status/0, queue_status/1, stats/0, stats/1]). 10 | 11 | -define(DEPS, [lager, crypto, quickrand, uuid, msgpack, msgpack_rpc, 12 | mnesia, ranch, cowlib, cowboy, jsonx, folsom, statsderl, lmq]). 13 | 14 | %% ================================================================== 15 | %% Public API 16 | %% ================================================================== 17 | 18 | start() -> 19 | [ensure_started(Dep) || Dep <- ?DEPS], 20 | lager:set_loglevel(lager_console_backend, debug). 21 | 22 | stop() -> 23 | [application:stop(Dep) || Dep <- lists:reverse(?DEPS)], 24 | ok. 25 | 26 | push(Name, Content) -> 27 | push(Name, [], Content). 28 | 29 | push(Name, MD, Content) when is_binary(Name) -> 30 | push(binary_to_atom(Name, latin1), MD, Content); 31 | push(Name, MD, Content) when is_atom(Name) -> 32 | Pid = lmq_queue_mgr:get(Name, [create]), 33 | lmq_queue:push(Pid, {MD, Content}). 34 | 35 | pull(Name) when is_binary(Name) -> 36 | pull(binary_to_atom(Name, latin1)); 37 | pull(Name) when is_atom(Name) -> 38 | Pid = lmq_queue_mgr:get(Name, [create]), 39 | Msg = lmq_queue:pull(Pid), 40 | [{queue, Name} | lmq_lib:export_message(Msg)]. 41 | 42 | pull(Name, Timeout) when is_binary(Name) -> 43 | pull(binary_to_atom(Name, latin1), Timeout); 44 | pull(Name, Timeout) when is_atom(Name) -> 45 | Pid = lmq_queue_mgr:get(Name, [create]), 46 | case lmq_queue:pull(Pid, Timeout) of 47 | empty -> empty; 48 | Msg -> [{queue, Name} | lmq_lib:export_message(Msg)] 49 | end. 50 | 51 | pull(Name, Timeout, Monitor) when is_binary(Name) -> 52 | pull(binary_to_atom(Name, latin1), Timeout, Monitor); 53 | pull(Name, Timeout, Monitor) when is_atom(Name) -> 54 | Pid = lmq_queue_mgr:get(Name, [create]), 55 | Id = lmq_queue:pull_async(Pid, Timeout), 56 | Wait = case Timeout of 57 | infinity -> infinity; 58 | 0 -> infinity; 59 | N -> round(N * 1000) 60 | end, 61 | MonitorRef = erlang:monitor(process, Monitor), 62 | R = receive 63 | {Id, {error, timeout}} -> empty; 64 | {Id, Msg} -> [{queue, Name} | lmq_lib:export_message(Msg)]; 65 | {'DOWN', MonitorRef, process, Monitor, _} -> 66 | lmq_queue:pull_cancel(Pid, Id), 67 | receive 68 | {Id, #message{id={_, UUID}}} -> lmq_queue:put_back(Pid, UUID) 69 | after 0 -> ok 70 | end, 71 | {error, down} 72 | after Wait -> 73 | empty 74 | end, 75 | erlang:demonitor(MonitorRef, [flush]), 76 | R. 77 | 78 | ack(Name, UUID) -> 79 | process_message(done, Name, UUID). 80 | 81 | abort(Name, UUID) -> 82 | process_message(release, Name, UUID). 83 | 84 | keep(Name, UUID) -> 85 | process_message(retain, Name, UUID). 86 | 87 | push_all(Regexp, Content) -> 88 | push_all(Regexp, [], Content). 89 | 90 | push_all(Regexp, MD, Content) when is_binary(Regexp) -> 91 | case lmq_queue_mgr:match(Regexp) of 92 | {error, _}=R -> 93 | R; 94 | Queues -> 95 | {ok, [{Name, lmq_queue:push(Pid, {MD, Content})} || {Name, Pid} <- Queues]} 96 | end. 97 | 98 | pull_any(Regexp) -> 99 | pull_any(Regexp, inifinity). 100 | 101 | pull_any(Regexp, Timeout) when is_binary(Regexp) -> 102 | {ok, Pid} = lmq_mpull:start(), 103 | lmq_mpull:pull(Pid, Regexp, Timeout). 104 | 105 | pull_any(Regexp, Timeout, Monitor) when is_binary(Regexp) -> 106 | {ok, Pid} = lmq_mpull:start(), 107 | {ok, Ref} = lmq_mpull:pull_async(Pid, Regexp, Timeout), 108 | MonitorRef = erlang:monitor(process, Monitor), 109 | receive 110 | {Ref, Msg} -> 111 | erlang:demonitor(MonitorRef, [flush]), 112 | Msg; 113 | {'DOWN', MonitorRef, process, Monitor, _} -> 114 | lmq_mpull:pull_cancel(Pid), 115 | receive 116 | {Ref, [{queue, Name}, {id, UUID}, _, _]} -> 117 | Q = lmq_queue_mgr:get(Name, [create]), 118 | lmq_queue:put_back(Q, UUID) 119 | after 0 -> ok 120 | end, 121 | {error, down} 122 | end. 123 | 124 | delete(Name) when is_binary(Name) -> 125 | delete(binary_to_atom(Name, latin1)); 126 | delete(Name) when is_atom(Name) -> 127 | lmq_queue_mgr:delete(Name). 128 | 129 | get_props(Name) when is_binary(Name) -> 130 | get_props(binary_to_atom(Name, latin1)); 131 | get_props(Name) when is_atom(Name) -> 132 | lmq_lib:get_properties(Name). 133 | 134 | update_props(Name) -> 135 | update_props(Name, []). 136 | 137 | update_props(Name, Props) when is_binary(Name) -> 138 | update_props(binary_to_atom(Name, latin1), Props); 139 | update_props(Name, Props) when is_atom(Name) -> 140 | lmq_queue_mgr:get(Name, [create, update, {props, Props}]). 141 | 142 | set_default_props(Props) -> 143 | lmq_queue_mgr:set_default_props(Props). 144 | 145 | get_default_props() -> 146 | lmq_queue_mgr:get_default_props(). 147 | 148 | status() -> 149 | [{active_nodes, lists:sort(mnesia:system_info(running_db_nodes))}, 150 | {all_nodes, lists:sort(mnesia:system_info(db_nodes))}, 151 | {queues, [{N, queue_status(N)} || N <- lists:sort(lmq_lib:all_queue_names())]} 152 | ]. 153 | 154 | queue_status(Name) -> 155 | [{size, mnesia:table_info(Name, size)}, 156 | {memory, mnesia:table_info(Name, memory) * erlang:system_info(wordsize)}, 157 | {nodes, mnesia:table_info(Name, where_to_write)}, 158 | {props, lmq_lib:get_properties(Name)} 159 | ]. 160 | 161 | stats() -> 162 | [stats(N) || N <- lists:sort(lmq_lib:all_queue_names())]. 163 | 164 | stats(Name) when is_atom(Name) -> 165 | {Name, [{push, lmq_metrics:get_metric(Name, push)}, 166 | {pull, lmq_metrics:get_metric(Name, pull)}, 167 | {retention, lmq_metrics:get_metric(Name, retention)} 168 | ]}. 169 | 170 | %% ================================================================== 171 | %% Private functions 172 | %% ================================================================== 173 | 174 | ensure_started(App) -> 175 | case application:start(App) of 176 | ok -> ok; 177 | {error, {already_started, App}} -> ok 178 | end. 179 | 180 | process_message(Fun, Name, UUID) when is_atom(Fun), is_binary(Name) -> 181 | process_message(Fun, binary_to_atom(Name, latin1), UUID); 182 | process_message(Fun, Name, UUID) when is_atom(Fun), is_atom(Name) -> 183 | case lmq_queue_mgr:get(Name) of 184 | not_found -> 185 | {error, queue_not_found}; 186 | Pid -> 187 | try parse_uuid(UUID) of 188 | MsgId -> 189 | case lmq_queue:Fun(Pid, MsgId) of 190 | ok -> ok; 191 | not_found -> {error, not_found} 192 | end 193 | catch exit:badarg -> 194 | {error, not_found} 195 | end 196 | end. 197 | 198 | parse_uuid(UUID) when is_binary(UUID) -> 199 | parse_uuid(binary_to_list(UUID)); 200 | parse_uuid(UUID) when is_list(UUID) -> 201 | uuid:string_to_uuid(UUID); 202 | parse_uuid(UUID) -> 203 | UUID. 204 | -------------------------------------------------------------------------------- /src/lmq_api.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_api). 2 | 3 | -export([delete/1, push/2, pull/1, pull/2, push_all/2, 4 | pull_any/1, pull_any/2, done/2, retain/2, release/2, 5 | update_props/1, update_props/2, set_default_props/1, get_default_props/0, 6 | normalize_props/1, export_props/1, 7 | normalize_default_props/1, export_default_props/1]). 8 | -include("lmq.hrl"). 9 | 10 | delete(Name) when is_binary(Name) -> 11 | lager:info("lmq_api:delete(~s)", [Name]), 12 | ok = lmq:delete(Name), 13 | <<"ok">>. 14 | 15 | push(Name, Content) when is_binary(Name) -> 16 | lager:info("lmq_api:push(~s, ...)", [Name]), 17 | Bin = msgpack:pack(Content), 18 | case lmq:push(Name, [{<<"content-type">>, 19 | <<"application/x-msgpack">>}], Bin) of 20 | ok -> <<"ok">>; 21 | {accum, new} -> <<"packing started">>; 22 | {accum, yes} -> <<"packed">> 23 | end. 24 | 25 | pull(Name) when is_binary(Name) -> 26 | lager:info("lmq_api:pull(~s)", [Name]), 27 | Response = lmq:pull(Name), 28 | export_message(extract_content_value(Response)). 29 | 30 | pull(Name, Timeout) when is_binary(Name) -> 31 | lager:info("lmq_api:pull(~s, ~p)", [Name, Timeout]), 32 | {monitors, [{process, Conn}]} = erlang:process_info(self(), monitors), 33 | case lmq:pull(Name, Timeout, Conn) of 34 | {error, down} -> ok; 35 | empty -> <<"empty">>; 36 | Msg -> export_message(extract_content_value(Msg)) 37 | end. 38 | 39 | push_all(Regexp, Content) when is_binary(Regexp) -> 40 | lager:info("lmq_api:push_all(~s, ...)", [Regexp]), 41 | case lmq:push_all(Regexp, Content) of 42 | {ok, _} -> <<"ok">>; 43 | {error, Reason} -> throw(Reason) 44 | end. 45 | 46 | pull_any(Regexp) -> 47 | pull_any(Regexp, inifinity). 48 | 49 | pull_any(Regexp, Timeout) when is_binary(Regexp) -> 50 | lager:info("lmq_api:pull_any(~s, ~p)", [Regexp, Timeout]), 51 | Timeout2 = if 52 | is_number(Timeout) -> round(Timeout * 1000); 53 | true -> Timeout 54 | end, 55 | {monitors, [{process, Conn}]} = erlang:process_info(self(), monitors), 56 | case lmq:pull_any(Regexp, Timeout2, Conn) of 57 | {error, down} -> ok; 58 | empty -> <<"empty">>; 59 | Msg -> export_message(extract_content_value(Msg)) 60 | end. 61 | 62 | done(Name, UUID) when is_binary(Name), is_binary(UUID) -> 63 | lager:info("lmq_api:done(~s, ~s)", [Name, UUID]), 64 | process_message(ack, Name, UUID). 65 | 66 | retain(Name, UUID) when is_binary(Name), is_binary(UUID) -> 67 | lager:info("lmq_api:retain(~s, ~s)", [Name, UUID]), 68 | process_message(keep, Name, UUID). 69 | 70 | release(Name, UUID) when is_binary(Name), is_binary(UUID) -> 71 | lager:info("lmq_api:release(~s, ~s)", [Name, UUID]), 72 | process_message(abort, Name, UUID). 73 | 74 | update_props(Name) when is_binary(Name) -> 75 | lager:info("lmq_api:update_props(~s)", [Name]), 76 | lmq:update_props(Name), 77 | <<"ok">>. 78 | 79 | update_props(Name, Props) when is_binary(Name) -> 80 | lager:info("lmq_api:update_props(~s, ~p)", [Name, Props]), 81 | case normalize_props(Props) of 82 | {ok, Props2} -> 83 | lmq:update_props(Name, to_new_props(Props2)), 84 | <<"ok">>; 85 | {error, Reason} -> 86 | throw(Reason) 87 | end. 88 | 89 | set_default_props(PropsList) -> 90 | lager:info("lmq_api:set_default_props(~p)", [PropsList]), 91 | case normalize_default_props(PropsList) of 92 | {ok, PropsList2} -> 93 | PropsList3 = [{K, to_new_props(V)} || {K, V} <- PropsList2], 94 | case lmq:set_default_props(PropsList3) of 95 | ok -> <<"ok">>; 96 | Reason -> throw(Reason) 97 | end; 98 | {error, Reason} -> 99 | throw(Reason) 100 | end. 101 | 102 | get_default_props() -> 103 | lager:info("lmq_api:get_default_props()"), 104 | Props = [{K, to_old_props(V)} || {K, V} <- lmq:get_default_props()], 105 | export_default_props(Props). 106 | 107 | normalize_props({Props}) -> 108 | %% jiffy style to proplists 109 | normalize_props(Props, []). 110 | 111 | export_props(Props) -> 112 | export_props(Props, []). 113 | 114 | normalize_default_props(DefaultProps) -> 115 | normalize_default_props(DefaultProps, []). 116 | 117 | export_default_props(DefaultProps) -> 118 | export_default_props(DefaultProps, []). 119 | 120 | %% ================================================================== 121 | %% Private functions 122 | %% ================================================================== 123 | 124 | process_message(Fun, Name, UUID) when is_atom(Fun) -> 125 | case lmq:Fun(Name, UUID) of 126 | ok -> <<"ok">>; 127 | {error, Reason} -> throw(Reason) 128 | end. 129 | 130 | export_message(Msg) -> 131 | export_message(Msg, []). 132 | 133 | export_message([{id, V} | Tail], Acc) -> 134 | export_message(Tail, [{<<"id">>, V} | Acc]); 135 | export_message([{type, compound} | Tail], Acc) -> 136 | export_message(Tail, [{<<"type">>, <<"package">>} | Acc]); 137 | export_message([{K, V} | Tail], Acc) when is_atom(V) -> 138 | export_message(Tail, [{atom_to_binary(K, latin1), 139 | atom_to_binary(V, latin1)} | Acc]); 140 | export_message([{K, V} | Tail], Acc) when is_atom(K) -> 141 | export_message(Tail, [{atom_to_binary(K, latin1), V} | Acc]); 142 | export_message([], Acc) -> 143 | {lists:reverse(Acc)}. 144 | 145 | normalize_props([{<<"pack">>, Duration} | T], Acc) when is_number(Duration) -> 146 | normalize_props(T, [{pack, round(Duration * 1000)} | Acc]); 147 | normalize_props([{<<"accum">>, Duration} | T], Acc) when is_number(Duration) -> 148 | normalize_props(T, [{accum, round(Duration * 1000)} | Acc]); 149 | normalize_props([{<<"retry">>, N} | T], Acc) when is_integer(N) -> 150 | normalize_props(T, [{retry, N} | Acc]); 151 | normalize_props([{<<"timeout">>, N} | T], Acc) when is_number(N) -> 152 | normalize_props(T, [{timeout, N} | Acc]); 153 | normalize_props([{<<"hooks">>, {L}} | T], Acc) when is_list(L) -> 154 | normalize_props(T, [{hooks, normalize_hooks(L, [])} | Acc]); 155 | normalize_props([], Acc) -> 156 | {ok, lists:reverse(Acc)}; 157 | normalize_props(_, _) -> 158 | {error, invalid}. 159 | 160 | normalize_hooks([], Acc) -> 161 | lists:reverse(Acc); 162 | normalize_hooks([{Name, L}|T], Acc) when is_binary(Name), is_list(L) -> 163 | Name2 = binary_to_atom(Name, latin1), 164 | normalize_hooks(T, [{Name2, normalize_hooks2(L, [])}|Acc]). 165 | 166 | normalize_hooks2([], Acc) -> 167 | lists:reverse(Acc); 168 | normalize_hooks2([[Name, L]|T], Acc) when is_binary(Name), is_list(L) -> 169 | Name2 = binary_to_existing_atom(Name, latin1), 170 | normalize_hooks2(T, [{Name2, L}|Acc]). 171 | 172 | export_props([{pack, Duration} | T], Acc) -> 173 | export_props(T, [{<<"pack">>, Duration / 1000} | Acc]); 174 | export_props([{accum, Duration} | T], Acc) -> 175 | export_props(T, [{<<"accum">>, Duration / 1000} | Acc]); 176 | export_props([{hooks, L} | T], Acc) when is_list(L) -> 177 | export_props(T, [{<<"hooks">>, export_hooks(L, [])} | Acc]); 178 | export_props([{K, V} | T], Acc) -> 179 | export_props(T, [{atom_to_binary(K, latin1), V} | Acc]); 180 | export_props([], Acc) -> 181 | {lists:reverse(Acc)}. 182 | 183 | export_hooks([], Acc) -> 184 | {lists:reverse(Acc)}; 185 | export_hooks([{Name, L}|T], Acc) when is_atom(Name), is_list(L) -> 186 | export_hooks(T, [{atom_to_binary(Name, latin1), export_hooks2(L, [])}|Acc]). 187 | 188 | export_hooks2([], Acc) -> 189 | lists:reverse(Acc); 190 | export_hooks2([{Name, L}|T], Acc) when is_atom(Name), is_list(L) -> 191 | export_hooks2(T, [[atom_to_binary(Name, latin1), L]|Acc]). 192 | 193 | normalize_default_props([[Regexp, Props]|T], Acc) when is_binary(Regexp) -> 194 | case normalize_props(Props) of 195 | {ok, Props2} -> normalize_default_props(T, [{Regexp, Props2} | Acc]); 196 | {error, _}=R -> R 197 | end; 198 | normalize_default_props([], Acc) -> 199 | {ok, lists:reverse(Acc)}; 200 | normalize_default_props(_, _) -> 201 | {error, invalid}. 202 | 203 | export_default_props([{Regexp, Props} | T], Acc) when is_list(Regexp); is_binary(Regexp) -> 204 | export_default_props(T, [[Regexp, export_props(Props)] | Acc]); 205 | export_default_props([], Acc) -> 206 | lists:reverse(Acc). 207 | 208 | extract_content_value([QUEUE, ID, {type, normal}=TYPE, RETRY, {content, {MD, V}}]) -> 209 | [QUEUE, ID, TYPE, RETRY, {content, maybe_decode(MD, V)}]; 210 | extract_content_value([QUEUE, ID, {type, compound}=TYPE, RETRY, {content, Content}]) -> 211 | [QUEUE, ID, TYPE, RETRY, {content, [maybe_decode(MD, V) || {MD, V} <- Content]}]. 212 | 213 | maybe_decode(MD, V) -> 214 | case proplists:get_value(<<"content-type">>, MD) of 215 | <<"application/x-msgpack">> -> 216 | {ok, Term} = msgpack:unpack(V), 217 | Term; 218 | _ -> 219 | V 220 | end. 221 | 222 | to_new_props(Props) -> 223 | lists:keymap(fun(pack) -> accum; (Other) -> Other end, 1, Props). 224 | 225 | to_old_props(Props) -> 226 | lists:keymap(fun(accum) -> pack; (Other) -> Other end, 1, Props). 227 | 228 | %% ================================================================== 229 | %% EUnit test 230 | %% ================================================================== 231 | 232 | -ifdef(TEST). 233 | -include_lib("eunit/include/eunit.hrl"). 234 | 235 | normalize_props_test() -> 236 | ?assertEqual({ok, [{retry, 1}]}, normalize_props({[{<<"retry">>, 1}]})), 237 | ?assertEqual({ok, [{retry, 2}, {timeout, 5.0}, {pack, 500}]}, 238 | normalize_props({[{<<"retry">>, 2}, {<<"timeout">>, 5.0}, 239 | {<<"pack">>, 0.5}]})), 240 | ?assertEqual({ok, [{hooks, [{hook_point, [{hook_module, [<<"args1">>, <<"args2">>]}]}]}]}, 241 | normalize_props({[{<<"hooks">>, 242 | {[{<<"hook_point">>, 243 | [[<<"hook_module">>, [<<"args1">>, <<"args2">>]]]}]}}]})), 244 | ?assertEqual({error, invalid}, 245 | normalize_props({[{<<"retry">>, <<"3">>}, {<<"timeout">>, <<"5.0">>}, 246 | {<<"pack">>, <<"1">>}]})), 247 | ?assertEqual({error, invalid}, normalize_props({[{<<"not supported">>, 4}]})). 248 | 249 | export_props_test() -> 250 | ?assertEqual({[{<<"retry">>, 3}, {<<"timeout">>, 5.0}, {<<"pack">>, 0.5}]}, 251 | export_props([{retry, 3}, {timeout, 5.0}, {pack, 500}])), 252 | ?assertEqual({[{<<"hooks">>, 253 | {[{<<"hook_point">>, 254 | [[<<"hook_module">>, [<<"args1">>, <<"args2">>]]]}]}}]}, 255 | export_props([{hooks, [{hook_point, [{hook_module, [<<"args1">>, <<"args2">>]}]}]}])). 256 | 257 | normalize_default_props_test() -> 258 | ?assertEqual({ok, [{<<"lmq">>, [{pack, 1000}]}, {<<"def">>, [{retry, 0}]}]}, 259 | normalize_default_props([[<<"lmq">>, {[{<<"pack">>, 1}]}], 260 | [<<"def">>, {[{<<"retry">>, 0}]}]])), 261 | ?assertEqual({ok, [{<<"lmq">>, [{hooks, [{hook_point, [{hook_module, [<<"args1">>, <<"args2">>]}]}]}]}]}, 262 | normalize_default_props([[<<"lmq">>, 263 | {[{<<"hooks">>, 264 | {[{<<"hook_point">>, 265 | [[<<"hook_module">>, [<<"args1">>, <<"args2">>]]]}]}}]}]])), 266 | ?assertEqual({error, invalid}, 267 | normalize_default_props([[<<"lmq">>, {[{<<"pack">>, <<"1">>}]}]])), 268 | ?assertEqual({error, invalid}, normalize_default_props([<<"lmq">>])). 269 | 270 | export_default_props_test() -> 271 | ?assertEqual([[<<"lmq">>, {[{<<"pack">>, 1.0}]}], 272 | [<<"def">>, {[{<<"retry">>, 0}]}]], 273 | export_default_props([{<<"lmq">>, [{pack, 1000}]}, 274 | {<<"def">>, [{retry, 0}]}])), 275 | ?assertEqual([[<<"lmq">>, 276 | {[{<<"hooks">>, 277 | {[{<<"hook_point">>, 278 | [[<<"hook_module">>, [<<"args1">>, <<"args2">>]]]}]}}]}]], 279 | export_default_props([{<<"lmq">>, 280 | [{hooks, [{hook_point, [{hook_module, [<<"args1">>, <<"args2">>]}]}]}]}])). 281 | 282 | export_message_test() -> 283 | Ref = make_ref(), 284 | M = #message{content=Ref}, 285 | UUID = list_to_binary(uuid:uuid_to_string(element(2, M#message.id))), 286 | ?assertEqual({[{<<"id">>, UUID}, {<<"type">>, <<"normal">>}, 287 | {<<"retry">>, 0}, {<<"content">>, Ref}]}, 288 | export_message(lmq_lib:export_message(M))), 289 | 290 | M2 = M#message{type=compound}, 291 | ?assertEqual({[{<<"queue">>, <<"test">>}, {<<"id">>, UUID}, 292 | {<<"type">>, <<"package">>}, {<<"retry">>, 0}, {<<"content">>, Ref}]}, 293 | export_message([{queue, test} | lmq_lib:export_message(M2)])). 294 | 295 | props_compatibility_test() -> 296 | ?assertEqual([{accum, 0}, {retry, 2}, {timeout, 30}], 297 | to_new_props([{pack, 0}, {retry, 2}, {timeout, 30}])), 298 | ?assertEqual([{pack, 0}, {retry, 2}, {timeout, 30}], 299 | to_old_props([{accum, 0}, {retry, 2}, {timeout, 30}])). 300 | 301 | -endif. 302 | -------------------------------------------------------------------------------- /src/lmq_app.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | -define(MSGPACK_SERV, msgpack_serv). 9 | 10 | %% =================================================================== 11 | %% Application callbacks 12 | %% =================================================================== 13 | 14 | start(_StartType, _StartArgs) -> 15 | {ok, Port} = application:get_env(port), 16 | {ok, Http} = application:get_env(http), 17 | maybe_join(application:get_env(join)), 18 | R = lmq_sup:start_link(), 19 | start_cowboy(Http), 20 | {ok, _} = msgpack_rpc_server:start(?MSGPACK_SERV, tcp, lmq_api, [{port, Port}]), 21 | R. 22 | 23 | stop(_State) -> 24 | msgpack_rpc_server:stop(?MSGPACK_SERV), 25 | ok. 26 | 27 | %% ================================================================== 28 | %% Private functions 29 | %% ================================================================== 30 | 31 | start_cowboy({Ip, Port}) -> 32 | Dispatch = cowboy_router:compile([ 33 | {'_', [{"/messages", lmq_cow_msg, [multi]}, 34 | {"/messages/:name", lmq_cow_msg, []}, 35 | {"/messages/:name/:id", lmq_cow_reply, []}, 36 | {"/queues/:name", lmq_cow_queue, []}, 37 | {"/properties", lmq_cow_prop, []}, 38 | {"/properties/:name", lmq_cow_prop, []}, 39 | {"/stats", lmq_cow_stats, []} 40 | ]} 41 | ]), 42 | 43 | {ok, Ip2} = inet_parse:address(Ip), 44 | cowboy:start_http(lmq_http_listener, 100, 45 | [{ip, Ip2}, {port, Port}], 46 | [{env, [{dispatch, Dispatch}]}, 47 | {max_keepalive, 10000}] 48 | ). 49 | 50 | maybe_join(undefined) -> 51 | ok = lmq_lib:init_mnesia(); 52 | maybe_join({ok, Node}) -> 53 | case {net_kernel:connect_node(Node), net_adm:ping(Node)} of 54 | {true, pong} -> 55 | rpc:call(Node, lmq_console, add_new_node, [node()]); 56 | {_, pang} -> 57 | ok = lmq_lib:init_mnesia() 58 | end. 59 | -------------------------------------------------------------------------------- /src/lmq_console.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_console). 2 | 3 | -export([join/1, leave/1, add_new_node/1, status/1, stats/1]). 4 | 5 | join([NodeStr]) when is_list(NodeStr) -> 6 | Node = list_to_atom(NodeStr), 7 | join(Node); 8 | 9 | join(Node) when is_atom(Node) -> 10 | case {net_kernel:connect_node(Node), net_adm:ping(Node)} of 11 | {true, pong} -> 12 | ok = application:stop(lmq), 13 | delete_local_schema(), 14 | R = rpc:call(Node, lmq_console, add_new_node, [node()]), 15 | ok = application:start(lmq), 16 | R; 17 | {_, pang} -> 18 | {error, not_reachable} 19 | end. 20 | 21 | leave([]) -> 22 | RunningNodes = [Node || Node <- mnesia:system_info(running_db_nodes), 23 | Node =/= node()], 24 | ok = application:stop(lmq), 25 | ok = application:stop(mnesia), 26 | R = case leave_cluster(RunningNodes) of 27 | ok -> ok = mnesia:delete_schema([node()]); 28 | Other -> Other 29 | end, 30 | ok = application:start(mnesia), 31 | ok = application:start(lmq), 32 | R. 33 | 34 | delete_local_schema() -> 35 | ok = application:stop(mnesia), 36 | ok = mnesia:delete_schema([node()]), 37 | ok = application:start(mnesia). 38 | 39 | add_new_node(Node) -> 40 | case mnesia:change_config(extra_db_nodes, [Node]) of 41 | {ok, _} -> copy_all_tables(Node); 42 | {error, _}=E -> E 43 | end. 44 | 45 | status([]) -> 46 | Status = lmq:status(), 47 | io:format(" All nodes: ~s~n", [string:join( 48 | [atom_to_list(N) || N <- proplists:get_value(all_nodes, Status)], 49 | ", ")]), 50 | io:format("Active nodes: ~s~n", [string:join( 51 | [atom_to_list(N) || N <- proplists:get_value(active_nodes, Status)], 52 | ", ")]), 53 | io:format("~n"), 54 | lists:foreach(fun({Name, QStatus}) -> 55 | io:format("~s ~7.B messages ~15.B bytes~n", [ 56 | string:left(atom_to_list(Name), 40), 57 | proplists:get_value(size, QStatus), 58 | proplists:get_value(memory, QStatus) 59 | ]), 60 | Props = proplists:get_value(props, QStatus), 61 | io:format(" accum: ~p, retry: ~B, timeout: ~p~n", [ 62 | proplists:get_value(accum, Props) / 1000, 63 | proplists:get_value(retry, Props), 64 | proplists:get_value(timeout, Props) / 1 65 | ]) 66 | end, proplists:get_value(queues, Status)), 67 | ok. 68 | 69 | stats([]) -> 70 | lists:foreach(fun({Name, Info}) -> 71 | Push = proplists:get_value(push, Info), 72 | Pull = proplists:get_value(pull, Info), 73 | Retention = proplists:get_value(retention, Info), 74 | io:format("~s~n", [Name]), 75 | io:format(" push rate: 1min ~.2f, 5min ~.2f, 15min ~.2f, 1day ~.2f~n", [ 76 | proplists:get_value(K, Push) || K <- [one, five, fifteen, day]]), 77 | io:format(" pull rate: 1min ~.2f, 5min ~.2f, 15min ~.2f, 1day ~.2f~n", [ 78 | proplists:get_value(K, Pull) || K <- [one, five, fifteen, day]]), 79 | io:format(" retention time: min ~.3f, max ~.3f, mean ~.3f, median ~.3f~n", [ 80 | proplists:get_value(K, Retention) || K <- [min, max, arithmetic_mean, median]]) 81 | end, lmq:stats()). 82 | 83 | copy_all_tables(Node) -> 84 | Tables = mnesia:system_info(tables), 85 | Errors = lists:foldl(fun(Tab, Failed) -> 86 | case mnesia:add_table_copy(Tab, Node, ram_copies) of 87 | {atomic, ok} -> Failed; 88 | {aborted, {already_exists, _, _}} -> Failed; 89 | {aborted, Reason} -> [{Tab, Reason} | Failed] 90 | end 91 | end, [], Tables), 92 | case Errors of 93 | [] -> ok; 94 | Other -> {error, {copy_failed, Other}} 95 | end. 96 | 97 | leave_cluster([Node | Rest]) when is_atom(Node) -> 98 | case rpc:call(Node, mnesia, del_table_copy, [schema, node()]) of 99 | {atomic, ok} -> 100 | ok; 101 | {aborted, Reason} -> 102 | case Rest of 103 | [] -> {error, Reason}; 104 | _ -> leave_cluster(Rest) 105 | end 106 | end. 107 | -------------------------------------------------------------------------------- /src/lmq_cow_msg.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_cow_msg). 2 | 3 | -export([init/3, handle/2, info/3, terminate/3]). 4 | 5 | -record(state, {queue, pull, push, ref, cf}). 6 | 7 | init(_Transport, Req, []) -> 8 | {Name, Req2} = cowboy_req:binding(name, Req), 9 | init(cowboy_req:method(Req2), #state{queue=Name, pull=pull, push=push}); 10 | init(_Transport, Req, [multi]) -> 11 | case validate_multi_request(Req) of 12 | {{ok, Regexp}, Req2} -> 13 | init(cowboy_req:method(Req2), #state{queue=Regexp, pull=pull_any, push=push_all}); 14 | {error, Req2} -> 15 | {ok, Req3} = cowboy_req:reply(400, Req2), 16 | {shutdown, Req3, #state{}} 17 | end. 18 | 19 | init({<<"GET">>, Req}, #state{queue=Queue, pull=Pull}=State) -> 20 | case validate([fun validate_timeout/1, 21 | fun validate_compound_format/1], Req) of 22 | {ok, [Timeout, CF], Req2} -> 23 | Self = self(), 24 | Ref = make_ref(), 25 | spawn_link(fun() -> Self ! {Ref, lmq:Pull(Queue, Timeout, Self)} end), 26 | {loop, Req2, State#state{ref=Ref, cf=CF}}; 27 | {error, Req2} -> 28 | {ok, Req3} = cowboy_req:reply(400, Req2), 29 | {shutdown, Req3, State} 30 | end; 31 | init({<<"POST">>, Req}, State) -> 32 | {ok, Req, State}; 33 | init({_, Req}, State) -> 34 | {ok, Req2} = cowboy_req:reply(405, Req), 35 | {shutdown, Req2, State}. 36 | 37 | handle(Req, #state{queue=Queue, push=Push}=State) -> 38 | {ok, Content, Req2} = fetch_body(Req, <<>>), 39 | {CT, Req3} = cowboy_req:header(<<"content-type">>, Req2), 40 | MD = case CT of 41 | undefined -> []; 42 | _ -> [{<<"content-type">>, CT}] 43 | end, 44 | Res = export_push_resp(do_push(Push, Queue, MD, Content, Req3)), 45 | Res2 = jsonx:encode(Res), 46 | {ok, Req4} = cowboy_req:reply(200, 47 | [{<<"content-type">>, <<"application/json">>}], Res2, Req3), 48 | {ok, Req4, State}. 49 | 50 | info({Ref, empty}, Req, #state{ref=Ref}=State) -> 51 | {ok, Req2} = cowboy_req:reply(204, Req), 52 | {ok, Req2, State}; 53 | info({Ref, Msg}, Req, #state{ref=Ref, cf=CF}=State) -> 54 | {Hdrs, V} = encode_body(proplists:get_value(type, Msg), CF, Msg), 55 | Hdrs2 = Hdrs ++ [{<<"x-lmq-queue-name">>, 56 | atom_to_binary(proplists:get_value(queue, Msg), latin1)}, 57 | {<<"x-lmq-message-id">>, proplists:get_value(id, Msg)}, 58 | {<<"x-lmq-message-type">>, 59 | atom_to_binary(proplists:get_value(type, Msg), latin1)}, 60 | {<<"x-lmq-retry-remaining">>, integer_to_binary(proplists:get_value(retry, Msg))}], 61 | {ok, Req2} = cowboy_req:reply(200, Hdrs2, V, Req), 62 | {ok, Req2, State}; 63 | info(_Msg, Req, State) -> 64 | {loop, Req, State}. 65 | 66 | terminate({normal, _}, _Req, _State) -> 67 | %% maybe reuse connection 68 | ok; 69 | terminate({error, _}, _Req, _State) -> 70 | %% close connection and halt this process 71 | error. 72 | 73 | %% ================================================================== 74 | %% Private functions 75 | %% ================================================================== 76 | fetch_body(Req, Acc) -> 77 | case cowboy_req:body(Req) of 78 | {ok, Data, Req2} -> 79 | {ok, <>, Req2}; 80 | {more, Data, Req2} -> 81 | fetch_body(Req2, <>); 82 | Error -> 83 | Error 84 | end. 85 | 86 | do_push(push, Queue, MD, Content, Req) when is_binary(Queue) -> 87 | do_push(push, binary_to_atom(Queue, latin1), MD, Content, Req); 88 | do_push(push, Queue, MD, Content, Req) -> 89 | {MD2, Content2, _} = lmq_hook:call(Queue, pre_http_push, {MD, Content, Req}), 90 | lmq:push(Queue, MD2, Content2); 91 | do_push(push_all, Regexp, MD, Content, Req) -> 92 | case lmq_queue_mgr:match(Regexp) of 93 | {error, _}=R -> 94 | R; 95 | Queues -> 96 | {ok, [{Name, do_push(push, Name, MD, Content, Req)} || {Name, _} <- Queues]} 97 | end. 98 | 99 | export_push_resp({ok, L}) when is_list(L) -> 100 | L2 = lists:filtermap(fun({N, R}) -> 101 | case R of 102 | {error, _} -> false; 103 | _ -> {true, {N, export_push_resp(R)}} 104 | end 105 | end, L), 106 | {L2}; 107 | export_push_resp(ok) -> 108 | {[{accum, no}]}; 109 | export_push_resp({accum, _}=R) -> 110 | {[R]}. 111 | 112 | validate(L, Req) when is_list(L) -> 113 | lists:foldl(fun(F, {ok, Acc, Req2}) -> 114 | case F(Req2) of 115 | {ok, V, Req3} -> {ok, Acc ++ [V], Req3}; 116 | {error, Req3} -> {error, Req3} 117 | end; 118 | (_, {error, Req2}) -> 119 | {error, Req2} 120 | end, {ok, [], Req}, L). 121 | 122 | validate_multi_request(Req) -> 123 | case cowboy_req:qs_val(<<"qre">>, Req) of 124 | {undefined, Req2} -> {error, Req2}; 125 | {Regexp, Req2} -> 126 | case re:compile(Regexp) of 127 | {ok, _} -> {{ok, Regexp}, Req2}; 128 | _ -> {error, Req2} 129 | end 130 | end. 131 | 132 | validate_timeout(Req) -> 133 | case cowboy_req:qs_val(<<"t">>, Req) of 134 | {undefined, Req2} -> 135 | {ok, infinity, Req2}; 136 | {V, Req2} -> 137 | case lmq_misc:btof(V) of 138 | {ok, 0.0} -> 139 | {ok, 0, Req2}; 140 | {ok, T} -> 141 | case cowboy_req:qs_val(<<"qre">>, Req2) of 142 | {undefined, Req3} -> {ok, T, Req3}; 143 | {_, Req3} -> {ok, round(T * 1000), Req3} 144 | end; 145 | {error, _} -> 146 | {error, Req2} 147 | end 148 | end. 149 | 150 | validate_compound_format(Req) -> 151 | case cowboy_req:qs_val(<<"cf">>, Req) of 152 | {undefined, Req2} -> 153 | {ok, multipart, Req2}; 154 | {<<"multipart">>, Req2} -> 155 | {ok, multipart, Req2}; 156 | {<<"msgpack">>, Req2} -> 157 | {ok, msgpack, Req2}; 158 | {_, Req2} -> 159 | {error, Req2} 160 | end. 161 | 162 | encode_body(normal, _, Msg) -> 163 | {MD, V} = proplists:get_value(content, Msg), 164 | {make_headers(MD), V}; 165 | encode_body(compound, multipart, Msg) -> 166 | Boundary = proplists:get_value(id, Msg), 167 | Hdrs = [{<<"content-type">>, <<"multipart/mixed; boundary=", Boundary/binary>>}], 168 | V = to_multipart(Boundary, proplists:get_value(content, Msg)), 169 | {Hdrs, V}; 170 | encode_body(compound, msgpack, Msg) -> 171 | Hdrs = [{<<"content-type">>, <<"application/x-msgpack">>}], 172 | V = [[{stringify_metadata(make_headers(MD))}, V] || {MD, V} <- proplists:get_value(content, Msg)], 173 | {Hdrs, msgpack:pack(V, [{enable_str, true}])}. 174 | 175 | make_headers(MD) -> 176 | case proplists:is_defined(<<"content-type">>, MD) of 177 | true -> MD; 178 | false -> [{<<"content-type">>, <<"application/octet-stream">>}|MD] 179 | end. 180 | 181 | to_multipart(Boundary, Contents) when is_list(Contents) -> 182 | [[[<<"\r\n--">>, Boundary, <<"\r\n">>, encode_multipart_item(C)] 183 | || C <- Contents], 184 | <<"\r\n--">>, Boundary, <<"--\r\n">>]. 185 | 186 | encode_multipart_item({MD, V}) -> 187 | Hdrs = make_headers(MD), 188 | [[[Key, <<": ">>, Value, <<"\r\n">>] || {Key, Value} <- Hdrs], 189 | <<"content-transfer-encoding: binary\r\n">>, 190 | <<"\r\n">>, V]. 191 | 192 | stringify_metadata(MD) -> 193 | stringify_metadata(MD, []). 194 | 195 | stringify_metadata([{K, V}|Tail], Acc) when is_binary(K), is_binary(V) -> 196 | stringify_metadata(Tail, [{binary_to_list(K), binary_to_list(V)}|Acc]); 197 | stringify_metadata([], Acc) -> 198 | lists:reverse(Acc). 199 | 200 | %% ================================================================== 201 | %% EUnit test 202 | %% ================================================================== 203 | -ifdef(TEST). 204 | -include_lib("eunit/include/eunit.hrl"). 205 | 206 | export_push_resp_test_() -> 207 | [?_assertEqual({[{accum, no}]}, export_push_resp(ok)), 208 | ?_assertEqual({[{foo, {[{accum, no}]}}, {bar, {[{accum, no}]}}]}, 209 | export_push_resp({ok, [{foo, ok}, {bar, ok}]})), 210 | ?_assertEqual({[{bar, {[{accum, no}]}}]}, 211 | export_push_resp({ok, [{foo, {error, no_queue_exists}}, {bar, ok}]}))]. 212 | 213 | encode_body_normal_test_() -> 214 | Msg1 = [{id, <<"id1">>}, {content, {[{<<"content-type">>, <<"text/plain">>}], <<"msg1">>}}], 215 | Msg2 = [{id, <<"id2">>}, {content, {[], <<"msg2">>}}], 216 | Msg3 = [{id, <<"id3">>}, {content, {[{<<"x-lmq-sequence">>, <<"1">>}], <<"msg3">>}}], 217 | [?_assertEqual({[{<<"content-type">>, <<"text/plain">>}], <<"msg1">>}, 218 | encode_body(normal, multipart, Msg1)), 219 | ?_assertEqual({[{<<"content-type">>, <<"application/octet-stream">>}], <<"msg2">>}, 220 | encode_body(normal, multipart, Msg2)), 221 | ?_assertEqual({[{<<"content-type">>, <<"application/octet-stream">>}, 222 | {<<"x-lmq-sequence">>, <<"1">>}], <<"msg3">>}, 223 | encode_body(normal, multipart, Msg3))]. 224 | 225 | encode_body_multipart_test_() -> 226 | Msg1 = [{id, <<"id1">>}, {content, [{[{<<"content-type">>, <<"text/plain">>}], <<"msg1">>}, 227 | {[], <<"msg2">>}, 228 | {[{<<"x-lmq-sequence">>, <<"1">>}], <<"msg3">>}]}], 229 | [?_assertEqual([{<<"content-type">>, <<"multipart/mixed; boundary=id1">>}], 230 | element(1, encode_body(compound, multipart, Msg1))), 231 | ?_assertEqual(<<"\r\n--id1\r\n", 232 | "content-type: text/plain\r\n", 233 | "content-transfer-encoding: binary\r\n", 234 | "\r\n", 235 | "msg1" 236 | "\r\n--id1\r\n", 237 | "content-type: application/octet-stream\r\n", 238 | "content-transfer-encoding: binary\r\n", 239 | "\r\n", 240 | "msg2", 241 | "\r\n--id1\r\n", 242 | "content-type: application/octet-stream\r\n", 243 | "x-lmq-sequence: 1\r\n", 244 | "content-transfer-encoding: binary\r\n", 245 | "\r\n", 246 | "msg3", 247 | "\r\n--id1--\r\n">>, 248 | iolist_to_binary(element(2, encode_body(compound, multipart, Msg1)))) 249 | ]. 250 | 251 | encode_body_msgpack_test_() -> 252 | Msg1 = [{id, <<"id1">>}, {content, [{[{<<"content-type">>, <<"text/plain">>}], <<"msg1">>}, 253 | {[], <<"msg2">>}, 254 | {[{<<"x-lmq-sequence">>, <<"1">>}], <<"msg3">>}]}], 255 | Bin1 = msgpack:pack([[{[{"content-type", "text/plain"}]}, <<"msg1">>], 256 | [{[{"content-type", "application/octet-stream"}]}, <<"msg2">>], 257 | [{[{"content-type", "application/octet-stream"}, 258 | {"x-lmq-sequence", "1"}]}, <<"msg3">>]], 259 | [{enable_str, true}]), 260 | [?_assertEqual({[{<<"content-type">>, <<"application/x-msgpack">>}], Bin1}, 261 | encode_body(compound, msgpack, Msg1))]. 262 | 263 | -endif. 264 | -------------------------------------------------------------------------------- /src/lmq_cow_prop.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_cow_prop). 2 | 3 | -export([init/3, rest_init/2, allowed_methods/2, delete_resource/2, 4 | content_types_provided/2, content_types_accepted/2]). 5 | -export([to_json/2, update_props/2]). 6 | 7 | -record(state, {method, name}). 8 | 9 | init(_Transport, _Req, _Opts) -> 10 | {upgrade, protocol, cowboy_rest}. 11 | 12 | rest_init(Req, []) -> 13 | {Name, Req2} = cowboy_req:binding(name, Req), 14 | {Method, Req3} = cowboy_req:method(Req2), 15 | {ok, Req3, #state{method=Method, name=Name}}. 16 | 17 | allowed_methods(Req, #state{name=undefined}=State) -> 18 | {[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}; 19 | allowed_methods(Req, State) -> 20 | {[<<"GET">>, <<"PATCH">>, <<"DELETE">>], Req, State}. 21 | 22 | delete_resource(Req, #state{name=undefined}=State) -> 23 | lmq:set_default_props([]), 24 | {true, Req, State}; 25 | delete_resource(Req, #state{name=Name}=State) -> 26 | lmq:update_props(Name), 27 | {true, Req, State}. 28 | 29 | content_types_provided(Req, State) -> 30 | {[{{<<"application">>, <<"json">>, '*'}, to_json} 31 | ], Req, State}. 32 | 33 | content_types_accepted(Req, State) -> 34 | {[{{<<"application">>, <<"json">>, '*'}, update_props} 35 | ], Req, State}. 36 | 37 | to_json(Req, #state{name=undefined}=State) -> 38 | {jsonx:encode(lmq_api:export_default_props(lmq:get_default_props())), Req, State}; 39 | to_json(Req, #state{name=Name}=State) -> 40 | {jsonx:encode(lmq_api:export_props(lmq:get_props(Name))), Req, State}. 41 | 42 | update_props(Req, State) -> 43 | {ok, Content, Req2} = cowboy_req:body(Req), 44 | update_props(jsonx:decode(Content), Req2, State). 45 | 46 | update_props(Body, Req, #state{name=undefined}=State) -> 47 | case lmq_api:normalize_default_props(Body) of 48 | {ok, Props} -> 49 | lmq:set_default_props(Props), 50 | {true, Req, State}; 51 | {error, _} -> 52 | {false, Req, State} 53 | end; 54 | update_props(Body, Req, #state{name=Name}=State) -> 55 | case lmq_api:normalize_props(Body) of 56 | {ok, Props} -> 57 | lmq:update_props(Name, Props), 58 | {true, Req, State}; 59 | {error, _} -> 60 | {false, Req, State} 61 | end. 62 | -------------------------------------------------------------------------------- /src/lmq_cow_queue.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_cow_queue). 2 | 3 | -export([init/3, rest_init/2, allowed_methods/2, resource_exists/2, delete_resource/2]). 4 | 5 | -record(state, {name}). 6 | 7 | init(_Transport, _Req, _Opts) -> 8 | {upgrade, protocol, cowboy_rest}. 9 | 10 | rest_init(Req, []) -> 11 | {Name, Req2} = cowboy_req:binding(name, Req), 12 | {ok, Req2, #state{name=binary_to_atom(Name, latin1)}}. 13 | 14 | allowed_methods(Req, State) -> 15 | {[<<"DELETE">>], Req, State}. 16 | 17 | resource_exists(Req, #state{name=Name}=State) -> 18 | {lmq_queue_mgr:get(Name) =/= not_found, Req, State}. 19 | 20 | delete_resource(Req, #state{name=Name}=State) -> 21 | ok = lmq:delete(Name), 22 | {true, Req, State}. 23 | -------------------------------------------------------------------------------- /src/lmq_cow_reply.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_cow_reply). 2 | 3 | -export([init/3, rest_init/2, malformed_request/2, allowed_methods/2, 4 | allow_missing_post/2, resource_exists/2, content_types_accepted/2]). 5 | -export([process_post/2]). 6 | 7 | -record(state, {reply, action}). 8 | 9 | init(_Transport, _Req, []) -> 10 | {upgrade, protocol, cowboy_rest}. 11 | 12 | rest_init(Req, _Opts) -> 13 | {Reply, Req2} = cowboy_req:qs_val(<<"reply">>, Req), 14 | {ok, Req2, #state{reply=Reply}}. 15 | 16 | malformed_request(Req, #state{reply=Reply}=State) -> 17 | Invalid = false =:= lists:member(Reply, [<<"ack">>, <<"nack">>, <<"ext">>]), 18 | {Invalid, Req, State}. 19 | 20 | allowed_methods(Req, State) -> 21 | {[<<"POST">>], Req, State}. 22 | 23 | allow_missing_post(Req, State) -> 24 | {false, Req, State}. 25 | 26 | resource_exists(Req, State) -> 27 | %% Since no efficient way exists to know whether the message exists 28 | %% or not, try to process request here. 29 | action(Req, State). 30 | 31 | content_types_accepted(Req, State) -> 32 | {[{'*', process_post}], Req, State}. 33 | 34 | process_post(Req, #state{action=ok}=State) -> 35 | {true, Req, State}; 36 | process_post(Req, State) -> 37 | {false, Req, State}. 38 | 39 | %% ================================================================== 40 | %% Private functions 41 | %% ================================================================== 42 | 43 | action(Req, #state{reply= <<"ack">>}=State) -> 44 | action(ack, Req, State); 45 | action(Req, #state{reply= <<"nack">>}=State) -> 46 | action(abort, Req, State); 47 | action(Req, #state{reply= <<"ext">>}=State) -> 48 | action(keep, Req, State). 49 | 50 | action(Fun, Req, State) -> 51 | {Queue, Req2} = cowboy_req:binding(name, Req), 52 | {MsgId, Req3} = cowboy_req:binding(id, Req2), 53 | case lmq:Fun(Queue, MsgId) of 54 | ok -> {true, Req3, State#state{action=ok}}; 55 | {error, Reason} -> {false, Req3, State#state{action=Reason}} 56 | end. 57 | -------------------------------------------------------------------------------- /src/lmq_cow_stats.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_cow_stats). 2 | 3 | -export([init/3, rest_init/2, allowed_methods/2, content_types_provided/2]). 4 | -export([stats_to_json/2]). 5 | 6 | init(_Transport, _Req, _Opts) -> 7 | {upgrade, protocol, cowboy_rest}. 8 | 9 | rest_init(Req, []) -> 10 | {ok, Req, []}. 11 | 12 | allowed_methods(Req, State) -> 13 | {[<<"GET">>], Req, State}. 14 | 15 | content_types_provided(Req, State) -> 16 | {[{{<<"application">>, <<"json">>, '*'}, stats_to_json} 17 | ], Req, State}. 18 | 19 | stats_to_json(Req, State) -> 20 | Status = lmq:status(), 21 | Stats = lmq:stats(), 22 | Queues = lists:foldr(fun({Name, Info}, Acc) -> 23 | S = jsonify_stats(proplists:get_value(Name, Stats)), 24 | Info2 = [{stats, S}|Info], 25 | [{Name, Info2}|Acc] 26 | end, [], proplists:get_value(queues, Status)), 27 | R = lists:keyreplace(queues, 1, Status, {queues, Queues}), 28 | {jsonx:encode(R), Req, State}. 29 | 30 | jsonify_stats(Stats) -> 31 | R = lists:foldr(fun({K, V}, Acc) when K =:= percentile -> 32 | [{K, jsonify_proplist_keys(V)}|Acc]; 33 | ({K, V}, Acc) when K =:= histogram -> 34 | [{K, jsonify_histogram(V)}|Acc]; 35 | (E, Acc) -> 36 | [E|Acc] 37 | end, [], proplists:get_value(retention, Stats)), 38 | lists:keyreplace(retention, 1, Stats, {retention, R}). 39 | 40 | jsonify_proplist_keys(L) -> 41 | lists:reverse(jsonify_proplist_keys(L, [])). 42 | 43 | jsonify_proplist_keys([], Acc) -> 44 | Acc; 45 | jsonify_proplist_keys([{K, V}|T], Acc) when is_integer(K) -> 46 | jsonify_proplist_keys(T, [{integer_to_binary(K), V}|Acc]); 47 | jsonify_proplist_keys([E|T], Acc) -> 48 | jsonify_proplist_keys(T, [E|Acc]). 49 | 50 | jsonify_histogram(L) -> 51 | lists:foldr(fun({Bin, Count}, Acc) -> [[{bin, Bin}, {count, Count}]|Acc] end, [], L). 52 | 53 | %% ================================================================== 54 | %% EUnit test 55 | %% ================================================================== 56 | -ifdef(TEST). 57 | -include_lib("eunit/include/eunit.hrl"). 58 | 59 | jsonify_stats_test() -> 60 | Value = [{push, 61 | [{count,24}, 62 | {one,0.264930464299551}, 63 | {five,0.07182249856427589}, 64 | {fifteen,0.025679351706894803}, 65 | {day,2.7766597276742793e-4}, 66 | {mean,9.008392962075889e-8}, 67 | {acceleration, 68 | [{one_to_five,6.436932191175838e-4}, 69 | {five_to_fifteen,7.690524476230181e-5}, 70 | {one_to_fifteen,2.658345695473958e-4}]}]}, 71 | {pull, 72 | [{count,24}, 73 | {one,0.264930464299551}, 74 | {five,0.07182249856427589}, 75 | {fifteen,0.025679351706894803}, 76 | {day,2.7766597276742793e-4}, 77 | {mean,9.008394821789985e-8}, 78 | {acceleration, 79 | [{one_to_five,6.436932191175838e-4}, 80 | {five_to_fifteen,7.690524476230181e-5}, 81 | {one_to_fifteen,2.658345695473958e-4}]}]}, 82 | {retention, 83 | [{min,5.290508270263672e-4}, 84 | {max,0.0031280517578125}, 85 | {arithmetic_mean,0.0015397212084601907}, 86 | {geometric_mean,0.00127043818306925}, 87 | {harmonic_mean,0.0010519247023452565}, 88 | {median,0.0016698837280273438}, 89 | {variance,8.87850095456516e-7}, 90 | {standard_deviation,9.422579771254345e-4}, 91 | {skewness,0.43425944017223067}, 92 | {kurtosis,-1.3902812493309589}, 93 | {percentile, 94 | [{50,0.0016698837280273438}, 95 | {75,0.002238035202026367}, 96 | {90,0.0029451847076416016}, 97 | {95,0.0031011104583740234}, 98 | {99,0.0031280517578125}, 99 | {999,0.0031280517578125}]}, 100 | {histogram,[{0.0031280517578125,17}]}, 101 | {n,17}]}], 102 | Expected = [{push, 103 | [{count,24}, 104 | {one,0.264930464299551}, 105 | {five,0.07182249856427589}, 106 | {fifteen,0.025679351706894803}, 107 | {day,2.7766597276742793e-4}, 108 | {mean,9.008392962075889e-8}, 109 | {acceleration, 110 | [{one_to_five,6.436932191175838e-4}, 111 | {five_to_fifteen,7.690524476230181e-5}, 112 | {one_to_fifteen,2.658345695473958e-4}]}]}, 113 | {pull, 114 | [{count,24}, 115 | {one,0.264930464299551}, 116 | {five,0.07182249856427589}, 117 | {fifteen,0.025679351706894803}, 118 | {day,2.7766597276742793e-4}, 119 | {mean,9.008394821789985e-8}, 120 | {acceleration, 121 | [{one_to_five,6.436932191175838e-4}, 122 | {five_to_fifteen,7.690524476230181e-5}, 123 | {one_to_fifteen,2.658345695473958e-4}]}]}, 124 | {retention, 125 | [{min,5.290508270263672e-4}, 126 | {max,0.0031280517578125}, 127 | {arithmetic_mean,0.0015397212084601907}, 128 | {geometric_mean,0.00127043818306925}, 129 | {harmonic_mean,0.0010519247023452565}, 130 | {median,0.0016698837280273438}, 131 | {variance,8.87850095456516e-7}, 132 | {standard_deviation,9.422579771254345e-4}, 133 | {skewness,0.43425944017223067}, 134 | {kurtosis,-1.3902812493309589}, 135 | {percentile, 136 | [{<<"50">>,0.0016698837280273438}, 137 | {<<"75">>,0.002238035202026367}, 138 | {<<"90">>,0.0029451847076416016}, 139 | {<<"95">>,0.0031011104583740234}, 140 | {<<"99">>,0.0031280517578125}, 141 | {<<"999">>,0.0031280517578125}]}, 142 | {histogram,[[{bin, 0.0031280517578125},{count,17}]]}, 143 | {n,17}]}], 144 | ?assertEqual(Expected, jsonify_stats(Value)). 145 | 146 | -endif. 147 | -------------------------------------------------------------------------------- /src/lmq_event.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_event). 2 | 3 | -export([start_link/0, start_link/1, add_handler/1, add_handler/2, 4 | notify/1, notify_remote/2]). 5 | -export([queue_created/1, queue_deleted/1, new_message/1]). 6 | 7 | %% ================================================================== 8 | %% Public API 9 | %% ================================================================== 10 | 11 | start_link() -> 12 | Nodes = [N || N <- mnesia:system_info(running_db_nodes), 13 | N =/= node()], 14 | start_link(Nodes). 15 | 16 | start_link(Nodes) -> 17 | {ok, Pid} = gen_event:start_link({local, ?MODULE}), 18 | add_handler(lmq_handler_dist, [Nodes]), 19 | add_handler(lmq_handler), 20 | {ok, Pid}. 21 | 22 | add_handler(Module) -> 23 | add_handler(Module, []). 24 | 25 | add_handler(Module, Args) -> 26 | gen_event:add_handler(?MODULE, Module, Args). 27 | 28 | notify(Event) -> 29 | gen_event:notify(?MODULE, {local, Event}). 30 | 31 | notify_remote(Node, Event) -> 32 | gen_event:notify({?MODULE, Node}, {remote, Event}). 33 | 34 | queue_created(Name) -> 35 | notify({queue_created, Name}). 36 | 37 | queue_deleted(Name) -> 38 | notify({queue_deleted, Name}). 39 | 40 | new_message(QName) -> 41 | notify({new_message, QName}). 42 | -------------------------------------------------------------------------------- /src/lmq_handler.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_handler). 2 | 3 | -behaviour(gen_event). 4 | 5 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, 6 | code_change/3, terminate/2]). 7 | 8 | %% ================================================================== 9 | %% gen_event callbacks 10 | %% ================================================================== 11 | 12 | init([]) -> 13 | {ok, []}. 14 | 15 | handle_event({local, {new_message, _}}, State) -> 16 | {ok, State}; 17 | 18 | handle_event({remote, {new_message, QName}}, State) -> 19 | lager:debug("new message arrived at remote queue ~s", [QName]), 20 | case lmq_queue_mgr:get(QName) of 21 | not_found -> ok; 22 | Pid -> lmq_queue:notify(Pid) 23 | end, 24 | {ok, State}; 25 | 26 | handle_event({local, {queue_created, Name}}, State) -> 27 | lager:debug("local queue_created ~s", [Name]), 28 | [lmq_mpull:maybe_pull(Pid, Name) || Pid <- lmq_mpull:list_active()], 29 | {ok, State}; 30 | 31 | handle_event({remote, {queue_created, Name}=E}, State) -> 32 | lager:debug("remote queue_created ~s", [Name]), 33 | case lmq_queue_mgr:get(Name) of 34 | not_found -> 35 | lmq_queue_mgr:get(Name, [create]), 36 | handle_event({local, E}, State); 37 | Pid -> 38 | lmq_queue:reload_properties(Pid), 39 | {ok, State} 40 | end; 41 | 42 | handle_event({local, {queue_deleted, _}}, State) -> 43 | {ok, State}; 44 | 45 | handle_event({remote, {queue_deleted, Name}}, State) -> 46 | lager:debug("remote queue deleted ~s", [Name]), 47 | lmq_queue_mgr:delete(Name), 48 | {ok, State}; 49 | 50 | handle_event(Event, State) -> 51 | lager:warning("Unknown event received: ~p", [Event]), 52 | {ok, State}. 53 | 54 | handle_call(_, State) -> 55 | {ok, ok, State}. 56 | 57 | handle_info(_, State) -> 58 | {ok, State}. 59 | 60 | code_change(_OldVsn, State, _Extra) -> 61 | {ok, State}. 62 | 63 | terminate(_Reason, _State) -> 64 | ok. 65 | -------------------------------------------------------------------------------- /src/lmq_handler_dist.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_handler_dist). 2 | 3 | -behaviour(gen_event). 4 | 5 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, 6 | code_change/3, terminate/2]). 7 | 8 | -record(state, {nodes}). 9 | 10 | %% ================================================================== 11 | %% gen_event callbacks 12 | %% ================================================================== 13 | 14 | init([Nodes]) -> 15 | broadcast(Nodes, {join_node, node()}), 16 | {ok, #state{nodes=Nodes}}. 17 | 18 | handle_event({remote, {join_node, Node}}, #state{nodes=Nodes}=S) -> 19 | case lists:member(Node, Nodes) of 20 | true -> {ok, S}; 21 | false -> {ok, S#state{nodes=[Node | Nodes]}} 22 | end; 23 | 24 | handle_event({remote, _Event}, State) -> 25 | {ok, State}; 26 | 27 | handle_event({local, Event}, State) -> 28 | broadcast(State#state.nodes, Event), 29 | {ok, State}; 30 | 31 | handle_event(_Event, State) -> 32 | {ok, State}. 33 | 34 | handle_call(_, State) -> 35 | {ok, ok, State}. 36 | 37 | handle_info(_, State) -> 38 | {ok, State}. 39 | 40 | code_change(_OldVsn, State, _Extra) -> 41 | {ok, State}. 42 | 43 | terminate(_Reason, _State) -> 44 | ok. 45 | 46 | %% ================================================================== 47 | %% Private functions 48 | %% ================================================================== 49 | 50 | broadcast(Nodes, Event) -> 51 | [lmq_event:notify_remote(Node, Event) || Node <- Nodes], 52 | ok. 53 | -------------------------------------------------------------------------------- /src/lmq_hook.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook). 2 | 3 | %% ================================================================== 4 | %% behaviour definition 5 | %% ================================================================== 6 | -callback init() -> 7 | {ok | stop, Reason :: term()}. 8 | 9 | -callback hooks() -> 10 | [atom()]. 11 | 12 | -callback activate([term()]) -> 13 | {ok, term()}. 14 | 15 | -callback deactivate(term()) -> 16 | ok. 17 | 18 | %% ================================================================== 19 | %% hook implementation 20 | %% ================================================================== 21 | %% hook configuration for each queue: 22 | %% [{hook_name, [{lmq_hook_sample1, [term(), ...]}, 23 | %% {lmq_hook_sample2, [term(), ...]}]}] 24 | %% hook state for each queue: 25 | %% [{hook_name, [{lmq_hook_sample1, State}, 26 | %% {lmq_hook_sample2, State}]}] 27 | -behaviour(gen_server). 28 | 29 | -export([start_link/0, stop/0, register/2, call/3]). 30 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 31 | code_change/3, terminate/2]). 32 | 33 | -record(state, {loaded_hooks :: list(module()), 34 | queue_hooks :: dict() 35 | }). 36 | 37 | %% ================================================================== 38 | %% Public API 39 | %% ================================================================== 40 | start_link() -> 41 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 42 | 43 | stop() -> 44 | gen_server:call(?MODULE, stop). 45 | 46 | register(Queue, Config) when is_atom(Queue) -> 47 | gen_server:call(?MODULE, {register, Queue, Config}). 48 | 49 | call(Queue, Hook, Value) when is_atom(Queue), is_atom(Hook) -> 50 | gen_server:call(?MODULE, {call, Queue, Hook, Value}). 51 | 52 | %% ================================================================== 53 | %% gen_server callbacks 54 | %% ================================================================== 55 | init([]) -> 56 | {ok, #state{loaded_hooks=[], queue_hooks=dict:new()}}. 57 | 58 | handle_call({register, Queue, Config}, _From, #state{loaded_hooks=LoadedHooks, 59 | queue_hooks=QueueHooks}=State) -> 60 | try lists:foldl(fun maybe_init_hook/2, LoadedHooks, get_modules(Config)) of 61 | LoadedHooks2 -> 62 | Hooks = case dict:find(Queue, QueueHooks) of 63 | {ok, V} -> V; 64 | error -> [] 65 | end, 66 | try update_hooks(Hooks, Config) of 67 | Hooks2 -> 68 | {reply, ok, State#state{loaded_hooks=LoadedHooks2, 69 | queue_hooks=dict:store(Queue, Hooks2, QueueHooks)}} 70 | catch 71 | error:function_clause -> 72 | {reply, {error, bad_config}, State#state{loaded_hooks=LoadedHooks2}}; 73 | error:not_hookable -> 74 | {reply, {error, bad_config}, State#state{loaded_hooks=LoadedHooks2}} 75 | end 76 | catch 77 | error:undef -> 78 | {reply, {error, bad_hook}, State} 79 | end; 80 | handle_call({call, Queue, HookName, Value}, From, #state{queue_hooks=QueueHooks}=State) -> 81 | case dict:find(Queue, QueueHooks) of 82 | {ok, Hooks} -> 83 | F = fun() -> 84 | R = lists:foldl(fun({Module, S}, Acc) -> 85 | try Module:HookName(Acc, S) 86 | catch _:_ -> Acc 87 | end 88 | end, Value, proplists:get_value(HookName, Hooks, [])), 89 | gen_server:reply(From, R) 90 | end, 91 | spawn_link(F), 92 | {noreply, State}; 93 | error -> 94 | {reply, Value, State} 95 | end; 96 | handle_call(stop, _From, State) -> 97 | {stop, normal, ok, State}. 98 | 99 | handle_cast(_, State) -> 100 | {noreply, State}. 101 | 102 | handle_info(_, State) -> 103 | {noreply, State}. 104 | 105 | code_change(_OldVsn, State, _Extra) -> 106 | {ok, State}. 107 | 108 | terminate(_Reason, _State) -> 109 | ok. 110 | 111 | %% ================================================================== 112 | %% Private functions 113 | %% ================================================================== 114 | get_modules(Config) -> 115 | lists:foldl(fun({_, Hooks}, Acc) -> 116 | Names = proplists:get_keys(Hooks), 117 | lists:merge(Acc, lists:sort(Names)) 118 | end, [], Config). 119 | 120 | maybe_init_hook(Module, Hooks) -> 121 | case proplists:is_defined(Module, Hooks) of 122 | true -> 123 | Hooks; 124 | false -> 125 | ok = Module:init(), 126 | [Module|Hooks] 127 | end. 128 | 129 | update_hooks(OldHooks, Config) -> 130 | update_hooks(OldHooks, Config, []). 131 | 132 | update_hooks([], [], Acc) -> 133 | lists:reverse(Acc); 134 | update_hooks([{_, Modules}|T], [], Acc) -> 135 | lists:foreach(fun({Module, State}) -> 136 | catch Module:deactivate(State) 137 | end, Modules), 138 | update_hooks(T, [], Acc); 139 | update_hooks(OldHooks, [{HookName, Modules}|T], Acc) -> 140 | Creater = fun({Module, Args}) -> 141 | case lists:member(HookName, Module:hooks()) of 142 | true -> 143 | {ok, State} = Module:activate(Args), 144 | {Module, State}; 145 | false -> 146 | erlang:error(not_hookable) 147 | end 148 | end, 149 | Remover = fun({Module, State}) -> catch Module:deactivate(State) end, 150 | Hooks = proplists:get_value(HookName, OldHooks, []), 151 | Hooks2 = sort_same_order(Hooks, Modules, Creater, Remover), 152 | OldHooks2 = proplists:delete(HookName, OldHooks), 153 | update_hooks(OldHooks2, T, [{HookName, Hooks2}|Acc]). 154 | 155 | sort_same_order(List, Guide, Creater, Remover) -> 156 | sort_same_order(List, Guide, [], Creater, Remover). 157 | 158 | sort_same_order([], [], Acc, _, _) -> 159 | lists:reverse(Acc); 160 | sort_same_order(List, [], Acc, Creater, Remover) -> 161 | lists:foreach(Remover, List), 162 | sort_same_order([], [], Acc, Creater, Remover); 163 | sort_same_order([], Guide, Acc, Creater, Remover) -> 164 | Acc2 =lists:foldl(fun(E, A) -> [Creater(E)|A] end, Acc, Guide), 165 | sort_same_order([], [], Acc2, Creater, Remover); 166 | sort_same_order([{Key, _}=Old|List], [{Key, _}=New|Guide], Acc, Creater, Remover) -> 167 | Remover(Old), 168 | sort_same_order(List, Guide, [Creater(New)|Acc], Creater, Remover); 169 | sort_same_order(List, [{Key, _}=E|Guide], Acc, Creater, Remover) -> 170 | case proplists:get_value(Key, List) of 171 | undefined -> 172 | sort_same_order(List, Guide, [Creater(E)|Acc], Creater, Remover); 173 | Other -> 174 | List2 = proplists:delete(Key, List), 175 | sort_same_order(List2, Guide, [{Key, Other}|Acc], Creater, Remover) 176 | end. 177 | 178 | %% ================================================================== 179 | %% EUnit test 180 | %% ================================================================== 181 | -ifdef(TEST). 182 | -include_lib("eunit/include/eunit.hrl"). 183 | 184 | sort_same_order_test_() -> 185 | C = fun({K, [E]}) -> {K, E} end, 186 | R = fun(_) -> ok end, 187 | [?_assertEqual([{a, 1}, {b, 2}], sort_same_order([], [{a, [1]}, {b, [2]}], C, R)), 188 | ?_assertEqual([{b, 2}], sort_same_order([{a, 1}, {b, 2}, {c, 3}], [{b, [2]}], C, R)), 189 | ?_assertEqual([{b, 3}], sort_same_order([{b, 2}], [{b, [3]}], C, R)), 190 | ?_assertEqual([{c, 3}, {d, 4}, {a, 1}], 191 | sort_same_order([{a, 1}, {b, 2}, {c, 3}], [{c, [3]}, {d, [4]}, {a, [1]}], C, R))]. 192 | 193 | -endif. 194 | -------------------------------------------------------------------------------- /src/lmq_hook_preserve_header.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_preserve_header). 2 | -behaviour(lmq_hook). 3 | 4 | -export([init/0, hooks/0, activate/1, deactivate/1]). 5 | -export([pre_http_push/2]). 6 | 7 | init() -> 8 | ok. 9 | 10 | hooks() -> 11 | [pre_http_push]. 12 | 13 | activate(Preserve) -> 14 | {ok, Preserve}. 15 | 16 | deactivate(_) -> 17 | ok. 18 | 19 | pre_http_push({MD, Content, Req}, Preserve) -> 20 | {Hdrs, Req2} = cowboy_req:headers(Req), 21 | MD2 = lists:foldl(fun(K, Acc) -> 22 | case proplists:get_value(K, Hdrs) of 23 | undefined -> Acc; 24 | V -> lists:keystore(K, 1, Acc, {K, V}) 25 | end 26 | end, MD, Preserve), 27 | {MD2, Content, Req2}. 28 | 29 | %% ================================================================== 30 | %% EUnit test 31 | %% ================================================================== 32 | -ifdef(TEST). 33 | -include_lib("eunit/include/eunit.hrl"). 34 | 35 | hooks_test_() -> 36 | [?_assertEqual([pre_http_push], hooks())]. 37 | 38 | activate_test_() -> 39 | [?_assertEqual({ok, [<<"header1">>]}, activate([<<"header1">>]))]. 40 | 41 | deactivate_test_() -> 42 | [?_assertEqual(ok, deactivate([<<"header1">>]))]. 43 | 44 | pre_http_push_test_() -> 45 | Hdrs = [{<<"content-type">>, <<"text/plain">>}, 46 | {<<"x-custom-header">>, <<"foo">>}, 47 | {<<"x-dummy-header">>, <<"dummy">>}], 48 | Req = cowboy_req:new([], [], undefined, <<"POST">>, <<"/messages/test">>, <<"">>, 49 | <<"HTTP/1.1">>, Hdrs, undefined, undefined, <<"">>, false, false, undefined), 50 | MD = [{<<"content-type">>, <<"application/octet-stream">>}], 51 | Body = <<"body">>, 52 | [?_assertEqual({[{<<"content-type">>, <<"application/octet-stream">>}, 53 | {<<"x-custom-header">>, <<"foo">>}], 54 | Body, Req}, 55 | pre_http_push({MD, Body, Req}, [<<"x-custom-header">>])), 56 | ?_assertEqual({[{<<"content-type">>, <<"text/plain">>}, 57 | {<<"x-custom-header">>, <<"foo">>}], 58 | Body, Req}, 59 | pre_http_push({MD, Body, Req}, [<<"content-type">>, <<"x-custom-header">>]))]. 60 | 61 | -endif. 62 | -------------------------------------------------------------------------------- /src/lmq_lib.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_lib). 2 | 3 | -include("lmq.hrl"). 4 | -include_lib("stdlib/include/qlc.hrl"). 5 | -export([init_mnesia/0, create_admin_table/0, 6 | get_lmq_info/1, get_lmq_info/2, set_lmq_info/2, 7 | queue_info/1, update_queue_props/2, all_queue_names/0, create/1, 8 | create/2, delete/1, enqueue/2, enqueue/3, dequeue/2, done/2, retain/3, 9 | release/2, put_back/2, first/1, rfind/2, waittime/1, export_message/1, 10 | get_properties/1, get_properties/2]). 11 | 12 | init_mnesia() -> 13 | case mnesia:system_info(db_nodes) =:= [node()] of 14 | true -> create_admin_table(); 15 | false -> ok 16 | end. 17 | 18 | create_admin_table() -> 19 | case mnesia:create_table(?LMQ_INFO_TABLE, ?LMQ_INFO_TABLE_DEFS) of 20 | {atomic, ok} -> ok; 21 | {aborted, {already_exists, ?LMQ_INFO_TABLE}} -> ok; 22 | Other1 -> 23 | lager:error("Failed to create admin table: ~p", [Other1]) 24 | end, 25 | case mnesia:create_table(?QUEUE_INFO_TABLE, ?QUEUE_INFO_TABLE_DEFS) of 26 | {atomic, ok} -> ok; 27 | {aborted, {already_exists, ?QUEUE_INFO_TABLE}} -> ok; 28 | Other2 -> 29 | lager:error("Failed to create admin table: ~p", [Other2]) 30 | end. 31 | 32 | get_lmq_info(Key) -> 33 | transaction(fun() -> 34 | case mnesia:read(?LMQ_INFO_TABLE, Key) of 35 | [Info] -> {ok, Info#lmq_info.value}; 36 | _ -> {error, not_found} 37 | end 38 | end). 39 | 40 | get_lmq_info(Key, Default) -> 41 | case get_lmq_info(Key) of 42 | {ok, _}=R -> R; 43 | {error, _} -> {ok, Default} 44 | end. 45 | 46 | set_lmq_info(Key, Value) -> 47 | Info = #lmq_info{key=Key, value=Value}, 48 | transaction(fun() -> 49 | ok = mnesia:write(?LMQ_INFO_TABLE, Info, write) 50 | end). 51 | 52 | queue_info(Name) when is_atom(Name) -> 53 | F = fun() -> 54 | case qlc:e(qlc:q([P || #queue_info{name=N, props=P} 55 | <- mnesia:table(?QUEUE_INFO_TABLE), 56 | N =:= Name])) of 57 | [P] -> P; 58 | [] -> not_found 59 | end 60 | end, 61 | transaction(F). 62 | 63 | update_queue_props(Name, Props) when is_atom(Name) -> 64 | Info = #queue_info{name=Name, props=Props}, 65 | transaction(fun() -> 66 | mnesia:write(?QUEUE_INFO_TABLE, Info, write) 67 | end). 68 | 69 | all_queue_names() -> 70 | transaction(fun() -> 71 | qlc:e(qlc:q([N || #queue_info{name=N} 72 | <- mnesia:table(?QUEUE_INFO_TABLE)])) 73 | end). 74 | 75 | create(Name) when is_atom(Name) -> 76 | create(Name, []). 77 | 78 | create(Name, Props) when is_atom(Name) -> 79 | Def = [ 80 | {type, ordered_set}, 81 | {attributes, record_info(fields, message)}, 82 | {record_name, message}, 83 | {ram_copies, mnesia:system_info(db_nodes)} 84 | ], 85 | Info = #queue_info{name=Name, props=Props}, 86 | F = fun() -> mnesia:write(?QUEUE_INFO_TABLE, Info, write) end, 87 | 88 | case mnesia:create_table(Name, Def) of 89 | {atomic, ok} -> 90 | ok = transaction(F), 91 | lmq_event:queue_created(Name); 92 | {aborted, {already_exists, Name}} -> 93 | case queue_info(Name) of 94 | Props -> 95 | ok; 96 | _ -> 97 | ok = transaction(F), 98 | lmq_event:queue_created(Name) 99 | end; 100 | Other -> 101 | lager:error("Failed to create table '~p': ~p", [Name, Other]) 102 | end. 103 | 104 | delete(Name) when is_atom(Name) -> 105 | F = fun() -> mnesia:delete(?QUEUE_INFO_TABLE, Name, write) end, 106 | transaction(F), 107 | case mnesia:delete_table(Name) of 108 | {atomic, ok} -> 109 | lmq_event:queue_deleted(Name), 110 | ok; 111 | {aborted, {no_exists, Name}} -> 112 | ok; 113 | Other -> 114 | lager:error("Failed to delete table '~p': ~p", [Name, Other]) 115 | end. 116 | 117 | enqueue(Name, Content) -> 118 | enqueue(Name, Content, []). 119 | 120 | enqueue(Name, Content, Opts) -> 121 | case proplists:get_value(accum, Opts, 0) == 0 of 122 | true -> 123 | Retry = increment(proplists:get_value(retry, Opts, infinity)), 124 | Msg = #message{content=Content, retry=Retry}, 125 | transaction(fun() -> mnesia:write(Name, Msg, write) end); 126 | false -> %% accumulate duration in milliseconds 127 | accumulate_message(Name, Content, Opts) 128 | end. 129 | 130 | accumulate_message(Name, Content, Opts) -> 131 | transaction(fun() -> 132 | QC = qlc:cursor(qlc:q([M || M=#message{id={TS, _}, state=accum} 133 | <- mnesia:table(Name), 134 | TS >= lmq_misc:unixtime()])), 135 | {Ret, Msg} = case qlc:next_answers(QC, 1) of 136 | [M] -> %% accumulating process already started 137 | Content1 = M#message.content ++ [Content], 138 | {{accum, yes}, M#message{content=Content1}}; 139 | [] -> %% add new message for accumulating 140 | Retry = increment(proplists:get_value(retry, Opts, infinity)), 141 | Duration = proplists:get_value(accum, Opts), 142 | Id={lmq_misc:unixtime() + Duration / 1000, uuid:get_v4()}, 143 | {{accum, new}, #message{id=Id, state=accum, type=compound, 144 | retry=Retry, content=[Content]}} 145 | end, 146 | mnesia:write(Name, Msg, write), 147 | ok = qlc:delete_cursor(QC), 148 | Ret 149 | end). 150 | 151 | dequeue(Name, Timeout) -> 152 | case transaction(fun() -> get_first_message(Name, Timeout) end) of 153 | {ok, Msg, Retention} -> 154 | lmq_metrics:update_metric(Name, retention, Retention), 155 | Msg; 156 | continue -> 157 | dequeue(Name, Timeout); 158 | Other -> 159 | Other 160 | end. 161 | 162 | get_first_message(Name, Timeout) -> 163 | case mnesia:first(Name) of 164 | '$end_of_table' -> 165 | empty; 166 | Key -> 167 | [M] = mnesia:read(Name, Key, read), 168 | {TS, _} = M#message.id, 169 | Now = lmq_misc:unixtime(), 170 | case TS > Now of 171 | true -> 172 | empty; 173 | false -> 174 | mnesia:delete(Name, Key, write), 175 | NewId = {Now + Timeout, uuid:get_v4()}, 176 | case M#message.retry of 177 | infinity -> 178 | NewMsg = M#message{id=NewId, state=processing}, 179 | mnesia:write(Name, NewMsg, write), 180 | {ok, NewMsg, Now - TS}; 181 | N when N > 0 -> 182 | NewMsg = M#message{id=NewId, state=processing, retry=N-1}, 183 | mnesia:write(Name, NewMsg, write), 184 | {ok, NewMsg, Now - TS}; 185 | _ -> 186 | %% quit transaction to avoid infinite loop caused by 187 | %% large amount of waste messages 188 | continue 189 | end 190 | end 191 | end. 192 | 193 | increment(infinity) -> 194 | infinity; 195 | 196 | increment(Number) when is_integer(Number) -> 197 | Number + 1. 198 | 199 | done(Name, UUID) -> 200 | Now = lmq_misc:unixtime(), 201 | F = fun() -> 202 | case rfind(Name, UUID) of 203 | '$end_of_table' -> 204 | not_found; 205 | #message{id={TS, UUID}} when TS < Now -> 206 | not_found; 207 | #message{id=Key, state=processing} -> 208 | mnesia:delete(Name, Key, write); 209 | _ -> 210 | not_found 211 | end 212 | end, 213 | transaction(F). 214 | 215 | release(Name, UUID) -> 216 | put_back(Name, UUID, consume). 217 | 218 | put_back(Name, UUID) -> 219 | put_back(Name, UUID, keep). 220 | 221 | put_back(Name, UUID, Retry) -> 222 | Now = lmq_misc:unixtime(), 223 | F = fun() -> 224 | case rfind(Name, UUID) of 225 | '$end_of_table' -> 226 | not_found; 227 | #message{id={TS, UUID}} when TS < Now -> 228 | not_found; 229 | #message{state=processing, retry=R}=M -> 230 | M1 = if Retry =:= consume -> 231 | M#message{id={Now, UUID}, state=available, retry=R}; 232 | true -> 233 | M#message{id={Now, UUID}, state=available, retry=increment(R)} 234 | end, 235 | mnesia:write(Name, M1, write), 236 | mnesia:delete(Name, M#message.id, write); 237 | _ -> 238 | not_found 239 | end 240 | end, 241 | transaction(F). 242 | 243 | retain(Name, UUID, Timeout) -> 244 | Now = lmq_misc:unixtime(), 245 | F = fun() -> 246 | case rfind(Name, UUID) of 247 | '$end_of_table' -> 248 | not_found; 249 | #message{id={TS, UUID}} when TS < Now -> 250 | not_found; 251 | #message{state=processing}=M -> 252 | M1 = M#message{id={Now + Timeout, UUID}}, 253 | mnesia:write(Name, M1, write), 254 | mnesia:delete(Name, M#message.id, write); 255 | _ -> 256 | not_found 257 | end 258 | end, 259 | transaction(F). 260 | 261 | waittime(Name) -> 262 | case first(Name) of 263 | {error, _}=E -> E; 264 | empty -> infinity; 265 | Message -> 266 | {TS, _} = Message#message.id, 267 | Timeout = round((TS - lmq_misc:unixtime()) * 1000), 268 | lists:max([Timeout, 0]) 269 | end. 270 | 271 | first(Name) -> 272 | F = fun() -> 273 | case mnesia:first(Name) of 274 | '$end_of_table' -> empty; 275 | Key -> 276 | [Item] = mnesia:read(Name, Key, read), 277 | Item 278 | end 279 | end, 280 | transaction(F). 281 | 282 | rfind(Tab, Id) -> 283 | F = fun(M, _Acc) when element(2, M#message.id) =:= Id -> 284 | %% break loop, throw will be handled by foldr 285 | throw(M); 286 | (_, Acc) -> Acc 287 | end, 288 | mnesia:foldr(F, '$end_of_table', Tab). 289 | 290 | transaction(F) -> 291 | case mnesia:transaction(F) of 292 | {atomic, Val} -> Val; 293 | {aborted, {no_exists, _}} -> {error, no_queue_exists}; 294 | {aborted, Reason} -> {error, Reason} 295 | end. 296 | 297 | get_properties(Name) -> 298 | Base = get_props(Name), 299 | case lmq_lib:queue_info(Name) of 300 | not_found -> Base; 301 | Props -> lmq_misc:extend(Props, Base) 302 | end. 303 | 304 | get_properties(Name, Override) -> 305 | Props = get_properties(Name), 306 | lmq_misc:extend(Override, Props). 307 | 308 | get_props(Name) -> 309 | {ok, DefaultProps} = get_lmq_info(default_props, []), 310 | get_props(Name, DefaultProps). 311 | 312 | get_props(_Name, []) -> 313 | ?DEFAULT_QUEUE_PROPS; 314 | 315 | get_props(Name, PropsList) when is_atom(Name) -> 316 | get_props(atom_to_list(Name), PropsList); 317 | 318 | get_props(Name, [{Regexp, Props} | T]) when is_list(Name) -> 319 | {ok, MP} = re:compile(Regexp), 320 | case re:run(Name, MP) of 321 | {match, _} -> lmq_misc:extend(Props, ?DEFAULT_QUEUE_PROPS); 322 | _ -> get_props(Name, T) 323 | end. 324 | 325 | export_message(M=#message{}) -> 326 | UUID = list_to_binary(uuid:uuid_to_string(element(2, M#message.id))), 327 | [{id, UUID}, {type, M#message.type}, {retry, M#message.retry}, {content, M#message.content}]. 328 | 329 | %% ================================================================== 330 | %% EUnit tests 331 | %% ================================================================== 332 | 333 | -ifdef(TEST). 334 | -include_lib("eunit/include/eunit.hrl"). 335 | 336 | get_props_test() -> 337 | DefaultProps = [{"lmq", [{retry, 0}]}], 338 | Props = lmq_misc:extend([{retry, 0}], ?DEFAULT_QUEUE_PROPS), 339 | ?assertEqual(Props, get_props(lmq, DefaultProps)), 340 | ?assertEqual(Props, get_props("lmq", DefaultProps)), 341 | ?assertEqual(?DEFAULT_QUEUE_PROPS, get_props("foo", DefaultProps)), 342 | ?assertEqual(?DEFAULT_QUEUE_PROPS, get_props("lmq", [])). 343 | 344 | export_message_test() -> 345 | Ref = make_ref(), 346 | M = #message{content=Ref}, 347 | UUID = list_to_binary(uuid:uuid_to_string(element(2, M#message.id))), 348 | ?assertEqual([{id, UUID}, {type, normal}, {retry, 0}, {content, Ref}], 349 | export_message(M)), 350 | 351 | M2 = M#message{type=compound, retry=1}, 352 | ?assertEqual([{id, UUID}, {type, compound}, {retry, 1}, {content, Ref}], 353 | export_message(M2)). 354 | 355 | -endif. 356 | -------------------------------------------------------------------------------- /src/lmq_metrics.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_metrics). 2 | 3 | -export([create_queue_metrics/1, update_metric/2, update_metric/3, 4 | get_metric/2, get_metric_name/2]). 5 | 6 | -include("lmq.hrl"). 7 | 8 | %% ================================================================== 9 | %% Public API 10 | %% ================================================================== 11 | 12 | create_queue_metrics(Name) when is_atom(Name) -> 13 | folsom_metrics:new_meter(get_metric_name(Name, push)), 14 | folsom_metrics:new_meter(get_metric_name(Name, pull)), 15 | folsom_metrics:new_histogram(get_metric_name(Name, retention), 16 | slide_uniform, {60, 100}), 17 | 18 | Metrics = [get_metric_name(Name, push), 19 | get_metric_name(Name, pull), 20 | get_metric_name(Name, retention)], 21 | [folsom_metrics:tag_metric(M, Name) || M <- Metrics], 22 | [folsom_metrics:tag_metric(M, ?LMQ_ALL_METRICS) || M <- Metrics], 23 | ok. 24 | 25 | update_metric(Name, push) when is_atom(Name) -> 26 | folsom_metrics:notify({get_metric_name(Name, push), 1}), 27 | influxdb_client:write(influxdb_data(rate, [queue, action, count], [[Name, push, 1]])), 28 | statsderl:increment(statsd_name(Name, push), 1, ?STATSD_SAMPLERATE); 29 | 30 | update_metric(Name, pull) when is_atom(Name) -> 31 | folsom_metrics:notify({get_metric_name(Name, pull), 1}), 32 | influxdb_client:write(influxdb_data(rate, [queue, action, count], [[Name, pull, 1]])), 33 | statsderl:increment(statsd_name(Name, pull), 1, ?STATSD_SAMPLERATE); 34 | update_metric(Name, List) when is_atom(Name), is_list(List) -> 35 | {Columns, Points} = lists:foldl(fun({Type, Value}, {Columns, Points}) -> 36 | update_metric(Name, Type, Value), 37 | {[Type|Columns], [Value|Points]} 38 | end, {[queue], [Name]}, List), 39 | influxdb_client:write(influxdb_data(stats, Columns, [Points])). 40 | 41 | update_metric(Name, retention, Time) when is_atom(Name) -> 42 | folsom_metrics:notify({get_metric_name(Name, retention), Time}), 43 | influxdb_client:write(influxdb_data(stats, [queue, retention], [[Name, Time]])), 44 | statsderl:timing(statsd_name(Name, retention), Time, ?STATSD_SAMPLERATE); 45 | update_metric(Name, Type, Value) -> 46 | statsderl:gauge(statsd_name(Name, Type), Value, 1). 47 | 48 | get_metric(Name, push) when is_atom(Name) -> 49 | folsom_metrics:get_metric_value(get_metric_name(Name, push)); 50 | 51 | get_metric(Name, pull) when is_atom(Name) -> 52 | folsom_metrics:get_metric_value(get_metric_name(Name, pull)); 53 | 54 | get_metric(Name, retention) when is_atom(Name) -> 55 | folsom_metrics:get_histogram_statistics(get_metric_name(Name, retention)). 56 | 57 | get_metric_name(Name, Type) when is_atom(Name), is_atom(Type) -> 58 | get_metric_name(atom_to_binary(Name, latin1), Type); 59 | 60 | get_metric_name(Name, push) when is_binary(Name) -> 61 | get_metric_name(Name, <<"push">>); 62 | 63 | get_metric_name(Name, pull) when is_binary(Name) -> 64 | get_metric_name(Name, <<"pull">>); 65 | 66 | get_metric_name(Name, retention) when is_binary(Name) -> 67 | get_metric_name(Name, <<"retention">>); 68 | 69 | get_metric_name(Name, Type) when is_binary(Name), is_binary(Type) -> 70 | <>/binary, Type/binary>>. 71 | 72 | %% ================================================================== 73 | %% EUnit tests 74 | %% ================================================================== 75 | 76 | statsd_name(Name, Action) -> 77 | lists:flatten(io_lib:format("lmq.~s.~s", [Name, Action])). 78 | 79 | influxdb_data(Name, Columns, Points) -> 80 | [[{name, Name}, {columns, Columns}, {points, Points}]]. 81 | 82 | -ifdef(TEST). 83 | -include_lib("eunit/include/eunit.hrl"). 84 | 85 | get_metric_name_test() -> 86 | ?assertEqual(<<"foo_push">>, get_metric_name(foo, push)), 87 | ?assertEqual(<<"foo_pull">>, get_metric_name(foo, pull)), 88 | ?assertEqual(<<"foo_retention">>, get_metric_name(foo, retention)). 89 | 90 | statsd_name_test() -> 91 | ?assertEqual("lmq.queue.push", statsd_name(queue, push)), 92 | ?assertEqual("lmq.Q/N.push", statsd_name('Q/N', push)). 93 | 94 | -endif. 95 | -------------------------------------------------------------------------------- /src/lmq_misc.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_misc). 2 | -export([unixtime/0, extend/2, btof/1]). 3 | 4 | unixtime() -> 5 | {MegaSecs, Secs, MicroSecs} = os:timestamp(), 6 | MegaSecs * 1000000 + Secs + MicroSecs / 1000000. 7 | 8 | extend(Override, Base) -> 9 | Props = lists:foldl(fun({K, _}=T, Acc) -> 10 | lists:keystore(K, 1, Acc, T) 11 | end, Base, Override), 12 | lists:keysort(1, Props). 13 | 14 | btof(B) -> 15 | B2 = <>, 16 | case string:to_float(binary_to_list(B2)) of 17 | {error, _} -> {error, badarg}; 18 | {F, _} -> {ok, F} 19 | end. 20 | 21 | -ifdef(TEST). 22 | -include_lib("eunit/include/eunit.hrl"). 23 | 24 | -define(assertEq(S, Expected), 25 | try 26 | ?assertEqual(S, Expected) 27 | catch _:_ -> 28 | ?debugFmt("~nexpected: ~p~n", [Expected]), 29 | ?debugFmt("~ngot: ~p~n", [S]), 30 | throw(assertion_error) 31 | end). 32 | 33 | unixtime_test() -> 34 | T = lmq_misc:unixtime(), 35 | ?assert(is_float(T)), 36 | ?assertEq(round(T) div 1000000000, 1). 37 | 38 | extend_test() -> 39 | ?assertEq(lmq_misc:extend([{retry, infinity}, {timeout, 5}], 40 | [{timeout, 30}, {type, normal}]), 41 | [{retry, infinity}, {timeout, 5}, {type, normal}]), 42 | ?assertEq(lmq_misc:extend([], [{timeout, 30}, {type, normal}]), 43 | [{timeout, 30}, {type, normal}]). 44 | 45 | binary_to_float_test() -> 46 | ?assertEq({ok, 10.0}, btof(<<"10">>)), 47 | ?assertEq({ok, 10.0}, btof(<<"10.0">>)), 48 | ?assertEq({ok, 12.34}, btof(<<"12.34">>)), 49 | ?assertEq({ok, 12.34}, btof(<<"12.34abc">>)), 50 | ?assertEq({error, badarg}, btof(<<"abc">>)). 51 | 52 | -endif. 53 | -------------------------------------------------------------------------------- /src/lmq_mpull.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_mpull). 2 | 3 | -behaviour(gen_fsm). 4 | 5 | -include("lmq.hrl"). 6 | 7 | -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, 8 | terminate/3, code_change/4, 9 | idle/2, idle/3, waiting/2, finalize/2]). 10 | -export([start/0, start_link/0, pull/2, pull/3, 11 | pull_async/2, pull_async/3, pull_cancel/1, 12 | maybe_pull/2, list_active/0]). 13 | 14 | -define(UNEXPECTED(Event, State), 15 | lager:warning("~p received unknown event ~p while in state ~p", 16 | [self(), Event, State])). 17 | -define(CLOSE_WAIT, 10). 18 | 19 | -record(state, {from, monitor, regexp, timeout, mapping}). 20 | 21 | %% ================================================================== 22 | %% Public API 23 | %% ================================================================== 24 | 25 | start() -> 26 | supervisor:start_child(lmq_mpull_sup, []). 27 | 28 | start_link() -> 29 | gen_fsm:start_link(?MODULE, [], []). 30 | 31 | pull(Pid, Regexp) -> 32 | gen_fsm:sync_send_event(Pid, {pull, Regexp, infinity}, infinity). 33 | 34 | pull(Pid, Regexp, Timeout) -> 35 | gen_fsm:sync_send_event(Pid, {pull, Regexp, Timeout}, infinity). 36 | 37 | pull_async(Pid, Regexp) -> 38 | pull_async(Pid, Regexp, infinity). 39 | 40 | pull_async(Pid, Regexp, Timeout) -> 41 | gen_fsm:sync_send_event(Pid, {pull_async, Regexp, Timeout}). 42 | 43 | pull_cancel(Pid) -> 44 | gen_fsm:send_event(Pid, cancel). 45 | 46 | maybe_pull(Pid, QName) when is_atom(QName) -> 47 | gen_fsm:send_event(Pid, {maybe_pull, QName}). 48 | 49 | list_active() -> 50 | [Pid || {_, Pid, _, _} <- supervisor:which_children(lmq_mpull_sup)]. 51 | 52 | %% ================================================================== 53 | %% gen_fsm callbacks 54 | %% ================================================================== 55 | 56 | init([]) -> 57 | {ok, idle, #state{}}. 58 | 59 | idle(Event, State) -> 60 | ?UNEXPECTED(Event, idle), 61 | {next_state, idle, State}. 62 | 63 | idle({pull, Regexp, Timeout}, {Pid, _}=From, #state{}=S) -> 64 | Monitor = erlang:monitor(process, Pid), 65 | case lmq_queue_mgr:match(Regexp) of 66 | {error, _}=R -> 67 | {stop, error, R, S#state{from=From, monitor=Monitor}}; 68 | Queues -> 69 | Mapping = lists:foldl(fun({_, QPid}=Q, Acc) -> 70 | Id = lmq_queue:pull_async(QPid, Timeout), 71 | dict:store(Id, Q, Acc) 72 | end, dict:new(), Queues), 73 | if is_number(Timeout), Timeout > 0 -> 74 | gen_fsm:send_event_after(Timeout, cancel); 75 | true -> ok 76 | end, 77 | State = S#state{from=From, monitor=Monitor, regexp=Regexp, 78 | timeout=Timeout, mapping=Mapping}, 79 | {next_state, waiting, State} 80 | end; 81 | 82 | idle({pull_async, Regexp, Timeout}, {Pid, _}=From, #state{}=S) -> 83 | case idle({pull, Regexp, Timeout}, From, S) of 84 | {next_state, Next, State} -> 85 | Ref = make_ref(), 86 | {reply, {ok, Ref}, Next, State#state{from={async, Pid, Ref}}}; 87 | Other -> 88 | Other 89 | end; 90 | 91 | idle(Event, _From, State) -> 92 | ?UNEXPECTED(Event, idle), 93 | {next_state, idle, State}. 94 | 95 | waiting({maybe_pull, QName}, #state{}=S) -> 96 | State = case re:compile(S#state.regexp) of 97 | {ok, MP} -> 98 | case re:run(atom_to_list(QName), MP) of 99 | {match, _} -> 100 | case lmq_queue_mgr:get(QName) of 101 | not_found -> S; 102 | Pid -> 103 | Id = lmq_queue:pull_async(Pid, S#state.timeout), 104 | Mapping = dict:store(Id, {QName, Pid}, S#state.mapping), 105 | S#state{mapping=Mapping} 106 | end; 107 | _ -> S 108 | end; 109 | {error, _} -> S 110 | end, 111 | {next_state, waiting, State}; 112 | 113 | waiting(cancel, #state{}=S) -> 114 | waiting(timeout, S); 115 | 116 | waiting(timeout, #state{}=S) -> 117 | cancel_pull(S#state.mapping), 118 | reply(S#state.from, empty), 119 | {next_state, finalize, S, ?CLOSE_WAIT}; 120 | 121 | waiting(Event, State) -> 122 | ?UNEXPECTED(Event, waiting), 123 | {next_state, waiting, State}. 124 | 125 | finalize(timeout, State) -> 126 | {stop, normal, State}; 127 | 128 | finalize(Event, State) -> 129 | ?UNEXPECTED(Event, finalize), 130 | {next_state, finalize, State}. 131 | 132 | handle_info({Id, #message{}=M}, waiting, #state{mapping=Mapping}=S) -> 133 | {Name, _} = dict:fetch(Id, Mapping), 134 | cancel_pull(dict:erase(Id, Mapping)), 135 | Response = lmq_lib:export_message(M), 136 | reply(S#state.from, [{queue, Name} | Response]), 137 | {next_state, finalize, S, ?CLOSE_WAIT}; 138 | 139 | handle_info({Id, {error, Reason}}, waiting, #state{mapping=Mapping}=S) -> 140 | Mapping1 = dict:erase(Id, Mapping), 141 | lager:debug("pull_any for ~p: ~p, rest ~p", 142 | [element(1, dict:fetch(Id, Mapping)), Reason, dict:size(Mapping1)]), 143 | case dict:size(Mapping1) of 144 | 0 -> 145 | reply(S#state.from, empty), 146 | %% it is safe to shutdown because all responses are received. 147 | {stop, normal, S}; 148 | _ -> 149 | %% short period for procces messages that already in the mailbox. 150 | {next_state, waiting, S#state{mapping=Mapping1}, ?CLOSE_WAIT} 151 | end; 152 | 153 | handle_info({Id, #message{id={_, UUID}}}, finalize, #state{}=S) -> 154 | {_, Pid} = dict:fetch(Id, S#state.mapping), 155 | lmq_queue:put_back(Pid, UUID), 156 | {next_state, finalize, S, ?CLOSE_WAIT}; 157 | 158 | handle_info({_Id, {error, _Reason}}, finalize, State) -> 159 | {next_state, finalize, State, ?CLOSE_WAIT}; 160 | 161 | handle_info({'DOWN', Monitor, process, _, _}, _, #state{monitor=Monitor}=S) -> 162 | cancel_pull(S#state.mapping), 163 | {next_state, finalize, S, ?CLOSE_WAIT}; 164 | 165 | handle_info(Event, StateName, State) -> 166 | ?UNEXPECTED(Event, StateName), 167 | {next_state, StateName, State}. 168 | 169 | handle_event(Event, StateName, State) -> 170 | ?UNEXPECTED(Event, StateName), 171 | {next_state, StateName, State}. 172 | 173 | handle_sync_event(Event, _From, StateName, State) -> 174 | ?UNEXPECTED(Event, StateName), 175 | {reply, error, StateName, State}. 176 | 177 | terminate(normal, _StateName, #state{monitor=Monitor}) -> 178 | erlang:demonitor(Monitor, [flush]), 179 | ok; 180 | 181 | terminate(error, _StateName, #state{monitor=Monitor}) -> 182 | erlang:demonitor(Monitor, [flush]), 183 | ok. 184 | 185 | code_change(_OldVsn, StateName, State, _Extra) -> 186 | {ok, StateName, State}. 187 | 188 | %% ================================================================== 189 | %% Private functions 190 | %% ================================================================== 191 | 192 | cancel_pull(Mapping) -> 193 | %% after calling this function, queues never sent a new message. 194 | dict:map(fun(Id, {_, Pid}) -> lmq_queue:pull_cancel(Pid, Id) end, Mapping). 195 | 196 | reply({async, Pid, Ref}, Msg) -> 197 | Pid ! {Ref, Msg}; 198 | reply(From, Msg) -> 199 | gen_fsm:reply(From, Msg). 200 | -------------------------------------------------------------------------------- /src/lmq_mpull_sup.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_mpull_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([init/1]). 6 | -export([start_link/0]). 7 | 8 | start_link() -> 9 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 10 | 11 | init([]) -> 12 | MaxRestart = 5, 13 | MaxTime = 3600, 14 | {ok, {{simple_one_for_one, MaxRestart, MaxTime}, 15 | [{lmq_mpull, 16 | {lmq_mpull, start_link, []}, 17 | temporary, 18 | 5000, 19 | worker, 20 | [lmq_mpull]}]}}. 21 | -------------------------------------------------------------------------------- /src/lmq_queue.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue). 2 | -behaviour(gen_server). 3 | -export([start/1, start/2, start_link/1, start_link/2, stop/1, notify/1, 4 | push/2, pull/1, pull/2, pull_async/1, pull_async/2, pull_cancel/2, 5 | done/2, retain/2, release/2, put_back/2, props/2, get_properties/1, 6 | reload_properties/1]). 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 8 | code_change/3, terminate/2]). 9 | 10 | -include("lmq.hrl"). 11 | -record(state, {name, props, raw_props, waiting=queue:new(), monitors=gb_sets:empty()}). 12 | -record(waiting, {from, ref, timeout, start_time=lmq_misc:unixtime()}). 13 | 14 | start(Name) -> 15 | supervisor:start_child(lmq_queue_sup, [Name]). 16 | 17 | start(Name, Props) -> 18 | supervisor:start_child(lmq_queue_sup, [Name, Props]). 19 | 20 | start_link(Name) when is_atom(Name) -> 21 | case lmq_lib:queue_info(Name) of 22 | not_found -> ok = lmq_lib:create(Name); 23 | _ -> ok 24 | end, 25 | gen_server:start_link(?MODULE, Name, []). 26 | 27 | start_link(Name, Props) when is_atom(Name) -> 28 | ok = lmq_lib:create(Name, Props), 29 | gen_server:start_link(?MODULE, Name, []). 30 | 31 | push(Pid, Content) -> 32 | gen_server:call(Pid, {push, Content}). 33 | 34 | pull(Pid) -> 35 | gen_server:call(Pid, {pull, infinity}, infinity). 36 | 37 | pull(Pid, 0) -> 38 | %% in this case, cannot use gen_server's timeout 39 | case gen_server:call(Pid, {pull, 0}) of 40 | {error, timeout} -> empty; 41 | R -> R 42 | end; 43 | 44 | pull(Pid, Timeout) -> 45 | try 46 | case gen_server:call(Pid, {pull, Timeout}, round(Timeout * 1000)) of 47 | {error, timeout} -> empty; 48 | R -> R 49 | end 50 | catch 51 | exit:{timeout, _} -> empty 52 | end. 53 | 54 | pull_async(Pid) -> 55 | pull_async(Pid, infinity). 56 | 57 | pull_async(Pid, Timeout) -> 58 | gen_server:call(Pid, {pull_async, Timeout}). 59 | 60 | pull_cancel(Pid, Ref) -> 61 | gen_server:call(Pid, {pull_cancel, Ref}). 62 | 63 | done(Pid, UUID) -> 64 | gen_server:call(Pid, {done, UUID}). 65 | 66 | retain(Pid, UUID) -> 67 | gen_server:call(Pid, {retain, UUID}). 68 | 69 | release(Pid, UUID) -> 70 | gen_server:call(Pid, {release, UUID}). 71 | 72 | put_back(Pid, UUID) -> 73 | gen_server:call(Pid, {put_back, UUID}). 74 | 75 | props(Pid, Props) -> 76 | gen_server:call(Pid, {props, Props}). 77 | 78 | get_properties(Pid) -> 79 | gen_server:call(Pid, get_properties). 80 | 81 | reload_properties(Pid) -> 82 | gen_server:cast(Pid, reload_properties). 83 | 84 | notify(Pid) -> 85 | gen_server:cast(Pid, notify). 86 | 87 | stop(Pid) -> 88 | gen_server:call(Pid, stop). 89 | 90 | %% ================================================================== 91 | %% gen_server callbacks 92 | %% ================================================================== 93 | 94 | init(Name) -> 95 | lager:info("Starting the queue: ~s ~p", [Name, self()]), 96 | Props = lmq_lib:get_properties(Name), 97 | RawProps = lmq_lib:queue_info(Name), 98 | lmq_hook:register(Name, proplists:get_value(hooks, Props, [])), 99 | %% this is necessary when a queue restarted by supervisor 100 | lmq_queue_mgr:queue_started(Name, self()), 101 | ok = lmq_metrics:create_queue_metrics(Name), 102 | {ok, #state{name=Name, props=Props, raw_props=RawProps}}. 103 | 104 | handle_call(stop, _From, State) -> 105 | lager:info("Stopping the queue: ~s ~p", [State#state.name, self()]), 106 | {stop, normal, ok, State}; 107 | 108 | handle_call(Msg, From, State) -> 109 | case handle_queue_call(Msg, From, State) of 110 | {reply, Reply, State1} -> 111 | {State2, Sleep} = prepare_sleep(State1), 112 | {reply, Reply, State2, Sleep}; 113 | {noreply, State1} -> 114 | {State2, Sleep} = prepare_sleep(State1), 115 | {noreply, State2, Sleep} 116 | end. 117 | 118 | handle_cast(reload_properties, S) -> 119 | Props = lmq_lib:get_properties(S#state.name), 120 | lmq_hook:register(S#state.name, proplists:get_value(hooks, Props, [])), 121 | lager:info("Reload queue properties: ~s ~p", [S#state.name, Props]), 122 | State = S#state{props=Props}, 123 | {State1, Sleep} = prepare_sleep(State), 124 | {noreply, State1, Sleep}; 125 | 126 | handle_cast(notify, State) -> 127 | {State1, Sleep} = prepare_sleep(State), 128 | {noreply, State1, Sleep}; 129 | 130 | handle_cast(Msg, State) -> 131 | lager:warning("Unknown message received: ~p", [Msg]), 132 | {noreply, State}. 133 | 134 | handle_info(timeout, S=#state{}) -> 135 | NewState = maybe_push_message(S), 136 | lager:debug("number of waitings in ~p: ~p", [S#state.name, queue:len(NewState#state.waiting)]), 137 | {State, Sleep} = prepare_sleep(NewState), 138 | {noreply, State, Sleep}; 139 | 140 | handle_info({'DOWN', Ref, process, _Pid, _}, S=#state{}) -> 141 | State = remove_waiting(Ref, S), 142 | {State1, Sleep} = prepare_sleep(State), 143 | {noreply, State1, Sleep}; 144 | 145 | handle_info(Msg, State) -> 146 | lager:warning("Unknown message received: ~p", [Msg]), 147 | {noreply, State}. 148 | 149 | code_change(_OldVsn, State, _Extra) -> 150 | {ok, State}. 151 | 152 | terminate(_Reason, _State) -> 153 | ok. 154 | 155 | %% ================================================================== 156 | %% Private functions 157 | %% ================================================================== 158 | 159 | handle_queue_call({push, Content}, _From, S=#state{}) -> 160 | R = lmq_lib:enqueue(S#state.name, Content, S#state.props), 161 | lmq_event:new_message(S#state.name), 162 | lmq_metrics:update_metric(S#state.name, push), 163 | {reply, R, S}; 164 | 165 | handle_queue_call({pull, Timeout}, From={Pid, _}, S=#state{}) -> 166 | State = add_waiting(From, Pid, Timeout, S), 167 | {noreply, State}; 168 | 169 | handle_queue_call({pull_async, Timeout}, {Pid, _}, S=#state{}) -> 170 | State = add_waiting(Pid, Timeout, S), 171 | Ref = (queue:get_r(State#state.waiting))#waiting.ref, 172 | {reply, Ref, State}; 173 | 174 | handle_queue_call({pull_cancel, Ref}, _From, S=#state{}) -> 175 | State = remove_waiting(Ref, S), 176 | {reply, ok, State}; 177 | 178 | handle_queue_call({done, UUID}, _From, S=#state{}) -> 179 | R = lmq_lib:done(S#state.name, UUID), 180 | {reply, R, S}; 181 | 182 | handle_queue_call({retain, UUID}, _From, S=#state{props=Props}) -> 183 | R = lmq_lib:retain(S#state.name, UUID, proplists:get_value(timeout, Props)), 184 | {reply, R, S}; 185 | 186 | handle_queue_call({release, UUID}, _From, S=#state{}) -> 187 | R = lmq_lib:release(S#state.name, UUID), 188 | {reply, R, S}; 189 | 190 | handle_queue_call({put_back, UUID}, _From, S=#state{}) -> 191 | R = lmq_lib:put_back(S#state.name, UUID), 192 | {reply, R, S}; 193 | 194 | handle_queue_call({props, Props}, _From, S=#state{raw_props=Props}) -> 195 | lager:info("Queue property of ~s already set to ~p", [S#state.name, Props]), 196 | {reply, ok, S}; 197 | handle_queue_call({props, Props}, _From, S=#state{}) -> 198 | lmq_lib:update_queue_props(S#state.name, Props), 199 | Props1 = lmq_lib:get_properties(S#state.name), 200 | lmq_hook:register(S#state.name, proplists:get_value(hooks, Props1, [])), 201 | lager:info("Queue property of ~s updated to ~p", [S#state.name, Props1]), 202 | State = S#state{props=Props1, raw_props=Props}, 203 | {reply, ok, State}; 204 | 205 | handle_queue_call(get_properties, _From, S) -> 206 | Props = S#state.props, 207 | {reply, Props, S}. 208 | 209 | maybe_push_message(S=#state{props=Props, waiting=Waiting}) -> 210 | case queue:out(Waiting) of 211 | {{value, W=#waiting{ref=Ref}}, NewWaiting} -> 212 | Timeout = proplists:get_value(timeout, Props), 213 | %% timeout = 0 is special case and it is safe to use it, 214 | %% because invalid waitings are removed before sleeping. 215 | %% thus, only waitings added in this tick are remained. 216 | case (W#waiting.timeout =:= 0 orelse wait_valid(W)) andalso 217 | lmq_lib:dequeue(S#state.name, Timeout) of 218 | false -> %% client timeout 219 | maybe_push_message(S#state{waiting=NewWaiting}); 220 | empty -> 221 | S; 222 | Msg -> 223 | erlang:demonitor(Ref, [flush]), 224 | Monitors = gb_sets:delete(Ref, S#state.monitors), 225 | case W#waiting.from of 226 | {_, _}=From -> gen_server:reply(From, Msg); 227 | P when is_pid(P) -> P ! {Ref, Msg} 228 | end, 229 | lmq_metrics:update_metric(S#state.name, pull), 230 | S#state{waiting=NewWaiting, monitors=Monitors} 231 | end; 232 | {empty, Waiting} -> 233 | S 234 | end. 235 | 236 | add_waiting(Pid, Timeout, S=#state{}) -> 237 | add_waiting(Pid, Pid, Timeout, S). 238 | 239 | add_waiting(From, MonitorPid, Timeout, S=#state{}) -> 240 | Ref = erlang:monitor(process, MonitorPid), 241 | Waiting = queue:in(#waiting{from=From, ref=Ref, timeout=Timeout}, 242 | S#state.waiting), 243 | Monitors = gb_sets:add(Ref, S#state.monitors), 244 | S#state{waiting=Waiting, monitors=Monitors}. 245 | 246 | remove_waiting(Ref, S=#state{monitors=M}) -> 247 | case gb_sets:is_member(Ref, M) of 248 | true -> 249 | erlang:demonitor(Ref, [flush]), 250 | Waiting = queue:filter( 251 | fun(#waiting{ref=V}) when V =:= Ref -> false; 252 | (_) -> true 253 | end, S#state.waiting), 254 | S#state{waiting=Waiting, monitors=gb_sets:delete(Ref, M)}; 255 | false -> 256 | S 257 | end. 258 | 259 | wait_valid(#waiting{timeout=infinity}) -> 260 | true; 261 | wait_valid(#waiting{start_time=StartTime, timeout=Timeout}) -> 262 | StartTime + Timeout > lmq_misc:unixtime(). 263 | 264 | prepare_sleep(S=#state{}) -> 265 | case queue:is_empty(S#state.waiting) of 266 | true -> {S, infinity}; 267 | false -> 268 | case lmq_lib:waittime(S#state.name) of 269 | 0 -> {S, 0}; 270 | T -> 271 | %% remove invalid waitings before sleeping 272 | Waitings = queue:filter(fun(W=#waiting{}) -> 273 | case wait_valid(W) of 274 | true -> true; 275 | false -> 276 | case W#waiting.from of 277 | {_, _}=From -> gen_server:reply(From, {error, timeout}); 278 | P when is_pid(P) -> P ! {W#waiting.ref, {error, timeout}} 279 | end, 280 | false 281 | end 282 | end, S#state.waiting), 283 | case queue:is_empty(Waitings) of 284 | true -> {S#state{waiting=Waitings}, infinity}; 285 | false -> {S#state{waiting=Waitings}, T} 286 | end 287 | end 288 | end. 289 | -------------------------------------------------------------------------------- /src/lmq_queue_mgr.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue_mgr). 2 | 3 | -behaviour(gen_server). 4 | -export([start_link/0, start_link/1, queue_started/2, delete/1, get/1, get/2, match/1, 5 | set_default_props/1, get_default_props/0]). 6 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 7 | code_change/3, terminate/2]). 8 | 9 | -include("lmq.hrl"). 10 | 11 | -record(state, {sup, qmap=dict:new(), stats_interval}). 12 | 13 | %% ================================================================== 14 | %% Public API 15 | %% ================================================================== 16 | 17 | start_link() -> 18 | start_link([]). 19 | 20 | start_link(Opts) -> 21 | gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []). 22 | 23 | queue_started(Name, QPid) when is_atom(Name) -> 24 | gen_server:cast(?MODULE, {queue_started, Name, QPid}). 25 | 26 | get(Name) when is_atom(Name) -> 27 | gen_server:call(?MODULE, {get, Name, []}). 28 | 29 | get(Name, Opts) when is_atom(Name) -> 30 | gen_server:call(?MODULE, {get, Name, Opts}). 31 | 32 | match(Regexp) when is_list(Regexp); is_binary(Regexp) -> 33 | gen_server:call(?MODULE, {match, Regexp}). 34 | 35 | delete(Name) when is_atom(Name) -> 36 | gen_server:call(?MODULE, {delete, Name}). 37 | 38 | set_default_props(PropsList) -> 39 | gen_server:call(?MODULE, {set_default_props, PropsList}). 40 | 41 | get_default_props() -> 42 | gen_server:call(?MODULE, {get_default_props}). 43 | 44 | %% ================================================================== 45 | %% gen_server callbacks 46 | %% ================================================================== 47 | 48 | init(Opts) -> 49 | lager:info("Starting the queue manager: ~p ~p", [self(), Opts]), 50 | lists:foreach(fun(Name) -> 51 | lmq_queue:start(Name) 52 | end, lmq_lib:all_queue_names()), 53 | 54 | StatsInterval = proplists:get_value(stats_interval, Opts), 55 | maybe_send_after(StatsInterval, emit_stats), 56 | {ok, #state{stats_interval=StatsInterval}}. 57 | 58 | handle_call({delete, Name}, _From, S=#state{}) when is_atom(Name) -> 59 | State = case dict:find(Name, S#state.qmap) of 60 | {ok, {Pid, _}} -> 61 | lmq_queue:stop(Pid), 62 | S#state{qmap=dict:erase(Name, S#state.qmap)}; 63 | error -> 64 | S 65 | end, 66 | ok = lmq_lib:delete(Name), 67 | {reply, ok, State}; 68 | 69 | handle_call({get, Name, Opts}, _From, S=#state{}) when is_atom(Name) -> 70 | case dict:find(Name, S#state.qmap) of 71 | {ok, {Pid, _}} -> 72 | case proplists:get_value(update, Opts) of 73 | true -> 74 | Props1 = case proplists:get_value(props, Opts, []) of 75 | [] -> []; 76 | Props -> lmq_misc:extend(Props, lmq_lib:queue_info(Name)) 77 | end, 78 | case lmq_queue:props(Pid, Props1) of 79 | ok -> {reply, Pid, S}; 80 | _ -> {reply, error, S} 81 | end; 82 | undefined -> 83 | {reply, Pid, S} 84 | end; 85 | error -> 86 | case proplists:get_value(create, Opts) of 87 | true -> 88 | {ok, Pid} = case proplists:get_value(props, Opts) of 89 | undefined -> lmq_queue:start(Name); 90 | Props -> lmq_queue:start(Name, lists:keysort(1, Props)) 91 | end, 92 | lager:info("The new queue created: ~s ~p", [Name, Pid]), 93 | {reply, Pid, update_qmap(Name, Pid, S)}; 94 | undefined -> 95 | {reply, not_found, S} 96 | end 97 | end; 98 | 99 | handle_call({match, Regexp}, _From, S=#state{}) -> 100 | R = case re:compile(Regexp) of 101 | {ok, MP} -> 102 | dict:fold(fun(Name, {Pid, _}, Acc) -> 103 | case re:run(atom_to_list(Name), MP) of 104 | {match, _} -> [{Name, Pid} | Acc]; 105 | _ -> Acc 106 | end 107 | end, [], S#state.qmap); 108 | {error, _} -> 109 | {error, invalid_regexp} 110 | end, 111 | {reply, R, S}; 112 | 113 | handle_call({set_default_props, PropsList}, _From, S=#state{}) -> 114 | case validate_props_list(PropsList) of 115 | {ok, _PropsList} -> 116 | lmq_lib:set_lmq_info(default_props, PropsList), 117 | dict:fold(fun(_, {Pid, _}, _) -> 118 | lmq_queue:reload_properties(Pid) 119 | end, ok, S#state.qmap), 120 | {reply, ok, S}; 121 | {error, Reason} -> 122 | {reply, Reason, S} 123 | end; 124 | 125 | handle_call({get_default_props}, _From, S=#state{}) -> 126 | PropsList = case lmq_lib:get_lmq_info(default_props) of 127 | {ok, Value} -> Value; 128 | _ -> [] 129 | end, 130 | {reply, PropsList, S}; 131 | 132 | handle_call(Msg, _From, State) -> 133 | lager:warning("Unknown message: ~p", [Msg]), 134 | {noreply, State}. 135 | 136 | handle_cast({queue_started, Name, Pid}, S) when is_atom(Name) -> 137 | {noreply, update_qmap(Name, Pid, S)}; 138 | 139 | handle_cast(_, State) -> 140 | {noreply, State}. 141 | 142 | handle_info({'DOWN', Ref, process, _Pid, _}, S=#state{qmap=QMap}) -> 143 | NewQMap = dict:filter(fun(_, {_, R}) -> 144 | R =/= Ref 145 | end, QMap), 146 | {noreply, S#state{qmap=NewQMap}}; 147 | handle_info(emit_stats, S=#state{}) -> 148 | maybe_send_after(S#state.stats_interval, emit_stats), 149 | WordSize = erlang:system_info(wordsize), 150 | Queues = dict:fetch_keys(S#state.qmap), 151 | lists:foreach(fun(Queue) -> 152 | Size = mnesia:table_info(Queue, size), 153 | Memory = mnesia:table_info(Queue, memory) * WordSize, 154 | lmq_metrics:update_metric(Queue, [{size, Size}, {memory, Memory}]) 155 | end, Queues), 156 | {noreply, S}; 157 | handle_info(Msg, State) -> 158 | lager:warning("Unknown message: ~p", [Msg]), 159 | {noreply, State}. 160 | 161 | terminate(_Reason, _State) -> 162 | ok. 163 | 164 | code_change(_OldVsn, State, _Extra) -> 165 | {ok, State}. 166 | 167 | %% ================================================================== 168 | %% Private functions 169 | %% ================================================================== 170 | 171 | update_qmap(Name, Pid, #state{qmap=QMap}=S) -> 172 | Ref = erlang:monitor(process, Pid), 173 | S#state{qmap=dict:store(Name, {Pid, Ref}, QMap)}. 174 | 175 | validate_props_list(PropsList) -> 176 | try 177 | {ok, validate_props_list(PropsList, [])} 178 | catch 179 | error:function_clause -> {error, invalid_syntax} 180 | end. 181 | 182 | validate_props_list([], Acc) -> 183 | lists:reverse(Acc); 184 | 185 | validate_props_list([{Regexp, Props}|T], Acc) when is_list(Regexp); is_binary(Regexp), is_list(Props) -> 186 | {ok, MP} = re:compile(Regexp), 187 | Props1 = lmq_misc:extend(Props, ?DEFAULT_QUEUE_PROPS), 188 | validate_props_list(T, [{MP, Props1} | Acc]). 189 | 190 | maybe_send_after(Time, Message) when is_integer(Time) -> 191 | erlang:send_after(Time, ?MODULE, Message); 192 | maybe_send_after(_, _) -> 193 | ignore. 194 | 195 | %% ================================================================== 196 | %% EUnit tests 197 | %% ================================================================== 198 | 199 | -ifdef(TEST). 200 | -include_lib("eunit/include/eunit.hrl"). 201 | 202 | validate_props_test() -> 203 | ?assertEqual( 204 | {ok, [{element(2, re:compile("lmq/a")), [{accum, 0}, {retry, 1}, {timeout, 30}]}, 205 | {element(2, re:compile("lmq/.*")), [{accum, 0}, {retry, 2}, {timeout, 60}]}]}, 206 | validate_props_list([{"lmq/a", [{retry, 1}]}, 207 | {"lmq/.*", [{timeout, 60}]}])), 208 | ?assertEqual( 209 | {ok, [{element(2, re:compile(<<"lmq/.*">>)), [{accum, 0}, {retry, 1}, {timeout, 30}]}]}, 210 | validate_props_list([{<<"lmq/.*">>, [{retry, 1}]}])), 211 | ?assertEqual( 212 | {error, invalid_syntax}, 213 | validate_props_list([{"lmq/a", {retry, 1}}])). 214 | 215 | -endif. 216 | -------------------------------------------------------------------------------- /src/lmq_queue_sup.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue_sup). 2 | 3 | -behaviour(supervisor). 4 | -export([start_link/0]). 5 | -export([init/1]). 6 | 7 | start_link() -> 8 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 9 | 10 | init([]) -> 11 | MaxRestart = 5, 12 | MaxTime = 3600, 13 | {ok, {{simple_one_for_one, MaxRestart, MaxTime}, 14 | [{lmq_queue, 15 | {lmq_queue, start_link, []}, 16 | transient, 17 | 5000, 18 | worker, 19 | [lmq_queue]}]}}. 20 | -------------------------------------------------------------------------------- /src/lmq_queue_supersup.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue_supersup). 2 | 3 | -behaviour(supervisor). 4 | -export([start_link/0]). 5 | -export([init/1]). 6 | 7 | start_link() -> 8 | supervisor:start_link(?MODULE, []). 9 | 10 | init([]) -> 11 | MgrArgs = case application:get_env(stats_interval) of 12 | {ok, T} when is_integer(T), T > 0 -> 13 | [[{stats_interval, T}]]; 14 | _ -> 15 | [] 16 | end, 17 | 18 | QueueSup = {lmq_queue_sup, 19 | {lmq_queue_sup, start_link, []}, 20 | permanent, infinity, supervisor, [lmq_queue_sup]}, 21 | 22 | QueueMgr = {lmq_queue_mgr, 23 | {lmq_queue_mgr, start_link, MgrArgs}, 24 | permanent, 5000, worker, [lmq_queue_mgr]}, 25 | 26 | Children = [QueueSup, QueueMgr], 27 | {ok, {{one_for_all, 5, 10}, Children}}. 28 | -------------------------------------------------------------------------------- /src/lmq_sup.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helper macro for declaring children of supervisor 12 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 13 | -define(CHILD(I, Type, Args), {I, {I, start_link, Args}, permanent, 5000, Type, [I]}). 14 | 15 | %% =================================================================== 16 | %% API functions 17 | %% =================================================================== 18 | 19 | start_link() -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 21 | 22 | %% =================================================================== 23 | %% Supervisor callbacks 24 | %% =================================================================== 25 | 26 | init([]) -> 27 | {ok, InfluxDB} = application:get_env(influxdb), 28 | Host = proplists:get_value(host, InfluxDB), 29 | Port = proplists:get_value(port, InfluxDB), 30 | 31 | {ok, {{one_for_all, 5, 10}, 32 | [?CHILD(lmq_event, worker), 33 | ?CHILD(lmq_hook, worker), 34 | ?CHILD(lmq_queue_supersup, supervisor), 35 | ?CHILD(lmq_mpull_sup, supervisor), 36 | ?CHILD(influxdb_client, worker, [Host, Port])]}}. 37 | -------------------------------------------------------------------------------- /test/lmq_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_SUITE). 2 | 3 | -include("lmq.hrl"). 4 | -include_lib("common_test/include/ct.hrl"). 5 | -export([init_per_suite/1, end_per_suite/1, 6 | init_per_testcase/2, end_per_testcase/2, all/0]). 7 | -export([push/1, pull/1, update_props/1, properties/1, status/1, stats/1]). 8 | 9 | all() -> 10 | [push, pull, update_props, properties, status, stats]. 11 | 12 | init_per_suite(Config) -> 13 | Priv = ?config(priv_dir, Config), 14 | application:start(mnesia), 15 | application:set_env(mnesia, dir, Priv), 16 | lmq:start(), 17 | Config. 18 | 19 | end_per_suite(_Config) -> 20 | lmq:stop(), 21 | mnesia:delete_schema([node()]). 22 | 23 | init_per_testcase(_, Config) -> 24 | [{qname, lmq_test} | Config]. 25 | 26 | end_per_testcase(_, Config) -> 27 | Name = ?config(qname, Config), 28 | lmq_queue_mgr:delete(Name). 29 | 30 | push(Config) -> 31 | Name = ?config(qname, Config), 32 | lmq:push(atom_to_binary(Name, latin1), <<"test 1">>), 33 | lmq:push(atom_to_binary(Name, latin1), 34 | [{<<"content-type">>, <<"text/plain">>}], <<"test 2">>), 35 | [{queue, Name}, {id, _}, {type, normal}, {retry, 2}, 36 | {content, {[], <<"test 1">>}}] = lmq:pull(Name, 0), 37 | [{queue, Name}, {id, _}, {type, normal}, {retry, 2}, 38 | {content, {[{<<"content-type">>, <<"text/plain">>}], <<"test 2">>}}] 39 | = lmq:pull(Name, 0). 40 | 41 | pull(Config) -> 42 | Name = ?config(qname, Config), 43 | Parent = self(), 44 | spawn_link(fun() -> Parent ! lmq:pull(Name) end), 45 | timer:sleep(100), 46 | lmq:push(Name, <<"test_data">>), 47 | receive 48 | [{queue, Name}, {id, _}, {type, normal}, {retry, 2}, {content, {[], <<"test_data">>}}] -> ok 49 | after 100 -> 50 | ct:fail(no_response) 51 | end. 52 | 53 | update_props(Config) -> 54 | Name = ?config(qname, Config), 55 | true = is_pid(lmq:update_props(Name, [{retry, 1}, {timeout, 0}])), 56 | ok = lmq:push(Name, 1), 57 | [{queue, Name}, {id, _}, {type, normal}, {retry, 1}, {content, {[], 1}}] = lmq:pull(Name), 58 | [{queue, Name}, {id, _}, {type, normal}, {retry, 0}, {content, {[], 1}}] = lmq:pull(Name), 59 | empty = lmq:pull(Name, 0), 60 | 61 | %% change retry count 62 | true = is_pid(lmq:update_props(Name, [{retry, 0}])), 63 | ok = lmq:push(Name, 2), 64 | [{queue, Name}, {id, _}, {type, normal}, {retry, 0}, {content, {[], 2}}] = lmq:pull(Name), 65 | empty = lmq:pull(Name, 0). 66 | 67 | properties(_Config) -> 68 | N1 = lmq_properies_test_q1, 69 | N2 = lmq_properies_test_q2, 70 | N3 = lmq_properies_test_q3, 71 | P1 = lmq_misc:extend([{retry, 0}], ?DEFAULT_QUEUE_PROPS), 72 | P2 = lmq_misc:extend([{retry, 1}, {timeout, 0}], ?DEFAULT_QUEUE_PROPS), 73 | P3 = lmq_misc:extend([{retry, 0}, {timeout, 0}], ?DEFAULT_QUEUE_PROPS), 74 | 75 | ok = lmq:push(N1, 1), 76 | Q1 = lmq_queue_mgr:get(N1), 77 | Q2 = lmq:update_props(N2), 78 | Q3 = lmq:update_props(N3, [{retry, 0}]), 79 | ?DEFAULT_QUEUE_PROPS = lmq_queue:get_properties(Q1), 80 | ?DEFAULT_QUEUE_PROPS = lmq_queue:get_properties(Q2), 81 | P1 = lmq_queue:get_properties(Q3), 82 | 83 | lmq:set_default_props([{".*", [{retry, 1}, {timeout, 0}]}]), 84 | timer:sleep(50), 85 | P2 = lmq_queue:get_properties(Q1), 86 | P2 = lmq_queue:get_properties(Q2), 87 | P3 = lmq_queue:get_properties(Q3), 88 | lmq:set_default_props([]). 89 | 90 | status(Config) -> 91 | Name = ?config(qname, Config), 92 | Node = node(), 93 | Q = lmq_queue_mgr:get(Name, [create]), 94 | lmq_queue:push(Q, 1), 95 | Status = lmq:status(), 96 | [Node] = proplists:get_value(all_nodes, Status), 97 | [Node] = proplists:get_value(active_nodes, Status), 98 | QStatus = proplists:get_value(Name, proplists:get_value(queues, Status)), 99 | 1 = proplists:get_value(size, QStatus), 100 | true = proplists:get_value(memory, QStatus) > 0, 101 | [Node] = proplists:get_value(nodes, QStatus), 102 | ?DEFAULT_QUEUE_PROPS = proplists:get_value(props, QStatus). 103 | 104 | stats(Config) -> 105 | Name = ?config(qname, Config), 106 | Q = lmq_queue_mgr:get(Name, [create]), 107 | lmq_queue:push(Q, 1), 108 | Stats = proplists:get_value(Name, lmq:stats()), 109 | [push, pull, retention] = proplists:get_keys(Stats). 110 | -------------------------------------------------------------------------------- /test/lmq_event_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_event_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include("lmq.hrl"). 5 | -include("lmq_test.hrl"). 6 | 7 | -export([init_per_suite/1, end_per_suite/1, 8 | init_per_testcase/2, end_per_testcase/2, 9 | all/0]). 10 | -export([emit_new_message/1, handle_new_message/1, handle_local_queue_created/1, 11 | handle_remote_queue_created/1]). 12 | 13 | all() -> 14 | [emit_new_message, handle_new_message, handle_local_queue_created, 15 | handle_remote_queue_created]. 16 | 17 | init_per_suite(Config) -> 18 | Priv = ?config(priv_dir, Config), 19 | application:start(mnesia), 20 | application:set_env(mnesia, dir, Priv), 21 | lmq:start(), 22 | Config. 23 | 24 | end_per_suite(_Config) -> 25 | lmq:stop(), 26 | mnesia:delete_schema([node()]). 27 | 28 | init_per_testcase(_, Config) -> 29 | lmq_event:add_handler(lmq_test_handler, self()), 30 | [{qname, lmq_event_test} | Config]. 31 | 32 | end_per_testcase(_, Config) -> 33 | Name = ?config(qname, Config), 34 | lmq_queue_mgr:delete(Name). 35 | 36 | emit_new_message(Config) -> 37 | Name = ?config(qname, Config), 38 | Q = lmq_queue_mgr:get(Name, [create]), 39 | lmq_queue:push(Q, 1), 40 | ?EVENT_OR_FAIL({local, {new_message, Name}}). 41 | 42 | handle_new_message(Config) -> 43 | Name = ?config(qname, Config), 44 | send_remote_event({new_message, Name}), 45 | timer:sleep(50), 46 | not_found = lmq_queue_mgr:get(Name), 47 | 48 | Q = lmq_queue_mgr:get(Name, [create]), 49 | Parent = self(), 50 | spawn(fun() -> Parent ! {Q, lmq_queue:pull(Q)} end), 51 | timer:sleep(50), 52 | lmq_lib:enqueue(Name, 1), 53 | send_remote_event({new_message, Name}), 54 | receive {Q, M} when M#message.content =:= 1 -> ok 55 | after 50 -> ct:fail(no_response) 56 | end. 57 | 58 | handle_local_queue_created(_Config) -> 59 | lmq_queue_mgr:get('lmq/mpull/a', [create]), 60 | lmq_queue_mgr:get('lmq/mpull/b', [create]), 61 | {ok, Pid} = lmq_mpull:start(), 62 | Parent = self(), Ref = make_ref(), 63 | spawn(fun() -> Parent ! {Ref, lmq_mpull:pull(Pid, <<"lmq/mpull/.*">>, 100)} end), 64 | timer:sleep(10), 65 | Q3 = lmq_queue_mgr:get('lmq/mpull/c', [create]), 66 | lmq_queue:push(Q3, <<"push after pull">>), 67 | 68 | receive {Ref, [{queue, 'lmq/mpull/c'}, _, _, _, 69 | {content, <<"push after pull">>}]} -> ok 70 | after 50 -> ct:fail(no_response) 71 | end. 72 | 73 | handle_remote_queue_created(_Config) -> 74 | %% start the queue if not exists 75 | not_found = lmq_queue_mgr:get('lmq/remote/a'), 76 | lmq_lib:create('lmq/remote/a', [{retry, 100}]), 77 | send_remote_event({queue_created, 'lmq/remote/a'}), 78 | timer:sleep(10), 79 | Q1 = lmq_queue_mgr:get('lmq/remote/a'), 80 | 100 = proplists:get_value(retry, lmq_queue:get_properties(Q1)), 81 | 82 | %% update the queue if exists 83 | Q2 = lmq_queue_mgr:get('lmq/remote/b', [create]), 84 | 2 = proplists:get_value(retry, lmq_queue:get_properties(Q2)), 85 | lmq_lib:create('lmq/remote/b', [{retry, 100}]), 86 | send_remote_event({queue_created, 'lmq/remote/b'}), 87 | timer:sleep(10), 88 | 100 = proplists:get_value(retry, lmq_queue:get_properties(Q2)), 89 | 90 | %% ensure mpull also works 91 | lmq_lib:create('lmq/remote/c'), 92 | lmq_lib:enqueue('lmq/remote/c', 1), 93 | {ok, Pid} = lmq_mpull:start(), 94 | Parent = self(), Ref = make_ref(), 95 | spawn(fun() -> Parent ! {Ref, lmq_mpull:pull(Pid, <<"lmq/remote/.*">>, 100)} end), 96 | not_found = lmq_queue_mgr:get('lmq/remote/c'), 97 | send_remote_event({queue_created, 'lmq/remote/c'}), 98 | receive {Ref, [{queue, 'lmq/remote/c'}, _, _, _, {content, 1}]} -> ok 99 | after 50 -> ct:fail(no_response) 100 | end. 101 | 102 | send_remote_event(Event) -> 103 | gen_event:notify(?LMQ_EVENT, {remote, Event}). 104 | -------------------------------------------------------------------------------- /test/lmq_hook_crash.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_crash). 2 | 3 | -export([init/0, hooks/0, activate/1, deactivate/1]). 4 | -export([hook1/2]). 5 | 6 | init() -> 7 | ok. 8 | 9 | hooks() -> 10 | [hook1, hook2]. 11 | 12 | activate([N]) -> 13 | {ok, N}. 14 | 15 | deactivate(N) -> 16 | N / 0. 17 | 18 | hook1(N, State) -> 19 | State / N. 20 | -------------------------------------------------------------------------------- /test/lmq_hook_invalid.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_invalid). 2 | 3 | -export([init/1, hooks/0, activate/1, deactivate/1]). 4 | -export([custom_hook/2]). 5 | 6 | init([]) -> 7 | ok. 8 | 9 | hooks() -> 10 | [custom_hook]. 11 | 12 | activate([N]) -> 13 | {ok, N}. 14 | 15 | deactivate(N) -> 16 | true = is_integer(N), 17 | ok. 18 | 19 | custom_hook(N, State) -> 20 | N + State. 21 | -------------------------------------------------------------------------------- /test/lmq_hook_sample1.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_sample1). 2 | -behaviour(lmq_hook). 3 | 4 | -export([init/0, hooks/0, activate/1, deactivate/1]). 5 | -export([custom_hook/2]). 6 | 7 | init() -> 8 | ok. 9 | 10 | hooks() -> 11 | [custom_hook]. 12 | 13 | activate([N]) -> 14 | {ok, N}. 15 | 16 | deactivate(N) -> 17 | true = is_integer(N), 18 | ok. 19 | 20 | custom_hook(N, State) -> 21 | N + State. 22 | -------------------------------------------------------------------------------- /test/lmq_hook_sample2.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_sample2). 2 | -behaviour(lmq_hook). 3 | 4 | -export([init/0, hooks/0, activate/1, deactivate/1]). 5 | -export([custom_hook/2, custom_hook2/2]). 6 | 7 | init() -> 8 | ok. 9 | 10 | hooks() -> 11 | [custom_hook, custom_hook2]. 12 | 13 | activate([N, M]) -> 14 | {ok, N * M}. 15 | 16 | deactivate(N) -> 17 | true = is_integer(N), 18 | ok. 19 | 20 | custom_hook(N, State) -> 21 | N * State. 22 | 23 | custom_hook2(N, State) -> 24 | custom_hook(N, State). 25 | -------------------------------------------------------------------------------- /test/lmq_hook_test.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_hook_test). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | -define(setup(F), {setup, fun start/0, fun stop/1, F}). 5 | 6 | %% ================================================================== 7 | %% test descriptions 8 | %% ================================================================== 9 | start_stop_test_() -> 10 | {"The hook manager can be started, stopped and has a registered name", 11 | ?setup(fun is_registered/1)}. 12 | 13 | register_test_() -> 14 | [{"A hook can be registered and be called", 15 | ?setup(fun register_hook/1)}, 16 | {"Multiple hooks can be registered and be called sequentially", 17 | ?setup(fun register_multiple_hooks/1)}, 18 | {"Hooks can be updated", 19 | ?setup(fun update_hooks/1)}, 20 | {"Hooks cannot be affected other queues", 21 | ?setup(fun other_queue/1)}, 22 | {"Hooks can be configured separately per queue", 23 | ?setup(fun configured_per_queue/1)}, 24 | {"Registering never crashes and simply aborts", 25 | ?setup(fun abort_registering/1)}, 26 | {"Unregistering never crashes", 27 | ?setup(fun unregistering_never_crash/1)}, 28 | {"Hooks not included in new config are unregistered", 29 | ?setup(fun automatic_unregister/1)} 30 | ]. 31 | 32 | call_test_() -> 33 | [{"Calling never crashes", 34 | ?setup(fun call_never_crash/1)}]. 35 | 36 | %% ================================================================== 37 | %% setup functions 38 | %% ================================================================== 39 | start() -> 40 | {ok, Pid} = lmq_hook:start_link(), 41 | Pid. 42 | 43 | stop(_) -> 44 | lmq_hook:stop(). 45 | 46 | %% ================================================================== 47 | %% actual tests 48 | %% ================================================================== 49 | is_registered(Pid) -> 50 | [?_assert(erlang:is_process_alive(Pid)), 51 | ?_assertEqual(Pid, whereis(lmq_hook))]. 52 | 53 | register_hook(_) -> 54 | Config = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 55 | Res = lmq_hook:register(hook_test, Config), 56 | [?_assertEqual(ok, Res), 57 | ?_assertEqual(3, lmq_hook:call(hook_test, custom_hook, 2))]. 58 | 59 | register_multiple_hooks(_) -> 60 | Config = [{custom_hook, [{lmq_hook_sample1, [1]}, {lmq_hook_sample2, [1, 2]}]}], 61 | Res = lmq_hook:register(hook_test, Config), 62 | [?_assertEqual(ok, Res), 63 | ?_assertEqual(6, lmq_hook:call(hook_test, custom_hook, 2))]. 64 | 65 | update_hooks(_) -> 66 | Config1 = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 67 | Config2 = [{custom_hook, [{lmq_hook_sample1, [1]}, {lmq_hook_sample2, [1, 2]}]}], 68 | Config3 = [{custom_hook, [{lmq_hook_sample2, [1, 2]}, {lmq_hook_sample1, [1]}]}], 69 | Config4 = [{custom_hook, [{lmq_hook_sample2, [1, 2]}]}], 70 | Res = [lmq_hook:register(hook_test, Config1), 71 | lmq_hook:call(hook_test, custom_hook, 2), 72 | lmq_hook:register(hook_test, Config2), 73 | lmq_hook:call(hook_test, custom_hook, 2), 74 | lmq_hook:register(hook_test, Config3), 75 | lmq_hook:call(hook_test, custom_hook, 2), 76 | lmq_hook:register(hook_test, Config4), 77 | lmq_hook:call(hook_test, custom_hook, 2)], 78 | [?_assertEqual([ok, 3, ok, 6, ok, 5, ok, 4], Res)]. 79 | 80 | other_queue(_) -> 81 | Config = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 82 | ok = lmq_hook:register(hook_test, Config), 83 | [?_assertEqual(2, lmq_hook:call(not_hooked, custom_hook, 2))]. 84 | 85 | configured_per_queue(_) -> 86 | Config1 = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 87 | Config2 = [{custom_hook, [{lmq_hook_sample1, [2]}]}], 88 | Config3 = [{custom_hook, [{lmq_hook_sample2, [2, 3]}]}], 89 | Res = [lmq_hook:register(hook_test1, Config1), 90 | lmq_hook:register(hook_test2, Config2), 91 | lmq_hook:register(hook_test3, Config3)], 92 | [?_assertEqual([ok, ok, ok], Res), 93 | ?_assertEqual(2, lmq_hook:call(hook_test1, custom_hook, 1)), 94 | ?_assertEqual(3, lmq_hook:call(hook_test2, custom_hook, 1)), 95 | ?_assertEqual(6, lmq_hook:call(hook_test3, custom_hook, 1))]. 96 | 97 | abort_registering(_) -> 98 | Config1 = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 99 | Config2 = [{custom_hook, [{lmq_hook_sample2, [1]}]}], 100 | Config3 = [{custom_hook, [{lmq_hook_no_exist, [1]}]}], 101 | Config4 = [{custom_hook, [{lmq_hook_invalid, [1]}]}], 102 | Config5 = [{invalid_hook, [{lmq_hook_sample1, [1]}]}], 103 | Res = [lmq_hook:register(hook_test, Config1), 104 | lmq_hook:call(hook_test, custom_hook, 2), 105 | lmq_hook:register(hook_test, Config2), 106 | lmq_hook:call(hook_test, custom_hook, 2), 107 | lmq_hook:register(hook_test, Config3), 108 | lmq_hook:call(hook_test, custom_hook, 2), 109 | lmq_hook:register(hook_test, Config4), 110 | lmq_hook:call(hook_test, custom_hook, 2), 111 | lmq_hook:register(hook_test, Config5), 112 | lmq_hook:call(hook_test, custom_hook, 2), 113 | lmq_hook:call(hook_test, invalid_hook, 2)], 114 | [?_assertEqual([ok, 3, 115 | {error, bad_config}, 3, 116 | {error, bad_hook}, 3, 117 | {error, bad_hook}, 3, 118 | {error, bad_config}, 3, 2], Res)]. 119 | 120 | unregistering_never_crash(_) -> 121 | Config1 = [{hook1, [{lmq_hook_crash, [2]}]}], 122 | Config2 = [{hook1, []}], 123 | Res = [lmq_hook:register(hook_test, Config1), 124 | lmq_hook:register(hook_test, Config2)], 125 | [?_assertEqual([ok, ok], Res)]. 126 | 127 | automatic_unregister(_) -> 128 | Config1 = [{custom_hook, [{lmq_hook_sample1, [1]}]}], 129 | Config2 = [{custom_hook2, [{lmq_hook_sample2, [1, 2]}]}], 130 | Res = [lmq_hook:register(hook_test, Config1), 131 | lmq_hook:call(hook_test, custom_hook, 2), 132 | lmq_hook:call(hook_test, custom_hook2, 2), 133 | lmq_hook:register(hook_test, Config2), 134 | lmq_hook:call(hook_test, custom_hook, 2), 135 | lmq_hook:call(hook_test, custom_hook2, 2)], 136 | [?_assertEqual([ok, 3, 2, ok, 2, 4], Res)]. 137 | 138 | call_never_crash(_) -> 139 | Config = [{hook1, [{lmq_hook_crash, [2]}]}], 140 | ok = lmq_hook:register(hook_test, Config), 141 | [?_assertEqual(1.0, lmq_hook:call(hook_test, hook1, 2)), 142 | ?_assertEqual(0, lmq_hook:call(hook_test, hook1, 0)), 143 | ?_assertEqual(1, lmq_hook:call(hook_test, hook2, 1))]. 144 | -------------------------------------------------------------------------------- /test/lmq_lib_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_lib_SUITE). 2 | 3 | -include("lmq.hrl"). 4 | -include("lmq_test.hrl"). 5 | -include_lib("common_test/include/ct.hrl"). 6 | -export([init_per_suite/1, end_per_suite/1, init_per_testcase/2, end_per_testcase/2, 7 | all/0]). 8 | -export([lmq_info/1, create_delete/1, queue_names/1, done/1, release/1, retain/1, waittime/1, 9 | limit_retry/1, error_case/1, accumlate/1, property/1, many_waste_messages/1]). 10 | -export([enqueue_many/2]). 11 | 12 | all() -> 13 | [lmq_info, create_delete, queue_names, done, release, retain, waittime, limit_retry, 14 | error_case, accumlate, property, many_waste_messages]. 15 | 16 | init_per_suite(Config) -> 17 | Priv = ?config(priv_dir, Config), 18 | application:start(mnesia), 19 | application:set_env(mnesia, dir, Priv), 20 | application:start(folsom), 21 | ok = lmq_lib:init_mnesia(), 22 | Config. 23 | 24 | end_per_suite(_Config) -> 25 | application:stop(mnesia), 26 | mnesia:delete_schema([node()]), 27 | ok. 28 | 29 | init_per_testcase(create_delete, Config) -> 30 | lmq_event:start_link(), 31 | lmq_event:add_handler(lmq_test_handler, self()), 32 | Config; 33 | 34 | init_per_testcase(_, Config) -> 35 | lmq_event:start_link(), 36 | lmq_event:add_handler(lmq_test_handler, self()), 37 | Name = test, 38 | lmq_lib:create(Name), 39 | [{qname, Name} | Config]. 40 | 41 | end_per_testcase(_, Config) -> 42 | ok = lmq_lib:delete(?config(qname, Config)). 43 | 44 | lmq_info(_Config) -> 45 | ok = lmq_lib:set_lmq_info(name, lmq), 46 | {ok, lmq} = lmq_lib:get_lmq_info(name), 47 | {error, not_found} = lmq_lib:get_lmq_info(non_exists), 48 | {ok, []} = lmq_lib:get_lmq_info(non_exists, []). 49 | 50 | create_delete(_Config) -> 51 | %% create new queue 52 | ok = lmq_lib:create(test), 53 | message = mnesia:table_info(test, record_name), 54 | [] = lmq_lib:queue_info(test), 55 | ?EVENT_OR_FAIL({local, {queue_created, test}}), 56 | 57 | %% update properties 58 | ok = lmq_lib:create(test, [{timeout, 10}]), 59 | [{timeout, 10}] = lmq_lib:queue_info(test), 60 | ?EVENT_OR_FAIL({local, {queue_created, test}}), 61 | 62 | %% no event occurred when properties are not changed 63 | ok = lmq_lib:create(test, [{timeout, 10}]), 64 | [{timeout, 10}] = lmq_lib:queue_info(test), 65 | ?EVENT_AND_FAIL({local, {queue_created, test}}), 66 | 67 | %% delete 68 | ok = lmq_lib:delete(test), 69 | {aborted, {no_exists, _}} = mnesia:delete_table(test), 70 | not_found = lmq_lib:queue_info(test), 71 | ok = lmq_lib:delete(test). 72 | 73 | queue_names(Config) -> 74 | Name = fooooo, 75 | lmq_lib:create(Name), 76 | true = lists:sort([?config(qname, Config), Name]) =:= 77 | lists:sort(lmq_lib:all_queue_names()), 78 | lmq_lib:delete(Name). 79 | 80 | done(Config) -> 81 | Name = ?config(qname, Config), 82 | ok = lmq_lib:enqueue(Name, make_ref()), 83 | Now = lmq_misc:unixtime(), 84 | M = lmq_lib:dequeue(Name, 10), 85 | {TS, UUID} = M#message.id, 86 | true = Now < TS andalso TS - Now - 10 < 1, 87 | ok = lmq_lib:done(Name, UUID), 88 | not_found = lmq_lib:done(Name, UUID). 89 | 90 | release(Config) -> 91 | Name = ?config(qname, Config), 92 | ok = lmq_lib:enqueue(Name, make_ref(), [{retry, 2}]), 93 | M = lmq_lib:dequeue(Name, 30), 94 | 2 = M#message.retry, 95 | {_, UUID} = M#message.id, 96 | ok = lmq_lib:release(Name, UUID), 97 | not_found = lmq_lib:release(Name, UUID), 98 | 99 | M1 = lmq_lib:dequeue(Name, 30), 100 | 1 = M1#message.retry, 101 | ok = lmq_lib:put_back(Name, element(2, M1#message.id)), 102 | 1 = (lmq_lib:dequeue(Name, 30))#message.retry, 103 | 104 | ok = lmq_lib:enqueue(Name, make_ref()), 105 | M2 = lmq_lib:dequeue(Name, 30), 106 | infinity = M2#message.retry, 107 | ok = lmq_lib:release(Name, element(2, M2#message.id)), 108 | infinity = (lmq_lib:dequeue(Name, 30))#message.retry. 109 | 110 | retain(Config) -> 111 | Name = ?config(qname, Config), 112 | ok = lmq_lib:enqueue(Name, make_ref()), 113 | Now = lmq_misc:unixtime(), 114 | M = lmq_lib:dequeue(Name, 15), 115 | {TS, UUID} = M#message.id, 116 | true = Now < TS andalso TS - Now - 15 < 1, 117 | ok = lmq_lib:retain(Name, UUID, 30), 118 | not_found = lmq_lib:retain(Name, "AAA", 30). 119 | 120 | waittime(Config) -> 121 | Name = ?config(qname, Config), 122 | empty = lmq_lib:first(Name), 123 | infinity = lmq_lib:waittime(Name), 124 | ok = lmq_lib:enqueue(Name, make_ref()), 125 | 0 = lmq_lib:waittime(Name), 126 | lmq_lib:dequeue(Name, 30), 127 | true = 29500 < lmq_lib:waittime(Name). 128 | 129 | limit_retry(Config) -> 130 | Name = ?config(qname, Config), 131 | Timeout = 0, 132 | Ref = make_ref(), 133 | %% retry 5 times, that means dequeue succeed 6 times 134 | ok = lmq_lib:enqueue(Name, Ref, [{retry, 5}]), 135 | lists:all(fun(M) -> Ref =:= M#message.content end, 136 | [lmq_lib:dequeue(Name, Timeout) || _ <- lists:seq(1, 6)]), 137 | empty = lmq_lib:dequeue(Name, Timeout), 138 | %% retry infinity 139 | ok = lmq_lib:enqueue(Name, Ref), 140 | lists:all(fun(M) -> Ref =:= M#message.content end, 141 | [lmq_lib:dequeue(Name, Timeout) || _ <- lists:seq(1, 10)]). 142 | 143 | error_case(_Config) -> 144 | Name = '__abcdefg__', 145 | {error, no_queue_exists} = lmq_lib:enqueue(Name, make_ref()), 146 | {error, no_queue_exists} = lmq_lib:dequeue(Name, 30), 147 | {error, no_queue_exists} = lmq_lib:done(Name, "AAA"), 148 | {error, no_queue_exists} = lmq_lib:release(Name, "AAA"), 149 | {error, no_queue_exists} = lmq_lib:retain(Name, "AAA", 30), 150 | {error, no_queue_exists} = lmq_lib:waittime(Name). 151 | 152 | accumlate(Config) -> 153 | Name = ?config(qname, Config), 154 | Timeout = 30, 155 | R1 = make_ref(), R2 = make_ref(), R3 = make_ref(), 156 | %% 0 means not accumulate 157 | ok = lmq_lib:enqueue(Name, R1, [{accum, 0}]), 158 | R1 = (lmq_lib:dequeue(Name, Timeout))#message.content, 159 | 160 | %% get 2 compound messages, each compound message contains 1 message 161 | {accum, new} = lmq_lib:enqueue(Name, R1, [{accum, 1}]), timer:sleep(1), 162 | {accum, new} = lmq_lib:enqueue(Name, R2, [{accum, 1}]), timer:sleep(1), 163 | [R1] = (lmq_lib:dequeue(Name, Timeout))#message.content, 164 | [R2] = (lmq_lib:dequeue(Name, Timeout))#message.content, 165 | 166 | %% accumulate and no accumulate message 167 | {accum, new} = lmq_lib:enqueue(Name, R1, [{accum, 100}]), 168 | {accum, yes} = lmq_lib:enqueue(Name, R2, [{accum, 100}]), 169 | ok = lmq_lib:enqueue(Name, R3), 170 | R3 = (lmq_lib:dequeue(Name, Timeout))#message.content, 171 | empty = lmq_lib:dequeue(Name, Timeout), 172 | timer:sleep(100), 173 | [R1, R2] = (lmq_lib:dequeue(Name, Timeout))#message.content. 174 | 175 | property(_Config) -> 176 | N1 = property_default, 177 | N2 = property_override1, 178 | N3 = property_override2, 179 | P1 = ?DEFAULT_QUEUE_PROPS, 180 | P2 = lmq_misc:extend([{retry, infinity}, {timeout, 0}], ?DEFAULT_QUEUE_PROPS), 181 | P3 = lmq_misc:extend([{retry, 0}, {timeout, 0}], ?DEFAULT_QUEUE_PROPS), 182 | 183 | lmq_lib:create(N1), 184 | lmq_lib:create(N2), 185 | lmq_lib:create(N3, [{retry, 0}]), 186 | lmq_lib:set_lmq_info(default_props, 187 | [{"override", [{retry, infinity}, {timeout, 0}]}]), 188 | 189 | P1 = lmq_lib:get_properties(N1), 190 | P2 = lmq_lib:get_properties(N2), 191 | P3 = lmq_lib:get_properties(N3), 192 | P3 = lmq_lib:get_properties(N2, [{retry, 0}]), 193 | 194 | P4 = lmq_misc:extend([{accum, 1}], ?DEFAULT_QUEUE_PROPS), 195 | P5 = lmq_misc:extend([{accum, 1}, {retry, 0}], ?DEFAULT_QUEUE_PROPS), 196 | lmq_lib:set_lmq_info(default_props, [{"override", [{accum, 1}]}]), 197 | P1 = lmq_lib:get_properties(N1), 198 | P4 = lmq_lib:get_properties(N2), 199 | P5 = lmq_lib:get_properties(N3). 200 | 201 | many_waste_messages(Config) -> 202 | Name = ?config(qname, Config), 203 | rpc:pmap({?MODULE, enqueue_many}, [Name], lists:seq(1, 10)), 204 | ct:timetrap(10000), 205 | empty = lmq_lib:dequeue(Name, 0). 206 | 207 | enqueue_many(I, Name) -> 208 | [lmq_lib:enqueue(Name, I*N, [{retry, -1}]) || N <- lists:seq(1, 1000)]. 209 | -------------------------------------------------------------------------------- /test/lmq_mpull_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_mpull_SUITE). 2 | 3 | -include("lmq.hrl"). 4 | -include_lib("common_test/include/ct.hrl"). 5 | -export([init_per_suite/1, end_per_suite/1, 6 | init_per_testcase/2, end_per_testcase/2, all/0]). 7 | -export([pull/1, pull_async/1, client_closed/1]). 8 | 9 | all() -> 10 | [pull, pull_async, client_closed]. 11 | 12 | init_per_suite(Config) -> 13 | Priv = ?config(priv_dir, Config), 14 | application:start(mnesia), 15 | application:set_env(mnesia, dir, Priv), 16 | lmq:start(), 17 | Config. 18 | 19 | end_per_suite(_Config) -> 20 | lmq:stop(), 21 | mnesia:delete_schema([node()]). 22 | 23 | init_per_testcase(_, Config) -> 24 | Config. 25 | 26 | end_per_testcase(_, _Config) -> 27 | Queues = ['mpull/a', 'mpull/b'], 28 | [lmq:delete(Q) || Q <- Queues], 29 | ok. 30 | 31 | pull(_Config) -> 32 | Queues = ['mpull/a', 'mpull/b'], 33 | [lmq:update_props(Q, [{retry, 0}]) || Q <- Queues], 34 | [lmq:push(Q, Q) || Q <- Queues], 35 | 36 | Pids = [Pid || {ok, Pid} <- [lmq_mpull:start() || _ <- lists:seq(1, 4)]], 37 | [{queue, R1}, _, _, _, _] = lmq_mpull:pull(lists:nth(1, Pids), <<"mpull/.*">>), 38 | [{queue, R2}, _, _, _, _] = lmq_mpull:pull(lists:nth(2, Pids), <<"mpull/.*">>, 0), 39 | true = lists:sort([R1, R2]) =:= Queues, 40 | 41 | empty = lmq_mpull:pull(lists:nth(3, Pids), <<"mpull/.*">>, 0), 42 | empty = lmq_mpull:pull(lists:nth(4, Pids), <<"mpull/.*">>, 200). 43 | 44 | pull_async(_Config) -> 45 | lmq:push('mpull/a', 1), 46 | {ok, MP1} = lmq_mpull:start(), 47 | {ok, Ref1} = lmq_mpull:pull_async(MP1, <<"mpull/a">>), 48 | receive {Ref1, [{queue, 'mpull/a'}, _, _, _, {content, {[], 1}}]} -> ok 49 | after 50 -> ct:fail(no_response) 50 | end, 51 | 52 | {ok, MP2} = lmq_mpull:start(), 53 | {ok, _} = lmq_mpull:pull_async(MP2, <<"mpull/a">>), 54 | ok = lmq_mpull:pull_cancel(MP2), 55 | 56 | lmq:push('mpull/a', 3), 57 | {ok, MP3} = lmq_mpull:start(), 58 | {ok, Ref3} = lmq_mpull:pull_async(MP3, <<"mpull/a">>), 59 | receive {Ref3, [{queue, 'mpull/a'}, _, _, _, {content, {[], 3}}]} -> ok 60 | after 50 -> ct:fail(no_response) 61 | end. 62 | 63 | client_closed(_Config) -> 64 | lmq:push('mpull/a', 1), 65 | lmq:pull('mpull/a', 0), 66 | Pid = spawn(fun() -> 67 | {ok, MP1} = lmq_mpull:start(), 68 | lmq_mpull:pull(MP1, <<"mpull/.*">>, 1000) 69 | end), 70 | timer:sleep(10), 71 | exit(Pid, kill), 72 | timer:sleep(10), 73 | 74 | lmq:push('mpull/a', 2), 75 | {ok, MP2} = lmq_mpull:start(), 76 | [{queue, 'mpull/a'}, _, _, _, {content, {[], 2}}] = lmq_mpull:pull(MP2, <<"mpull/.*">>, 1000). 77 | -------------------------------------------------------------------------------- /test/lmq_msgpack_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_msgpack_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | -export([init_per_suite/1, end_per_suite/1, 5 | init_per_testcase/2, end_per_testcase/2, all/0]). 6 | -export([push_pull_done/1, release/1, props_and_timeout/1, packed_queue/1, 7 | push_all/1, pull_any/1, default_props/1]). 8 | 9 | -define(RPC(Client, Method, Args), msgpack_rpc_client:call(Client, Method, Args)). 10 | 11 | all() -> 12 | [push_pull_done, release, props_and_timeout, packed_queue, push_all, pull_any, 13 | default_props]. 14 | 15 | init_per_suite(Config) -> 16 | Priv = ?config(priv_dir, Config), 17 | application:start(mnesia), 18 | application:set_env(mnesia, dir, Priv), 19 | lmq:start(), 20 | Config. 21 | 22 | end_per_suite(_Config) -> 23 | lmq:stop(), 24 | mnesia:delete_schema([node()]). 25 | 26 | init_per_testcase(_, Config) -> 27 | {ok, Pid} = msgpack_rpc_client:connect(tcp, "localhost", 18800, []), 28 | [{client, Pid}, {qname, <<"msgpack_test">>} | Config]. 29 | 30 | end_per_testcase(_, Config) -> 31 | Client = ?config(client, Config), 32 | Name = ?config(qname, Config), 33 | msgpack_rpc_client:close(Client), 34 | lmq_queue_mgr:delete(binary_to_atom(Name, latin1)). 35 | 36 | push_pull_done(Config) -> 37 | Client = ?config(client, Config), 38 | Name = ?config(qname, Config), 39 | Content = <<"test data">>, 40 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, push, [Name, Content]), 41 | {ok, Res} = msgpack_rpc_client:call(Client, pull, [Name]), 42 | {[{<<"queue">>, Name}, {<<"id">>, UUID}, {<<"type">>, <<"normal">>}, {<<"retry">>, 2}, {<<"content">>, Content}]} = Res, 43 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, retain, [Name, UUID]), 44 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, done, [Name, UUID]). 45 | 46 | release(Config) -> 47 | Client = ?config(client, Config), 48 | Name = ?config(qname, Config), 49 | Content = <<"test data 2">>, 50 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, push, [Name, Content]), 51 | {ok, Res} = msgpack_rpc_client:call(Client, pull, [Name]), 52 | {[{<<"queue">>, Name}, {<<"id">>, UUID}, {<<"type">>, <<"normal">>}, {<<"retry">>, 2}, {<<"content">>, Content}]} = Res, 53 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, release, [Name, UUID]), 54 | {error, _} = msgpack_rpc_client:call(Client, done, [Name, UUID]), 55 | {ok, Res1} = msgpack_rpc_client:call(Client, pull, [Name]), 56 | {[{<<"queue">>, Name}, {<<"id">>, UUID1}, {<<"type">>, <<"normal">>}, {<<"retry">>, 1}, {<<"content">>, Content}]} = Res1, 57 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, done, [Name, UUID1]). 58 | 59 | props_and_timeout(Config) -> 60 | Client = ?config(client, Config), 61 | Name = ?config(qname, Config), 62 | Props = {[{<<"retry">>, 1}, {<<"timeout">>, 0}]}, 63 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, update_props, [Name, Props]), 64 | {error, _} = msgpack_rpc_client:call(Client, update_props, [Name, {[{<<"pack">>, <<"10">>}]}]), 65 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, push, [Name, <<"test">>]), 66 | {ok, Res} = msgpack_rpc_client:call(Client, pull, [Name, 0.2]), 67 | {[{<<"queue">>, Name}, {<<"id">>, _UUID}, {<<"type">>, <<"normal">>}, {<<"retry">>, 1}, {<<"content">>, <<"test">>}]} = Res, 68 | {ok, {[_, _, _, {<<"retry">>, 0}, {<<"content">>, <<"test">>}]}} = ?RPC(Client, pull, [Name, 0]), 69 | {ok, <<"empty">>} = ?RPC(Client, pull, [Name, 0.1]), 70 | 71 | {ok, <<"ok">>} = ?RPC(Client, update_props, [Name]), 72 | timer:sleep(10), 73 | {ok, <<"ok">>} = ?RPC(Client, push, [Name, <<"test2">>]), 74 | {ok, {[_, _, _, {<<"retry">>, 2}, _]}} = ?RPC(Client, pull, [Name, 0]), 75 | {ok, <<"empty">>} = ?RPC(Client, pull, [Name, 0]). 76 | 77 | packed_queue(Config) -> 78 | Client = ?config(client, Config), 79 | Name = ?config(qname, Config), 80 | Props = {[{<<"timeout">>, 0.4}, {<<"pack">>, 0.2}]}, 81 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, update_props, [Name, Props]), 82 | {ok, <<"packing started">>} = msgpack_rpc_client:call(Client, push, [Name, 1]), 83 | {ok, <<"packed">>} = msgpack_rpc_client:call(Client, push, [Name, 2]), 84 | {ok, <<"empty">>} = msgpack_rpc_client:call(Client, pull, [Name, 0]), 85 | timer:sleep(200), 86 | {ok, {[{<<"queue">>, Name}, {<<"id">>, _}, {<<"type">>, <<"package">>}, {<<"retry">>, 2}, {<<"content">>, [1, 2]}]}} = 87 | msgpack_rpc_client:call(Client, pull, [Name, 0]), 88 | {ok, <<"packing started">>} = msgpack_rpc_client:call(Client, push, [Name, 3]), 89 | timer:sleep(400), 90 | {ok, {[{<<"queue">>, Name}, {<<"id">>, _}, {<<"type">>, <<"package">>}, {<<"retry">>, 2}, {<<"content">>, [3]}]}} = 91 | msgpack_rpc_client:call(Client, pull, [Name, 0]), 92 | {ok, {[{<<"queue">>, Name}, {<<"id">>, _}, {<<"type">>, <<"package">>}, {<<"retry">>, 1}, {<<"content">>, [1, 2]}]}} = 93 | msgpack_rpc_client:call(Client, pull, [Name, 0]). 94 | 95 | push_all(Config) -> 96 | Client = ?config(client, Config), 97 | Names = [<<"lmq/foo">>, <<"lmq/bar">>], 98 | [msgpack_rpc_client:call(Client, update_props, [Name]) || Name <- Names], 99 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, push_all, [<<"lmq/.*">>, <<"data">>]), 100 | {ok, {[{<<"queue">>, <<"lmq/foo">>}, {<<"id">>, _}, {<<"type">>, <<"normal">>}, {<<"retry">>, 2}, {<<"content">>, <<"data">>}]}} = 101 | msgpack_rpc_client:call(Client, pull, [<<"lmq/foo">>, 0]), 102 | {ok, {[{<<"queue">>, <<"lmq/bar">>}, {<<"id">>, _}, {<<"type">>, <<"normal">>}, {<<"retry">>, 2}, {<<"content">>, <<"data">>}]}} = 103 | msgpack_rpc_client:call(Client, pull, [<<"lmq/bar">>, 0]), 104 | [msgpack_rpc_client:call(Client, delete, [N]) || N <- Names]. 105 | 106 | pull_any(Config) -> 107 | Client = ?config(client, Config), 108 | Names = [<<"lmq/foo">>, <<"lmq/bar">>], 109 | Props = {[{<<"retry">>, 0}]}, 110 | [msgpack_rpc_client:call(Client, update_props, [Name, Props]) || Name <- Names], 111 | [msgpack_rpc_client:call(Client, push, [Name, Name]) || Name <- Names], 112 | {ok, {[{<<"queue">>, R1}, {<<"id">>, _}, {<<"type">>, <<"normal">>}, {<<"retry">>, 0}, {<<"content">>, R1}]}} = 113 | msgpack_rpc_client:call(Client, pull_any, [<<"lmq/.*">>, 0]), 114 | {ok, {[{<<"queue">>, R2}, {<<"id">>, _}, {<<"type">>, <<"normal">>}, {<<"retry">>, 0}, {<<"content">>, R2}]}} = 115 | msgpack_rpc_client:call(Client, pull_any, [<<"lmq/.*">>, 0]), 116 | true = lists:sort([R1, R2]) =:= lists:sort(Names), 117 | {ok, <<"empty">>} = 118 | msgpack_rpc_client:call(Client, pull_any, [<<"lmq/.*">>, 0]), 119 | {ok, <<"empty">>} = 120 | msgpack_rpc_client:call(Client, pull_any, [<<"lmq/.*">>, 0.2]), 121 | [msgpack_rpc_client:call(Client, delete, [N]) || N <- Names]. 122 | 123 | default_props(Config) -> 124 | Client = ?config(client, Config), 125 | DefaultProps = [[<<"def/">>, {[{<<"retry">>, 0}, {<<"timeout">>, 0}]}], 126 | [<<"lmq/">>, {[{<<"timeout">>, 0}]}]], 127 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, set_default_props, [DefaultProps]), 128 | {error, _} = msgpack_rpc_client:call(Client, set_default_props, 129 | lists:nth(1, DefaultProps)), 130 | {ok, DefaultProps} = msgpack_rpc_client:call(Client, get_default_props, []), 131 | Name = <<"def/a">>, 132 | {ok, <<"ok">>} = msgpack_rpc_client:call(Client, push, [Name, 1]), 133 | {ok, {[{<<"queue">>, Name}, {<<"id">>, _}, {<<"type">>, <<"normal">>}, {<<"retry">>, 0}, {<<"content">>, 1}]}} = 134 | msgpack_rpc_client:call(Client, pull, [Name, 0]), 135 | {ok, <<"empty">>} = msgpack_rpc_client:call(Client, pull, [Name, 0]). 136 | -------------------------------------------------------------------------------- /test/lmq_queue_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue_SUITE). 2 | 3 | -include("lmq.hrl"). 4 | -include_lib("common_test/include/ct.hrl"). 5 | -export([init_per_suite/1, end_per_suite/1, 6 | init_per_testcase/2, end_per_testcase/2, 7 | all/0, groups/0]). 8 | -export([init/1, push_pull_done/1, release/1, release_multi/1, multi_queue/1, 9 | pull_timeout/1, async_request/1, pull_async/1, pull_and_timeout/1, 10 | update_properties/1, reload_properties/1]). 11 | 12 | all() -> 13 | [init, push_pull_done, release, release_multi, multi_queue, pull_timeout, 14 | async_request, pull_async, update_properties, reload_properties, {group, timing}]. 15 | 16 | groups() -> 17 | [{timing, [{repeat_until_any_fail, 10}], [pull_and_timeout]}]. 18 | 19 | init_per_suite(Config) -> 20 | Priv = ?config(priv_dir, Config), 21 | application:start(mnesia), 22 | application:set_env(mnesia, dir, Priv), 23 | application:start(lager), 24 | application:start(folsom), 25 | ok = lmq_lib:init_mnesia(), 26 | Config. 27 | 28 | end_per_suite(_Config) -> 29 | application:stop(mnesia), 30 | mnesia:delete_schema([node()]). 31 | 32 | init_per_testcase(init, Config) -> 33 | {ok, _} = lmq_event:start_link(), 34 | {ok, _} = lmq_hook:start_link(), 35 | Config; 36 | 37 | init_per_testcase(_, Config) -> 38 | {ok, _} = lmq_event:start_link(), 39 | {ok, _} = lmq_hook:start_link(), 40 | {ok, Pid} = lmq_queue:start_link(message), 41 | [{queue, Pid} | Config]. 42 | 43 | end_per_testcase(init, _Config) -> 44 | lmq_hook:stop(); 45 | end_per_testcase(_, Config) -> 46 | lmq_queue:stop(?config(queue, Config)), 47 | lmq_hook:stop(), 48 | lmq_lib:delete(message). 49 | 50 | init(_Config) -> 51 | not_found = lmq_lib:queue_info(queue_test_1), 52 | {ok, Q1} = lmq_queue:start_link(queue_test_1), 53 | [] = lmq_lib:queue_info(queue_test_1), 54 | ?DEFAULT_QUEUE_PROPS = lmq_queue:get_properties(Q1), 55 | lmq_queue:stop(Q1), 56 | 57 | P2 = lmq_misc:extend([{timeout, 10}], ?DEFAULT_QUEUE_PROPS), 58 | {ok, Q2} = lmq_queue:start_link(queue_test_1, [{timeout, 10}]), 59 | [{timeout, 10}] = lmq_lib:queue_info(queue_test_1), 60 | P2 = lmq_queue:get_properties(Q2), 61 | lmq_queue:stop(Q2), 62 | 63 | {ok, Q3} = lmq_queue:start_link(queue_test_1), 64 | [{timeout, 10}] = lmq_lib:queue_info(queue_test_1), 65 | P2 = lmq_queue:get_properties(Q3), 66 | lmq_queue:stop(Q3), 67 | 68 | Props = [{hooks, [{custom_hook, [{lmq_hook_sample1, [1]}]}]}], 69 | 1 = lmq_hook:call(queue_test_1, custom_hook, 1), 70 | {ok, Q4} = lmq_queue:start_link(queue_test_1, Props), 71 | Props = lmq_lib:queue_info(queue_test_1), 72 | P3 = lmq_misc:extend(Props, ?DEFAULT_QUEUE_PROPS), 73 | P3 = lmq_queue:get_properties(Q4), 74 | 2 = lmq_hook:call(queue_test_1, custom_hook, 1), 75 | lmq_queue:stop(Q4). 76 | 77 | push_pull_done(Config) -> 78 | Pid = ?config(queue, Config), 79 | Ref = make_ref(), 80 | ok = lmq_queue:push(Pid, Ref), 81 | M = lmq_queue:pull(Pid), 82 | {TS, UUID} = M#message.id, 83 | true = is_float(TS), 84 | true = is_binary(UUID), 85 | uuid:uuid_to_string(UUID), 86 | Ref = M#message.content, 87 | ok = lmq_queue:done(Pid, UUID). 88 | 89 | release(Config) -> 90 | Pid = ?config(queue, Config), 91 | Ref = make_ref(), 92 | ok = lmq_queue:push(Pid, Ref), 93 | M1 = lmq_queue:pull(Pid), 94 | Ref = M1#message.content, 95 | {_, UUID1} = M1#message.id, 96 | 2 = M1#message.retry, 97 | 98 | ok = lmq_queue:release(Pid, UUID1), 99 | not_found = lmq_queue:release(Pid, UUID1), 100 | not_found = lmq_queue:done(Pid, UUID1), 101 | M2 = lmq_queue:pull(Pid), 102 | Ref = M2#message.content, 103 | {_, UUID2} = M2#message.id, 104 | true = UUID1 =/= UUID2, 105 | 1 = M2#message.retry, 106 | 107 | ok = lmq_queue:put_back(Pid, UUID2), 108 | not_found = lmq_queue:put_back(Pid, UUID2), 109 | M3 = lmq_queue:pull(Pid), 110 | Ref = M3#message.content, 111 | {_, UUID3} = M3#message.id, 112 | 1 = M2#message.retry, 113 | ok = lmq_queue:done(Pid, UUID3). 114 | 115 | release_multi(Config) -> 116 | Pid = ?config(queue, Config), 117 | R = make_ref(), 118 | Parent = self(), 119 | ok = lmq_queue:push(Pid, R), 120 | M1 = lmq_queue:pull(Pid), 121 | spawn(fun() -> Parent ! lmq_queue:pull(Pid) end), 122 | R = M1#message.content, 123 | {_, UUID1} = M1#message.id, 124 | timer:sleep(10), %% waiting for starting process 125 | ok = lmq_queue:release(Pid, UUID1), 126 | receive 127 | M2 -> 128 | R = M2#message.content, 129 | {_, UUID2} = M2#message.id, 130 | true = UUID1 =/= UUID2, 131 | ok = lmq_queue:done(Pid, UUID2) 132 | after 100 -> 133 | ct:fail(no_response) 134 | end. 135 | 136 | multi_queue(Config) -> 137 | Q1 = ?config(queue, Config), 138 | {ok, Q2} = lmq_queue:start_link(for_test), 139 | Ref1 = make_ref(), 140 | Ref2 = make_ref(), 141 | ok = lmq_queue:push(Q1, Ref1), 142 | ok = lmq_queue:push(Q2, Ref2), 143 | M2 = lmq_queue:pull(Q2), Ref2 = M2#message.content, 144 | M1 = lmq_queue:pull(Q1), Ref1 = M1#message.content, 145 | ok = lmq_queue:done(Q1, element(2, M1#message.id)), 146 | ok = lmq_queue:retain(Q2, element(2, M2#message.id)), 147 | ok = lmq_queue:release(Q2, element(2, M2#message.id)), 148 | M3 = lmq_queue:pull(Q2), 149 | ok = lmq_queue:done(Q2, element(2, M3#message.id)), 150 | ok = lmq_queue:stop(Q2). 151 | 152 | pull_timeout(_Config) -> 153 | {ok, Q} = lmq_queue:start_link(pull_timeout, [{timeout, 0.3}]), 154 | Ref = make_ref(), 155 | empty = lmq_queue:pull(Q, 0), 156 | ok = lmq_queue:push(Q, Ref), 157 | M1 = lmq_queue:pull(Q, 0), Ref = M1#message.content, 158 | M2 = lmq_queue:pull(Q), Ref = M2#message.content, 159 | true = M1 =/= M2, 160 | empty = lmq_queue:pull(Q, 0.2), 161 | M3 = lmq_queue:pull(Q, 0.2), Ref = M3#message.content. 162 | 163 | async_request(_Config) -> 164 | {ok, Q} = lmq_queue:start_link(async_request, [{timeout, 0.4}]), 165 | R1 = make_ref(), 166 | R2 = make_ref(), 167 | ok = lmq_queue:push(Q, R1), 168 | ok = lmq_queue:push(Q, R2), 169 | Parent = self(), 170 | spawn(fun() -> 171 | Parent ! {1, lmq_queue:pull(Q)}, 172 | Parent ! {2, lmq_queue:pull(Q)}, 173 | Parent ! {3, lmq_queue:pull(Q, 0.5)} 174 | end), 175 | lists:foreach(fun(_) -> 176 | receive 177 | {1, M1} -> ct:pal("~p", [M1]), R1 = M1#message.content; 178 | {2, M2} -> 179 | ct:pal("~p", [M2]), {_, UUID2} = M2#message.id, 180 | ok = lmq_queue:done(Q, UUID2); 181 | {3, M3} -> ct:pal("~p", [M3]), R1 = M3#message.content 182 | end 183 | end, lists:seq(1, 3)). 184 | 185 | pull_async(_Config) -> 186 | {ok, Q} = lmq_queue:start_link(message, [{timeout, 0.1}]), 187 | R = make_ref(), 188 | lmq_queue:push(Q, R), 189 | Id1 = lmq_queue:pull_async(Q), 190 | receive {Id1, M1} when R =:= M1#message.content -> ok 191 | after 100 -> ct:fail(no_response) 192 | end, 193 | 194 | Id2 = lmq_queue:pull_async(Q), 195 | receive {Id2, M2} when R =:= M2#message.content -> ok 196 | after 150 -> ct:fail(no_response) 197 | end, 198 | 199 | Id3 = lmq_queue:pull_async(Q), 200 | ok = lmq_queue:pull_cancel(Q, Id3), 201 | receive {Id3, _} -> ct:fail(cancel_failed) 202 | after 150 -> ok 203 | end. 204 | 205 | pull_and_timeout(Config) -> 206 | Pid = ?config(queue, Config), 207 | Parent = self(), 208 | Ref = make_ref(), 209 | F = fun() -> Parent ! {Ref, lmq_queue:pull(Pid, 0.01)} end, 210 | [spawn(F) || _ <- lists:seq(1, 100)], 211 | wait_pull_and_timeout(Ref, 100). 212 | 213 | wait_pull_and_timeout(_, 0) -> 214 | ok; 215 | wait_pull_and_timeout(Ref, N) -> 216 | receive {Ref, empty} -> wait_pull_and_timeout(Ref, N-1) 217 | after 200 -> ct:fail(no_response) 218 | end. 219 | 220 | update_properties(Config) -> 221 | Q = ?config(queue, Config), 222 | Props1 = [{timeout, 1}], 223 | Props2 = [{hooks, [{custom_hook, [{lmq_hook_sample1, [1]}]}]}], 224 | [] = lmq_lib:queue_info(message), 225 | ?DEFAULT_QUEUE_PROPS = lmq_queue:get_properties(Q), 226 | lmq_queue:props(Q, Props1), 227 | true = lmq_misc:extend(Props1, ?DEFAULT_QUEUE_PROPS) =:= lmq_queue:get_properties(Q), 228 | Props1 = lmq_lib:queue_info(message), 229 | 1 = lmq_hook:call(message, custom_hook, 1), 230 | lmq_queue:props(Q, Props2), 231 | true = lmq_misc:extend(Props2, ?DEFAULT_QUEUE_PROPS) =:= lmq_queue:get_properties(Q), 232 | Props2 = lmq_lib:queue_info(message), 233 | 2 = lmq_hook:call(message, custom_hook, 1). 234 | 235 | reload_properties(Config) -> 236 | Q = ?config(queue, Config), 237 | Props1 = [{timeout, 1}], 238 | Props2 = [{hooks, [{custom_hook, [{lmq_hook_sample1, [1]}]}]}], 239 | [] = lmq_lib:queue_info(message), 240 | ?DEFAULT_QUEUE_PROPS = lmq_queue:get_properties(Q), 241 | lmq_lib:update_queue_props(message, Props1), 242 | lmq_queue:reload_properties(Q), 243 | true = lmq_misc:extend(Props1, ?DEFAULT_QUEUE_PROPS) =:= lmq_queue:get_properties(Q), 244 | Props1 = lmq_lib:queue_info(message), 245 | 1 = lmq_hook:call(message, custom_hook, 1), 246 | lmq_lib:update_queue_props(message, Props2), 247 | lmq_queue:reload_properties(Q), 248 | true = lmq_misc:extend(Props2, ?DEFAULT_QUEUE_PROPS) =:= lmq_queue:get_properties(Q), 249 | Props2 = lmq_lib:queue_info(message), 250 | 2 = lmq_hook:call(message, custom_hook, 1). 251 | -------------------------------------------------------------------------------- /test/lmq_queue_mgr_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_queue_mgr_SUITE). 2 | 3 | -include("lmq.hrl"). 4 | -include_lib("common_test/include/ct.hrl"). 5 | -export([init_per_suite/1, end_per_suite/1, 6 | init_per_testcase/2, end_per_testcase/2, 7 | all/0]). 8 | -export([multi_queue/1, get/1, match/1, restart_queue/1, auto_load/1, 9 | default_props/1]). 10 | 11 | all() -> 12 | [multi_queue, get, match, restart_queue, auto_load, default_props]. 13 | 14 | init_per_suite(Config) -> 15 | Priv = ?config(priv_dir, Config), 16 | application:start(mnesia), 17 | application:set_env(mnesia, dir, Priv), 18 | lmq_lib:init_mnesia(), 19 | Config. 20 | 21 | end_per_suite(_Config) -> 22 | application:stop(mnesia), 23 | mnesia:delete_schema([node()]). 24 | 25 | init_per_testcase(auto_load, Config) -> 26 | {ok, _} = lmq_event:start_link(), 27 | {ok, _} = lmq_hook:start_link(), 28 | Config; 29 | 30 | init_per_testcase(_, Config) -> 31 | {ok, _} = lmq_event:start_link(), 32 | {ok, _} = lmq_hook:start_link(), 33 | {ok, _} = lmq_queue_supersup:start_link(), 34 | Config. 35 | 36 | end_per_testcase(auto_load, _Config) -> 37 | lmq_hook:stop(), 38 | mnesia:transaction(fun() -> mnesia:delete({?LMQ_INFO_TABLE, default_props}) end); 39 | 40 | end_per_testcase(_, _Config) -> 41 | lmq_hook:stop(). 42 | 43 | multi_queue(_Config) -> 44 | Q1 = lmq_queue_mgr:get(q1, [create]), 45 | Q2 = lmq_queue_mgr:get(q2, [create]), 46 | R1 = make_ref(), R2 = make_ref(), 47 | lmq_queue:push(Q1, R1), 48 | lmq_queue:push(Q2, R2), 49 | M2 = lmq_queue:pull(Q2), 50 | M1 = lmq_queue:pull(Q1), 51 | R1 = M1#message.content, 52 | R2 = M2#message.content. 53 | 54 | get(_Config) -> 55 | not_found = lmq_queue_mgr:get('get/a'), 56 | true = is_pid(lmq_queue_mgr:get('get/a', [create])), 57 | [] = lmq_lib:queue_info('get/a'), 58 | 59 | %% ensure do not override props if exists 60 | true = is_pid(lmq_queue_mgr:get('get/a', [create, {props, [{retry, infinity}]}])), 61 | [] = lmq_lib:queue_info('get/a'), 62 | 63 | %% ensure override props if setting update flag 64 | true = is_pid(lmq_queue_mgr:get('get/a', [update, {props, [{retry, infinity}]}])), 65 | [{retry, infinity}] = lmq_lib:queue_info('get/a'), 66 | 67 | %% ensure custom props can be set 68 | true = is_pid(lmq_queue_mgr:get('get/b', [create, {props, [{retry, infinity}]}])), 69 | [{retry, infinity}] = lmq_lib:queue_info('get/b'), 70 | 71 | %% ensure old props are considered when updating 72 | true = is_pid(lmq_queue_mgr:get('get/b', [update, {props, [{pack, 10}]}])), 73 | [{pack, 10}, {retry, infinity}] = lmq_lib:queue_info('get/b'), 74 | 75 | %% ensure do not override props if exists in mnesia 76 | lmq_lib:create('get/c', [{pack, 10}]), 77 | true = is_pid(lmq_queue_mgr:get('get/c', [create])), 78 | [{pack, 10}] = lmq_lib:queue_info('get/c'). 79 | 80 | match(_Config) -> 81 | lmq_queue_mgr:get(foo, [create]), 82 | Q1 = lmq_queue_mgr:get('foo/bar', [create]), 83 | Q2 = lmq_queue_mgr:get('foo/baz', [create]), 84 | R = lmq_queue_mgr:match("^foo/.*"), 85 | R = lmq_queue_mgr:match(<<"^foo/.*">>), 86 | true = lists:sort([{'foo/bar', Q1}, {'foo/baz', Q2}]) =:= lists:sort(R), 87 | %% error case 88 | [] = lmq_queue_mgr:match("AAA"), 89 | {error, invalid_regexp} = lmq_queue_mgr:match("a[1-"). 90 | 91 | restart_queue(_Config) -> 92 | Q1 = lmq_queue_mgr:get(test, [create]), 93 | exit(Q1, kill), 94 | timer:sleep(50), % sleep until DOWN message handled 95 | Q2 = lmq_queue_mgr:get(test), 96 | true = Q1 =/= Q2. 97 | 98 | auto_load(_Config) -> 99 | DefaultProps = [{"lmq", [{retry, 0}]}], 100 | lmq_lib:set_lmq_info(default_props, DefaultProps), 101 | lmq_lib:create(auto_loaded_1), 102 | lmq_lib:create(auto_loaded_2), 103 | {ok, _} = lmq_queue_supersup:start_link(), 104 | timer:sleep(100), % wait until queue will be started 105 | DefaultProps = lmq_queue_mgr:get_default_props(), 106 | true = is_pid(lmq_queue_mgr:get('auto_loaded_1')), 107 | true = is_pid(lmq_queue_mgr:get('auto_loaded_2')), 108 | not_found = lmq_queue_mgr:get('auto_loaded_3'). 109 | 110 | default_props(_Config) -> 111 | PropsList = [{"lmq", [{retry, 0}]}], 112 | [] = lmq_queue_mgr:get_default_props(), 113 | ok = lmq_queue_mgr:set_default_props(PropsList), 114 | {ok, PropsList} = lmq_lib:get_lmq_info(default_props), 115 | PropsList = lmq_queue_mgr:get_default_props(), 116 | invalid_syntax = lmq_queue_mgr:set_default_props({}). 117 | -------------------------------------------------------------------------------- /test/lmq_test_handler.erl: -------------------------------------------------------------------------------- 1 | -module(lmq_test_handler). 2 | 3 | -behaviour(gen_event). 4 | 5 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, 6 | code_change/3, terminate/2]). 7 | 8 | init(Pid) -> 9 | {ok, Pid}. 10 | 11 | handle_event(Event, Pid) -> 12 | ct:pal("Event received: ~p", [Event]), 13 | Pid ! {test_handler, Event}, 14 | {ok, Pid}. 15 | 16 | handle_call(_, State) -> 17 | {ok, ok, State}. 18 | 19 | handle_info(_, State) -> 20 | {ok, State}. 21 | 22 | code_change(_OldVsn, State, _Extra) -> 23 | {ok, State}. 24 | 25 | terminate(_Reason, _State) -> 26 | ok. 27 | --------------------------------------------------------------------------------