├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── EXAMPLES.md ├── LICENSE ├── README.md ├── config └── config.exs ├── examples ├── bench.exs ├── pub.exs └── sub.exs ├── lib ├── nats.ex └── nats │ ├── client.ex │ ├── connection.ex │ └── parser.ex ├── mix.exs ├── mix.lock ├── scripts └── install_gnatsd.sh ├── src ├── json_lexer.erl ├── json_lexer.xrl ├── json_parser.erl └── json_parser.yrl └── test ├── bench-results.txt ├── conf ├── auth.conf ├── key.pem ├── plain.conf ├── server.pem └── tls.conf ├── nats ├── client_test.exs ├── connection_test.exs ├── parser_test.exs └── pubsub_test.exs ├── run-test-servers.sh └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | /_build 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | *.exs~ 7 | *.ex~ 8 | # Benchstuff, keep now 9 | #/bench/graphs 10 | #/bench/snapshots 11 | # generated docs/binaries 12 | /doc 13 | /rel 14 | /ebin 15 | # OSX 16 | .DS_Store 17 | Icon? 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | cache: 3 | directories: 4 | - $HOME/gnatsd 5 | before_install: 6 | - bash ./scripts/install_gnatsd.sh 7 | before_script: 8 | - export PATH=$HOME/gnatsd:$PATH 9 | language: elixir 10 | otp_release: 11 | - 18.0 12 | - 19.1 13 | - 19.2 14 | elixir: 15 | - 1.2.2 16 | - 1.3.4 17 | - 1.4.2 18 | matrix: 19 | exclude: 20 | - otp_release: 19.1 21 | elixir: 1.2.2 22 | - otp_release: 19.2 23 | elixir: 1.2.2 24 | sudo: false 25 | script: 26 | - ./test/run-test-servers.sh < /dev/null 2>&1 & 27 | - "MIX_ENV=test mix do deps.get, compile, coveralls.travis" 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to elixir-nats 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved! 5 | 6 | ## Using the issue tracker 7 | 8 | Use the issues tracker for: 9 | 10 | * [bug reports](#bug-reports) 11 | * [submitting pull requests](#pull-requests) 12 | 13 | Please **do not** use the issue tracker for personal support requests nor feature requests. Instead, 14 | please reach out to us at 15 | 16 | * [Twitter](https://twitter.com/nats_io) 17 | * [Google Mailing List](https://groups.google.com/forum/#!forum/natsio) 18 | * [Slack](https://docs.google.com/a/apcera.com/forms/d/104yA7oqq7SPoMDG_J9MnVE74gVwBnTmVHKP5ABHoM5k/viewform?embedded=true) 19 | 20 | 21 | ## Bug reports 22 | 23 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 24 | Good bug reports are extremely helpful - thank you! 25 | 26 | Guidelines for bug reports: 27 | 28 | 1. **Use the GitHub issue search** — [check if the issue has already been 29 | reported](https://github.com/nats-io/elixir-nats/search?type=Issues) 30 | 31 | 2. **Check if the issue has been fixed** — try to reproduce it using the 32 | `master` branch in the repository. 33 | 34 | 3. **Isolate and report the problem** — ideally create a test case in the test 35 | directory demonstrating the issue as simply and with as little extraneous code as possible. 36 | 37 | Please try to be as detailed as possible in your report. Include information about 38 | your Operating System, your Erlang and Elixir versions. Please provide steps to 39 | reproduce the issue as well as the outcome you were expecting! All these details 40 | will help developers to fix any potential bugs. 41 | 42 | Example: 43 | 44 | > Short and descriptive example bug report title 45 | > 46 | > A summary of the issue and the environment in which it occurs. If suitable, 47 | > include the steps required to reproduce the bug. 48 | > 49 | > 1. This is the first step 50 | > 2. This is the second step 51 | > 3. Further steps, etc. 52 | > 53 | > `` - a link to the reduced test case (e.g. a GitHub Gist) 54 | > 55 | > Any other information you want to share that is relevant to the issue being 56 | > reported. This might include the lines of code that you have identified as 57 | > causing the bug, and potential solutions (and your opinions on their 58 | > merits). 59 | 60 | ## Feature requests 61 | 62 | Feature requests are welcome and should be discussed on [the natsio mailing list](https://groups.google.com/forum/#!forum/natsio), 63 | or [reach out on Slack](https://docs.google.com/a/apcera.com/forms/d/104yA7oqq7SPoMDG_J9MnVE74gVwBnTmVHKP5ABHoM5k/viewform?embedded=true). 64 | 65 | ## Contributing 66 | 67 | We invite everyone to contribute to *elixir-nats* and help us tackle 68 | existing issues! 69 | 70 | With tests running and passing, you are ready to contribute and 71 | send your pull requests. 72 | 73 | ## Contributing Documentation 74 | 75 | Code documentation (`@doc`, `@moduledoc`, `@typedoc`) has a special convention: 76 | the first paragraph is considered to be a short summary. 77 | 78 | For functions, macros and callbacks say what it will do. For example write 79 | something like: 80 | 81 | ```elixir 82 | @doc """ 83 | Returns only those elements for which `fun` is `true`. 84 | 85 | ... 86 | """ 87 | def filter(collection, fun) ... 88 | ``` 89 | 90 | For modules, protocols and types say what it is. For example write 91 | something like: 92 | 93 | ```elixir 94 | defmodule File.Stat do 95 | @moduledoc """ 96 | Information about a file. 97 | 98 | ... 99 | """ 100 | 101 | defstruct [...] 102 | end 103 | ``` 104 | 105 | Keep in mind that the first paragraph might show up in a summary somewhere, long 106 | texts in the first paragraph create very ugly summaries. As a rule of thumb 107 | anything longer than 80 characters is too long. 108 | 109 | Try to keep unnecessary details out of the first paragraph, it's only there to 110 | give a user a quick idea of what the documented "thing" does/is. The rest of the 111 | documentation string can contain the details, for example when a value and when 112 | `nil` is returned. 113 | 114 | If possible include examples, preferably in a form that works with doctests. For 115 | example: 116 | 117 | ```elixir 118 | @doc """ 119 | Returns only those elements for which `fun` is `true`. 120 | 121 | ## Examples 122 | 123 | iex> Enum.filter([1, 2, 3], fn(x) -> rem(x, 2) == 0 end) 124 | [2] 125 | 126 | """ 127 | def filter(collection, fun) ... 128 | ``` 129 | 130 | This makes it easy to test the examples so that they don't go stale and examples 131 | are often a great help in explaining what a function does. 132 | 133 | ## Pull requests 134 | 135 | Good pull requests - patches, improvements, new features - are a fantastic 136 | help. They should remain focused in scope and avoid containing unrelated 137 | commits. 138 | 139 | **IMPORTANT**: By submitting a patch, you agree that your work will be 140 | licensed under the license used by the project. 141 | 142 | If you have any large pull request in mind (e.g. implementing features, 143 | refactoring code, etc), **please ask first** otherwise you risk spending 144 | a lot of time working on something that the project's developers might 145 | not want to merge into the project. 146 | 147 | Please adhere to the coding conventions in the project (indentation, 148 | accurate comments, etc.) and don't forget to add your own tests and 149 | documentation. When working with Git, we recommend the following process 150 | in order to craft an excellent pull request: 151 | 152 | 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your fork, 153 | and configure the remotes: 154 | 155 | ```sh 156 | # Clone your fork of the repo into the current directory 157 | git clone https://github.com//elixir-nats 158 | # Navigate to the newly cloned directory 159 | cd elixir-nats 160 | # Assign the original repo to a remote called "upstream" 161 | git remote add upstream https://github.com/nats-io/elixir-nats 162 | ``` 163 | 164 | 2. If you cloned a while ago, get the latest changes from upstream: 165 | 166 | ```sh 167 | git checkout master 168 | git pull upstream master 169 | ``` 170 | 171 | 3. Create a new topic branch (off of `master`) to contain your feature, change, 172 | or fix. 173 | 174 | **IMPORTANT**: Making changes in `master` is discouraged. You should always 175 | keep your local `master` in sync with upstream `master` and make your 176 | changes in topic branches. 177 | 178 | ```sh 179 | git checkout -b 180 | ``` 181 | 182 | 4. Commit your changes in logical chunks. Keep your commit messages organized, 183 | with a short description in the first line and more detailed information on 184 | the following lines. Feel free to use Git's 185 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 186 | feature to tidy up your commits before making them public. 187 | 188 | 5. Make sure all the tests are still passing. 189 | 190 | ```sh 191 | mix test 192 | ``` 193 | 194 | This command will compile the code in your branch and use that 195 | version of Elixir to run the tests. This is needed to ensure your changes can 196 | pass all the tests. 197 | 198 | 6. Push your topic branch up to your fork: 199 | 200 | ```sh 201 | git push origin 202 | ``` 203 | 204 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 205 | with a clear title and description. 206 | 207 | 8. If you haven't updated your pull request for a while, you should consider 208 | rebasing on master and resolving any conflicts. 209 | 210 | **IMPORTANT**: _Never ever_ merge upstream `master` into your branches. You 211 | should always `git rebase` on `master` to bring your changes up to date when 212 | necessary. 213 | 214 | ```sh 215 | git checkout master 216 | git pull upstream master 217 | git checkout 218 | git rebase master 219 | ``` 220 | 221 | Thank you for your contributions! 222 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Walkthrough 4 | 5 | ## Ex 1 6 | 7 | See [Some reference](exampless) 8 | ```sh 9 | $ mix run examples/pub.exs 10 | ``` 11 | 12 | ## Ex 2 13 | 14 | ## Next Steps 15 | 16 | ## References 17 | Copyright 2016 Apcera Inc. All rights reserved. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 NATS Cloud Native Messaging System 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Deprecated) An Elixir framework for [NATS](https://nats.io/) 2 | [![Build Status](https://travis-ci.org/nats-io/elixir-nats.svg?branch=master)](https://travis-ci.org/nats-io/elixir-nats) 3 | [![Coverage Status](https://coveralls.io/repos/github/nats-io/elixir-nats/badge.svg?branch=master)](https://coveralls.io/github/nats-io/elixir-nats?branch=master) 4 | 5 | # Deprecated 6 | 7 | :no_entry_sign: **Do not use this client.** It has been deprecated. Use the [nats.ex](https://github.com/nats-io/nats.ex) elixir client instead. 8 | 9 | # Original Content 10 | 11 | _Elixir style_ documentation is located [here](https://nats-io.github.io/elixir-nats/) 12 | 13 | ## Getting Started 14 | 15 | The framework requires Elixir 1.2.2 or above. To use it in your project, 16 | add the following to your `mix.exs`: 17 | 18 | ```elixir 19 | defp deps do 20 | # for github 21 | [{:nats, git: "https://github.com/nats-io/elixir-nats.git"}] 22 | # for hex (forthcoming) 23 | [{:natsio, "~> 0.1.6"}] 24 | end 25 | ``` 26 | 27 | 28 | ## To build and/or test from sources 29 | 30 | Run the test servers: 31 | 32 | ```sh 33 | ./test/run-test-servers.sh 34 | ``` 35 | 36 | Clone, fork or pull this repository. And then: 37 | 38 | ```sh 39 | $ mix deps.get 40 | $ mix compile 41 | $ mix test 42 | ``` 43 | 44 | To run the examples: 45 | 46 | ```sh 47 | $ mix run examples/sub.exs 48 | $ mix run examples/pub.exs 49 | ``` 50 | 51 | The default NATS configuration looks for a [gnatsd](https://github.com/nats-io/gnatsd) instance running on the default port of 4222 on 127.0.0.1. 52 | 53 | You can override the configuration by passing a map to `Client.start_link`. For example: 54 | 55 | ```elixir 56 | alias Nats.Client 57 | 58 | nats_conf = %{host: "some-host", port: 3222, 59 | tls_required: true, 60 | auth: %{ user: "some-user", pass: "some-pass"}} 61 | {:ok, ref} = Client.start_link(nats_conf) 62 | Client.pub(ref, "subject", "hello NATS world!") 63 | ``` 64 | 65 | The framework leverages the standard logger, by default only errors are logged. To view additional logging, update your `config/config.exs`: 66 | ```elixir 67 | use Mix.Config 68 | 69 | # debug will log most everything 70 | # info prints connection lifecycle events 71 | # error prints errors 72 | config :logger, level: :debug 73 | ``` 74 | 75 | ## Status 76 | 77 | Most NATS related capabilities are in place: publishing, subscribing, tls, 78 | authorization. 79 | 80 | Elixir Application, supervisor/monitor and environment support needs improved 81 | 82 | Documentation is minimal. For now: 83 | 84 | ```sh 85 | $ mix docs 86 | $ open docs/index.html 87 | $ cat examples/*.exs 88 | ``` 89 | 90 | ## Release Library 91 | 92 | Bump version in `mix.exs`: 93 | 94 | ```elixir 95 | ... 96 | 97 | @version "0.1.6" 98 | 99 | ... 100 | ``` 101 | 102 | As an administrator in the natsio [hex package](https://hex.pm/packages/natsio): 103 | 104 | ```sh 105 | mix hex.publish 106 | ``` 107 | 108 | ## License 109 | 110 | [License](LICENSE) 111 | 112 | Copyright 2016 Apcera Inc. All rights reserved. 113 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | 3 | use Mix.Config 4 | 5 | config :logger, level: :error 6 | #config :nats, level: :error 7 | -------------------------------------------------------------------------------- /examples/bench.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | defmodule Bench do 3 | alias Nats.Client 4 | 5 | @moduledoc """ 6 | Simple benchmark for NATS client operations. Far from perfect :-( and 7 | very ugly! 8 | 9 | ## Overview 10 | This module's goals is to measure the amount of time it takes to perfom 11 | various NATS verbs/operations for a given message size N. 12 | 13 | Our operations here are pub, pubsub and req/rep. 14 | 15 | From a model perspective we wish to define: 16 | - throughout in bytes/memory per second (or some time frame) 17 | - throughout in messages per second 18 | - latency for pub/sub and req/reply operations 19 | """ 20 | 21 | # Test how many messages of a given size we can publish 22 | # in a given time frame... 23 | @mesg_sizes [0, 8, 512, 1024, 4096, 8192] 24 | @sync_point 32768 25 | 26 | # make a message of the given size... 27 | defp make_mesg(size) do 28 | template = "Hello NATS world!" 29 | template_size = String.length(template) 30 | String.duplicate(template, div(size, template_size)) <> 31 | String.slice(template, 0, rem(size, template_size)) 32 | end 33 | 34 | defp receiver_loop(so_far, so_far, _, sync_pid) do 35 | # IO.puts "receiver_loop: got expected messages, exiting..." 36 | send sync_pid, {:done, so_far} 37 | end 38 | defp receiver_loop(so_far, expected, sync, sync_pid) do 39 | receive do 40 | # make sure its a nats message 41 | {:msg, _, _, _, _ } -> 42 | so_far = so_far + 1 43 | # if rem(so_far, sync) == 0, do: send sync_pid, {:received, so_far} 44 | receiver_loop(so_far, expected, sync, sync_pid) 45 | :stop_recv -> 46 | # IO.puts "receiver_loop: stopping #{expected}" 47 | :ok 48 | after 6000 -> 49 | IO.puts "receiver_loop: timeout" 50 | receiver_loop(so_far, expected, sync, sync_pid) 51 | end 52 | end 53 | defp receiver do 54 | receive do 55 | {:start, expected, sync, sync_pid} -> 56 | send sync_pid, :ok 57 | #IO.puts "starting receiver..." 58 | receiver_loop(0, expected, sync, sync_pid) 59 | :stop_recv -> 60 | #IO.puts "receiver: stopping (never started...)" 61 | :ok 62 | after 1000 -> IO.puts "receiver: timeout" 63 | end 64 | end 65 | def setup(size, num_msgs) do 66 | subject = "BS" 67 | {:ok, conn} = Client.start_link 68 | pid = spawn_link(&receiver/0) 69 | send pid, {:start, num_msgs, @sync_point, self()} 70 | # on_exit fn -> 71 | # IO.puts "done running..." 72 | # send pid, :stop_recv 73 | # :ok 74 | # end 75 | {:ok, %{ receiver: pid, mesg: make_mesg(size), 76 | conn: conn, num_msgs: num_msgs, 77 | subject: subject <> to_string(size) }} 78 | end 79 | 80 | def pub_bench(conn, receiver, subject, _size, mesg, num_msgs) do 81 | send receiver, :stop_recv 82 | before = get_mem(true) 83 | {t, :ok} = :timer.tc(fn -> do_pub(conn, subject, mesg, num_msgs, true) 84 | end) 85 | after_mem = get_mem() 86 | after_gc = get_mem(true) 87 | used = sub_mem(after_gc, before) 88 | used_pre_gc = sub_mem(after_mem, before) 89 | mem_stats = %{ used: used, used_pre_gc: used_pre_gc } 90 | {t, num_msgs, mem_stats} 91 | end 92 | def sub_bench(conn, receiver, subject, _size, _mesg, num_msgs) do 93 | send receiver, :stop_recv 94 | before = get_mem(true) 95 | {t, :ok} = :timer.tc(fn -> do_sub(conn, subject, receiver, num_msgs, true) 96 | end) 97 | after_mem = get_mem() 98 | after_gc = get_mem(true) 99 | used = sub_mem(after_gc, before) 100 | used_pre_gc = sub_mem(after_mem, before) 101 | mem_stats = %{ used: used, used_pre_gc: used_pre_gc } 102 | {t, num_msgs, mem_stats} 103 | end 104 | defp do_sub(_, _, _, 0, false), do: :ok 105 | defp do_sub(con, _, _, 0, true), do: Client.flush(con, :infinity) 106 | defp do_sub(con, sub, r, n, flush) do 107 | {:ok, _ref} = Client.sub(con, r, sub <> to_string(n)) 108 | do_sub(con, sub, r, n-1, flush) 109 | end 110 | def pubasync_bench(conn, receiver, subject, _size, mesg, num_msgs) do 111 | send receiver, :stop_recv 112 | before = get_mem(true) 113 | {t, :ok} = :timer.tc(fn -> do_pub(conn, subject, mesg, num_msgs, false) end) 114 | after_mem = get_mem() 115 | after_gc = get_mem(true) 116 | used = sub_mem(after_gc, before) 117 | used_pre_gc = sub_mem(after_mem, before) 118 | mem_stats = %{ used: used, used_pre_gc: used_pre_gc } 119 | {t, num_msgs, mem_stats} 120 | end 121 | defp do_pub(_, _, _, 0, false), do: :ok 122 | defp do_pub(con, _, _, 0, true), do: Client.flush(con, :infinity) 123 | defp do_pub(con, sub, what, n, flush) do 124 | :ok = Client.pub(con, sub, what) 125 | do_pub(con, sub, what, n-1, flush) 126 | end 127 | 128 | def time_start(), do: :erlang.timestamp() 129 | def time_delta(now, prev), do: :erlang.now_diff(now, prev) 130 | def get_mem(gc_first \\ false) do 131 | if gc_first, do: :erlang.garbage_collect() 132 | pinfo = for pid <- :erlang.processes(), 133 | do: :erlang.process_info(pid, :memory) 134 | sys = :erlang.memory() 135 | mapped = Enum.map(pinfo, fn v -> 136 | case v do 137 | {:memory, how_much} -> how_much 138 | _other -> 0 139 | end 140 | end) 141 | tot_mem = Enum.reduce(mapped, &+/2) 142 | %{memory: tot_mem, sys_bin: sys[:binary], sys_atom: sys[:atom]} 143 | end 144 | def sub_mem(now = %{}, prev = %{}) do 145 | %{ memory: now.memory - prev.memory, 146 | sys_bin: now.sys_bin - prev.sys_bin, 147 | sys_atom: now.sys_atom - prev.sys_atom} 148 | end 149 | 150 | def pubsub_bench(conn, receiver, subject, _size, mesg, num_msgs) do 151 | {:ok, v} = Client.start_link 152 | # v = conn 153 | {:ok, ref} = Client.sub(v, receiver, subject) 154 | :ok = Client.flush(v, :infinity) 155 | # wait till our receiver to start... 156 | 157 | before = get_mem(true) 158 | receive do :ok -> :ok end 159 | {t, :ok } = :timer.tc(fn -> 160 | do_pubsub(conn, subject, mesg, 0, num_msgs, 0) 161 | end) 162 | after_mem = get_mem() 163 | after_gc = get_mem(true) 164 | used = sub_mem(after_gc, before) 165 | used_pre_gc = sub_mem(after_mem, before) 166 | mem_stats = %{ used: used, used_pre_gc: used_pre_gc } 167 | :ok = Client.unsub(v, ref) 168 | :ok = GenServer.stop(v) 169 | {t, num_msgs, mem_stats} 170 | end 171 | 172 | defp drain(conn) do 173 | receive do 174 | {:done, _count} -> 175 | # IO.puts "drain #{count}" 176 | :ok 177 | _ -> drain(conn) 178 | after 5_000 -> 179 | # IO.puts "timeout draining" 180 | drain(conn) 181 | end 182 | end 183 | 184 | defp do_pubsub(conn, _, _, so_far, so_far, _) do 185 | Client.flush(conn, :infinity) 186 | drain(conn) 187 | end 188 | defp do_pubsub(conn, sub, what, so_far, n, last_update) do 189 | so_far = so_far + 1 190 | :ok = Client.pub(conn, sub, what) 191 | # if rem(so_far, @sync_point) == 0 do 192 | # receive do 193 | # {:received, x} -> 194 | # IO.puts x 195 | # last_update = x 196 | # end 197 | # end 198 | # if (so_far - last_update) == @sync_point do 199 | # :erlang.yield() # .sleep(10) 200 | # end 201 | do_pubsub(conn, sub, what, so_far, n, last_update) 202 | end 203 | defp teardown(ctx) do 204 | # IO.puts"stopping..." 205 | :ok = GenServer.stop(ctx.conn) 206 | end 207 | def run_test(_, test, size, num_msgs) do 208 | case setup(size, num_msgs) do 209 | {:ok, sub_ctx} -> 210 | # IO.puts "context: #{inspect Map.delete(sub_ctx, :mesg)}" 211 | res = test.(sub_ctx.conn, sub_ctx.receiver, sub_ctx.subject, size, 212 | sub_ctx.mesg, sub_ctx.num_msgs) 213 | teardown(sub_ctx) 214 | res 215 | other -> 216 | other 217 | end 218 | end 219 | def predict_test(nruns, duration, name, test, size) do 220 | # IO.puts "RUNNING: #{name}: N=#{inspect nruns} T=#{inspect duration} S=#{size}" 221 | res = run_test(name, test, size, nruns) 222 | case res do 223 | {micros, ^nruns, _mem} when micros < duration -> 224 | per_n = micros / nruns 225 | new_runs = if per_n == 0, do: nruns * 10, else: duration / per_n 226 | new_runs = trunc(min(nruns * 1.66 + (3.33*(micros / duration)), 227 | new_runs)) 228 | [res|predict_test(new_runs, duration, name, test, size)] 229 | _x -> [res] 230 | end 231 | end 232 | 233 | def predict_test(duration, name, test, size) do 234 | old_predicton = 10000 235 | duration = s2mu(duration) 236 | predict_test(old_predicton, duration, name, test, size) 237 | end 238 | def run_tests (duration) do 239 | run_tests(duration, 240 | [ 241 | {"PUB", &pub_bench/6, true}, 242 | {"PUB-SUB", &pubsub_bench/6, true}, 243 | {"PUB-ASYNC", &pubasync_bench/6, true}, 244 | {"SUB", &sub_bench/6, false} 245 | ], 246 | @mesg_sizes) 247 | end 248 | defp summarize(results) do 249 | true_res = last(results) 250 | # IO.inspect results 251 | per_n = fn { micros, count, _ } -> count / micros end 252 | per_n_red = Enum.map(results, per_n) 253 | stats = fn en -> 254 | cnt = Enum.count(en) 255 | mi = Enum.reduce(en, &min/2) 256 | ma = Enum.reduce(en, &max/2) 257 | sum = Enum.reduce(en, &+/2) 258 | mean = sum / cnt 259 | { cnt, mi, ma, sum, mean } 260 | end 261 | rst = stats.(per_n_red) 262 | { cnt, _mi, _ma, _sum, mean } = rst 263 | diffs = Enum.map(per_n_red, &:math.pow(&1 - mean, 2)) 264 | variances = stats.(diffs) 265 | { _scnt, _smi, _sma, _ssu, variance } = variances 266 | sdev = :math.sqrt(variance) 267 | std_err = sdev / :math.sqrt(cnt) 268 | zs = %{99 => 1.28, 269 | 98 => 1.645, 270 | 95 => 1.96, 271 | 90 => 2.33, 272 | 80 => 2.58} 273 | v = per_n.(true_res) 274 | # IO.inspect "per n ->" 275 | # IO.puts " #{inspect per_n_red}" 276 | # IO.puts " stats=#{inspect rst}" 277 | # IO.puts " vars=#{inspect variances}" 278 | # IO.puts " N=#{cnt} MIN=#{ft mi} MAX=#{ft ma}" 279 | # IO.puts " R=#{ft per_n.(true_res)}" 280 | # IO.puts " μ=#{ft mean}" 281 | # IO.puts " v=#{ft variance, 4} σ=#{ft sdev, 4}" 282 | # IO.puts " z=#{ft std_err}" 283 | sigs = Enum.filter_map(zs, fn {_, zv} -> 284 | abs(v - mean) <= (std_err * zv) 285 | end, fn {k, _} -> to_string(k) end) 286 | # IO.puts " sigs=#{inspect sigs}" 287 | List.to_tuple(Tuple.to_list(true_res) ++ [sigs]) 288 | end 289 | defp last([h]), do: h 290 | defp last([_|t]), do: last(t) 291 | 292 | def run_tests(duration, tests, mesg_sizes) do 293 | Enum.map(tests, fn {name, test, sized?} -> 294 | %{name: name, sized: sized?, 295 | results: if sized? do 296 | Enum.map(mesg_sizes, 297 | fn sz -> 298 | res = predict_test(duration, name, test, sz) 299 | IO.puts "Done with #{name}/#{sz}..." 300 | {sz, summarize(res)} 301 | end) 302 | else 303 | res = predict_test(duration, name, test, 0) 304 | IO.puts "Done with #{name}..." 305 | [{0, summarize(res)}] 306 | end 307 | } 308 | end) 309 | end 310 | 311 | def format_now do 312 | 313 | local = :calendar.local_time() 314 | {{yyyy,mm,dd},{hour,min,sec}} = local 315 | res = :io_lib. 316 | format("~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B", 317 | [yyyy, mm, dd, hour, min, sec]) 318 | res = IO.chardata_to_string(res) 319 | utc = :calendar.universal_time() 320 | offs = round ((:calendar.datetime_to_gregorian_seconds(local) - 321 | :calendar.datetime_to_gregorian_seconds(utc)) / 60) 322 | if offs != 0 do 323 | suf = if offs < 0, do: (offs = -offs; ?-), else: ?+ 324 | res = res <> 325 | IO.chardata_to_string(:io_lib.format("~c~2..0B", [suf, div(offs, 60)])) 326 | mins = rem(offs, 60) 327 | if mins, 328 | do: res = res <> IO.chardata_to_string(:io_lib.format(":~2..0B", 329 | [mins])) 330 | else 331 | res = res <> "Z" 332 | end 333 | res 334 | end 335 | 336 | def ft(t, ndigs \\ 2) do 337 | Float.round(t / 1.0, ndigs) 338 | end 339 | def mu2s(t), do: t / 1_000_000 340 | def s2mu(t), do: t * 1_000_000 341 | defp humanize_bytes(b) do 342 | units = 1_000_000 343 | b = b / units 344 | "#{ft b}mb" 345 | end 346 | def through(sized?, chunks, msg_size, total_micros) do 347 | msg_per_t = chunks / mu2s(total_micros) 348 | byte_per_t = (chunks * msg_size) / mu2s(total_micros) 349 | t_per_op = if chunks != 0, do: total_micros / chunks, else: 0 350 | bps = (sized? && " #{humanize_bytes byte_per_t}/sec") || "" 351 | "#{ft msg_per_t}msg/sec #{ft t_per_op}μs/op #{bps}" 352 | end 353 | end 354 | 355 | default_duration = 3.0#.0 356 | {tot, by_test} = :timer.tc(fn -> Bench.run_tests(default_duration) end) 357 | IO.puts "## Begin Bench" 358 | IO.puts "Run-on: #{Bench.format_now}" 359 | IO.puts "Duration-seconds: #{Bench.ft Bench.mu2s tot}" 360 | Enum.each(by_test, fn %{name: name, sized: sized?, results: results} -> 361 | Enum.map(results, fn x -> 362 | {size, {time, num_chunks, mem, extras}} = x 363 | IO.puts("#{name}#{(sized? && "-" <> to_string(size)) || ""}: T=#{Bench.ft Bench.mu2s time}: N=#{num_chunks} #{Bench.through(sized?, num_chunks, size, time)}") 364 | conf = if Enum.count(extras) != 0, 365 | do: Enum.map(extras, &(&1 <> "% ")), 366 | else: "(NONE)" 367 | IO.puts("## Confidence: #{conf}") 368 | IO.puts "## mem=#{inspect mem.used} gcmem=#{inspect mem.used_pre_gc}" 369 | {size, time} 370 | end) 371 | # IO.puts "results for #{name}: #{inspect xform}" 372 | end) 373 | 374 | -------------------------------------------------------------------------------- /examples/pub.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | alias Nats.Client 3 | 4 | defmodule Pub do 5 | def pub(con, subject, msg, tot) do 6 | IO.puts "publishing #{tot} messages..." 7 | pub(con, subject, msg, 0, tot) 8 | end 9 | def pub(_, _, _, tot, tot), do: true 10 | def pub(con, subject, msg, sofar, tot) do 11 | sofar = sofar + 1 12 | Client.pub(con, subject, "#{sofar}: #{msg}") 13 | pub(con, subject, msg, sofar, tot) 14 | end 15 | end 16 | 17 | subject = "elixir.subject" 18 | msg = "hello NATS world" 19 | IO.puts "starting NATS..." 20 | {:ok, pid} = Client.start_link 21 | Pub.pub(pid, subject, msg, 10) 22 | Client.stop(pid) 23 | IO.puts "exiting..." 24 | -------------------------------------------------------------------------------- /examples/sub.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | alias Nats.Client 3 | 4 | defmodule Sub do 5 | def receive_loop(pid), do: receive_loop(pid, :infinite) 6 | def receive_loop(pid, how_many) do 7 | IO.puts "starting; receiving #{how_many} messages..." 8 | receive_loop1(pid, how_many) 9 | end 10 | defp receive_loop1(_, 0), do: true 11 | defp receive_loop1(pid, num_left) do 12 | receive do 13 | w -> IO.puts("received NATS message: #{inspect(w)}") 14 | continue(pid, num_left) 15 | end 16 | end 17 | defp continue(pid, :infinite), do: receive_loop1(pid, :infinite) 18 | defp continue(pid, num), do: receive_loop1(pid, num - 1) 19 | end 20 | 21 | subject_pat = ">" 22 | IO.puts "starting NATS nats link..." 23 | {:ok, pid} = Client.start_link 24 | receive do after 500 -> true end 25 | IO.puts "subscribing..." 26 | ref = Client.sub(pid, self(), subject_pat); 27 | Sub.receive_loop(pid) 28 | Client.unsub(pid, ref) 29 | IO.puts "exiting..." 30 | -------------------------------------------------------------------------------- /lib/nats.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | defmodule Nats do 3 | @moduledoc """ 4 | NATS framework for Elixir 5 | """ 6 | end 7 | -------------------------------------------------------------------------------- /lib/nats/client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | defmodule Nats.Client do 3 | use GenServer 4 | require Logger 5 | 6 | alias Nats.Connection 7 | 8 | @default_host "127.0.0.1" 9 | @default_port 4222 10 | @default_timeout 5000 11 | 12 | @default_opts %{ tls_required: false, 13 | auth: %{}, # "user" => "user", "pass" => "pass"}, 14 | verbose: false, 15 | timeout: @default_timeout, 16 | host: @default_host, port: @default_port, 17 | socket_opts: [:binary, active: :once], 18 | ssl_opts: []} 19 | @start_state %{ conn: nil, opts: %{}, status: :starting, why: nil, 20 | subs_by_pid: %{}, 21 | subs_by_sid: %{}, 22 | opts: @default_opts, 23 | next_sid: 0} 24 | 25 | def start_link(opts \\ %{}) 26 | def start_link(opts) when is_map(opts) do 27 | GenServer.start_link(__MODULE__, opts) 28 | end 29 | def start_link(name) do 30 | start_link(name, %{}) 31 | end 32 | def start_link(name, opts) do 33 | GenServer.start_link(__MODULE__, opts, name: name) 34 | end 35 | 36 | def start(opts \\ %{}) 37 | def start(opts) when is_map(opts) do 38 | GenServer.start(__MODULE__, opts) 39 | end 40 | def start(name) do 41 | start(name, %{}) 42 | end 43 | def start(name, opts) when is_map(opts) do 44 | GenServer.start(__MODULE__, opts, name: name) 45 | end 46 | 47 | def init(orig_opts) do 48 | # IO.puts "init! #{inspect(opts)}" 49 | state = @start_state 50 | opts = Map.merge(state.opts, orig_opts) 51 | parent = self() 52 | case Connection.start_link(parent, opts) do 53 | {:ok, x} when is_pid(x) -> 54 | receive do 55 | {:connected, ^x } -> 56 | {:ok, %{state | conn: x, status: :connected, opts: opts}} 57 | after opts.timeout -> 58 | {:stop, "timeout connecting to NATS"} 59 | end 60 | other -> {:error, "unable to start connection link", other} 61 | end 62 | end 63 | 64 | def handle_info({:msg, subject, sid, reply, what}, 65 | state = %{ subs_by_sid: subs_by_sid, 66 | status: client_status}) 67 | when client_status != :closed do 68 | pid = Map.get(subs_by_sid, sid) 69 | if pid, do: send pid, {:msg, {sid, pid}, subject, reply, what} 70 | {:noreply, state} 71 | end 72 | # ignore messages we get after being closed... 73 | def handle_info({:msg, _subject, _sid, _reply, _what}, state) do 74 | {:noreply, state} 75 | end 76 | def terminate(reason, state = %{status: status}) when status != :closed do 77 | # Logger.log :info, "terminating client: #{inspect reason}: #{inspect state}" 78 | :ok = Connection.stop(state.conn) 79 | state = %{state | conn: nil, status: :closed} 80 | super(reason, state) 81 | end 82 | defp send_cmd(state, cmd), 83 | do: send_cmd(state, Nats.Parser.encode(cmd), false, nil) 84 | defp send_cmd(state, cmd, flush?, from), 85 | do: GenServer.cast(state.conn, 86 | {:write_flush, cmd, flush?, from}) 87 | # return an error for any calls after we are closed! 88 | def handle_call(_call, _from, state = %{status: :closed}) do 89 | {:reply, {:error, "connection closed"}, state} 90 | end 91 | def handle_call({:unsub, ref = {sid, who}, afterReceiving}, _from, 92 | state = %{subs_by_sid: subs_by_sid, 93 | subs_by_pid: subs_by_pid}) do 94 | case Map.get(subs_by_sid, sid, nil) do 95 | ^who -> 96 | other_subs_for_pid = Map.delete(Map.get(subs_by_pid, who), sid) 97 | 98 | new_subs_by_pid = case Map.size(other_subs_for_pid) > 0 do 99 | true -> Map.put(subs_by_pid, who, other_subs_for_pid) 100 | _else -> Map.delete(subs_by_pid, who) 101 | end 102 | 103 | new_state = %{state | 104 | subs_by_sid: Map.delete(subs_by_sid, sid), 105 | subs_by_pid: new_subs_by_pid} 106 | send_cmd(new_state, {:unsub, sid, afterReceiving}) 107 | {:reply, :ok, new_state} 108 | nil -> 109 | {:reply, {:error, {"not subscribed", ref}}, state} 110 | _ -> 111 | {:reply, {:error, {"wrong subscriber process", ref}}, state} 112 | end 113 | end 114 | def handle_call({:sub, who, subject, queue}, _from, 115 | state = %{subs_by_sid: subs_by_sid, 116 | subs_by_pid: subs_by_pid, 117 | next_sid: next_sid}) do 118 | sid = Integer.to_string(next_sid) 119 | m = Map.get(subs_by_pid, who, %{}) 120 | ref = {sid, who} 121 | m = Map.put(m, sid, ref) 122 | subs_by_pid = Map.put(subs_by_pid, who, m) 123 | subs_by_sid = Map.put(subs_by_sid, sid, who) 124 | state = %{state | 125 | subs_by_sid: subs_by_sid, 126 | subs_by_pid: subs_by_pid, 127 | next_sid: next_sid + 1} 128 | send_cmd(state, {:sub, subject, queue, sid}) 129 | # IO.puts "subscribed!! #{inspect(state)}" 130 | {:reply, {:ok, {sid, who}}, state} 131 | end 132 | def handle_call({:cmd, encoded, flush?} , from, 133 | state = %{status: client_status}) 134 | when client_status != :closed do 135 | # cast to the connection, and let it respond 136 | send_cmd(state, encoded, flush?, from) 137 | {:noreply, state} 138 | end 139 | 140 | def pub(self, subject, what) do pub(self, subject, nil, what) end 141 | def pub(self, subject, reply, what), 142 | do: GenServer.call(self, {:cmd, 143 | Nats.Parser.encode({:pub, subject, 144 | reply, what}), 145 | false}) 146 | def sub(self, who, subject, queue \\ nil), 147 | do: GenServer.call(self, {:sub, who, subject, queue}) 148 | def unsub(self, ref, afterReceiving \\ nil), 149 | do: GenServer.call(self, {:unsub, ref, afterReceiving}) 150 | def flush(self, timeout \\ :infinity), 151 | do: GenServer.call(self, {:cmd, nil, true}, timeout) 152 | def stop(self, timeout \\ :infinity) do 153 | flush(self, timeout) 154 | GenServer.stop(self) 155 | end 156 | end 157 | 158 | -------------------------------------------------------------------------------- /lib/nats/connection.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Apcera Inc. All rights reserved. 2 | defmodule Nats.Connection do 3 | use GenServer 4 | require Logger 5 | 6 | @start_state %{state: :want_connect, 7 | make_active: true, 8 | sock: nil, 9 | worker: nil, 10 | send_fn: &:gen_tcp.send/2, 11 | close_fn: &:gen_tcp.close/1, 12 | # do we start up a separate aagent to do sends? 13 | sep_writer: true, 14 | opts: %{}, 15 | ps: nil, 16 | ack_ref: nil, 17 | writer_pid: nil} 18 | 19 | defp format(x) when is_binary(x), do: x 20 | defp format(x) when is_list(x), do: Enum.join(Enum.map(x, &(format(&1))), " ") 21 | defp format(x), do: inspect(x) 22 | 23 | defp _log(level, what) do 24 | # IO.puts ((is_list(what) && Enum.join(Enum.map(what, &format(&1)), ": ")) || format(what)) 25 | Logger.log level, fn -> 26 | ((is_list(what) && Enum.join(Enum.map(what, &format/1), ": ")) 27 | || format(what)) 28 | end 29 | end 30 | # defp debug_log(what), do: _log(:debug, what) 31 | defp err_log(what), do: _log(:error, what) 32 | defp info_log(what), do: _log(:info, what) 33 | 34 | def start_link(worker, opts) when is_map (opts) do 35 | # IO.puts "opts -> #{inspect(opts)}" 36 | state = @start_state 37 | state = %{state | 38 | worker: worker, 39 | opts: opts, 40 | ps: nil, 41 | } 42 | # debug_log "starting link" 43 | GenServer.start_link(__MODULE__, state) 44 | end 45 | 46 | def init(state) do 47 | # IO.puts "client-> #{inspect(state)}" 48 | opts = state.opts 49 | host = opts.host 50 | port = opts.port 51 | hpstr = "#{host}:#{port}" 52 | info_log "connecting to #{hpstr}..." 53 | case :gen_tcp.connect(to_char_list(host), port, 54 | opts.socket_opts, 55 | opts.timeout) do 56 | {:ok, connected} -> 57 | state = %{state | 58 | sock: connected, 59 | state: :want_info} 60 | # FIXME: jam: required? 61 | :ok = :inet.setopts(connected, state.opts.socket_opts) 62 | #IO.puts "connected to nats: #{inspect(state)}" 63 | info_log "connected" 64 | if state.sep_writer do 65 | sender = &state.send_fn.(connected, &1) 66 | state = %{state | writer_pid: spawn_link(fn -> write_loop(sender, [], 0, :infinity) end)} 67 | end 68 | {:ok, state } 69 | {:error, reason} -> 70 | why = "error connecting to #{hpstr}: #{reason}" 71 | err_log why 72 | {:stop, why} 73 | end 74 | end 75 | 76 | defp wait_writer(writer, state) do 77 | if Process.alive?(writer) do 78 | receive do 79 | :closed -> :ok 80 | after 1_000 -> 81 | # FIXME: jam: hook up to sup tree 82 | # err_log "#{inspect self()} didn't get :closed ack back from writer..." 83 | err_log ["waiting for waiter ack...", writer, state] 84 | wait_writer(writer, state) 85 | end 86 | else 87 | receive do 88 | :closed -> :ok 89 | after 0 -> 90 | err_log ["writer died", writer, state] 91 | :ok 92 | end 93 | end 94 | end 95 | def terminate(reason, %{ writer_pid: writer, sep_writer: true } = state) when not is_nil(writer) do 96 | # err_log ["terminating writer", state] 97 | send writer, {:closed, self()} 98 | wait_writer(writer, state) 99 | terminate(reason, %{ state | writer_pid: nil}) 100 | end 101 | def terminate(reason, %{ sock: conn } = state) when not is_nil(conn) do 102 | # _v = state.close_fn.(conn) 103 | # err_log ["closing connection in terminate", 0] 104 | # IO.puts "connection closed in terminate: #{inspect v}" 105 | terminate(reason, %{state | sock: nil}) 106 | end 107 | def terminate(reason, %{ state: s } = state) when s != :closed do 108 | # IO.puts "terminate!!" 109 | super(reason, %{state | state: :closed}) 110 | end 111 | defp handshake_done(state) do 112 | # err_log "handshake done" 113 | send state.worker, {:connected, self() } 114 | {:noreply, %{state | state: :connected, ack_ref: nil}} 115 | end 116 | defp tls_handshake(%{opts: %{ tls_required: true }} = state) do 117 | # start our TLS handshake... 118 | opts = state.opts 119 | info_log ["upgrading to tls with timeout", opts.timeout, "ssl_opts", opts.ssl_opts] 120 | :ok = :inet.setopts(state.sock, [active: true]) 121 | case :ssl.connect(state.sock, opts.ssl_opts, opts.timeout) do 122 | {:ok, port} -> 123 | # debug_log ["tls handshake completed"] 124 | if state.sep_writer do 125 | new_sender = &(:ssl.send(port, &1)) 126 | send state.writer_pid, {:sender_changed, new_sender} 127 | end 128 | {:ok, %{state | sock: port, send_fn: &:ssl.send/2, close_fn: &:ssl.close/1, make_active: false}} 129 | {:error, why} -> 130 | info_log ["tls_handshake failed", why] 131 | {:error, why, state} 132 | end 133 | end 134 | defp tls_handshake(%{opts: %{ tls_required: false }} = state), do: {:ok, state} 135 | def handle_info({:packet_flushed, _, ref}, %{state: :ack_connect, 136 | ack_ref: ref} = state) do 137 | # debug_log "completed handshake: #{inspect res}" 138 | aopts = state.opts.auth 139 | if aopts != nil && Enum.count(aopts) != 0 do 140 | # FIXME: jam: this is a hack, when doing auth (and other?) 141 | # handshakes, they may fail but we don't know within a given 142 | # amount of time, so we need to send a ping and wait for a pong 143 | # or error... 144 | # yuck 145 | send_packet({:write_flush, Nats.Parser.encode({:ping}), 146 | true, nil}, state) 147 | {:noreply, %{state | state: :wait_err_or_pong, ack_ref: nil}} 148 | else 149 | handshake_done(state) 150 | end 151 | end 152 | def handle_info({:tcp_closed, msock}, %{sock: s} = state) when s == msock, 153 | do: nats_err(state, "connection closed") 154 | def handle_info({:ssl_closed, msock}, %{sock: s} = state) when s == msock, 155 | do: nats_err(state, "connection closed") 156 | def handle_info({:tcp_error, msock, reason}, %{sock: s} = state) 157 | when s == msock, 158 | do: nats_err(state, "tcp transport error #{inspect(reason)}") 159 | def handle_info({:tcp_passive, _sock}, state) do 160 | # IO.puts "passiv!!!" 161 | { :noreply, state } 162 | end 163 | def handle_info({:tcp, _sock, data}, state), do: transport_input(state, data) 164 | def handle_info({:ssl, _sock, data}, state), do: transport_input(state, data) 165 | def handle_cast(write_cmd = {:write_flush, _, _, _}, state) do 166 | send_packet(write_cmd, state) 167 | {:noreply, state} 168 | end 169 | defp send_packet({:write_flush, _what, _flush?, ack_mesg} = write_cmd, 170 | %{writer_pid: writer}) when is_pid(writer) do 171 | # debug_log ["send packet", write_cmd] 172 | # debug_log "send packet #{inspect flush?} #{inspect from} #{inspect writer} #{Process.alive?(writer)}" 173 | if Process.alive?(writer) do 174 | send writer, write_cmd 175 | else 176 | # IO.puts "DEAD!!!" 177 | case ack_mesg do 178 | {:packet_flushed, who, _ref} -> send who, ack_mesg 179 | nil -> nil 180 | _ -> GenServer.reply(ack_mesg, :ok) 181 | end 182 | end 183 | :ok 184 | end 185 | # defp send_packet(pack, state), 186 | # do: send_packet({:write_flush, pack, false, nil, nil}, state) 187 | defp send_packet({:write_flush, to_write, _flush?, ack_mesg}, 188 | %{sock: s, send_fn: send_fn, sep_writer: false}) do 189 | # err_log "send_packet-> open=#{s != nil} flush?=#{flush?} write?=#{not is_nil(to_write)} ack?=#{not is_nil(ack_mesg)}" 190 | # err_log "send_packet-> open=#{s != nil} flush?=#{inspect to_write} ack?=#{not is_nil(ack_mesg)}" 191 | if s != nil and to_write != nil do # writer == true do 192 | case to_write do 193 | {:msg, _what_len, what} -> 194 | v = send_fn.(s, what) 195 | # err_log ["wrote some bytes #{inspect what}", ack_mesg, what, v] 196 | end 197 | end 198 | case ack_mesg do 199 | {:packet_flushed, who, _ref} -> send who, ack_mesg 200 | nil -> nil 201 | _ -> GenServer.reply(ack_mesg, :ok) 202 | end 203 | :ok 204 | end 205 | 206 | @max_buff_size (32*1024) 207 | @flush_wait_time 24 208 | @min_flush_wait_time 2 209 | defp write_loop(send_fn, acc, acc_size, howlong) do 210 | # err_log "entering receive: acc_size=#{acc_size} howlong=#{howlong}" 211 | receive do 212 | {:sender_changed, send_fn} -> 213 | # err_log ["sender changed", send_fn] 214 | write_loop(send_fn, acc, acc_size, howlong) 215 | {:closed, waiter} -> 216 | # err_log ["closing", acc_size] 217 | if acc_size != 0, do: send_fn.(acc) 218 | send waiter, :closed 219 | # write_loop(nil, [], 0, :infinity) 220 | {:write_flush, w, flush?, ack_mesg} -> 221 | case w do 222 | {:msg, what_len, what} -> 223 | if what_len != 0 do 224 | acc = [acc|what] 225 | acc_size = acc_size + what_len 226 | if (IO.iodata_length(what) != what_len), do: exit(1) 227 | end 228 | nil -> nil 229 | end 230 | if acc_size != 0 do 231 | if (flush? || acc_size >= @max_buff_size) do 232 | # err_log [">buffer write/flush", acc_size, "/#{flush?}"] 233 | :ok = send_fn.(acc) 234 | # err_log [" @flush_wait_time 241 | x when x <= @min_flush_wait_time -> 242 | 0 243 | x -> div(x, 3) 244 | end 245 | # err_log ["buffer write/flush", acc_size, "/", flush?, " (buffering)", howlong] 246 | end 247 | else 248 | # err_log ["buffer write/flush (nothing to write)", acc_size] 249 | howlong = :infinity 250 | end 251 | # err_log ["ack_mesg -> #{inspect ack_mesg}"] 252 | case ack_mesg do 253 | {:packet_flushed, who, _ref} -> send who, ack_mesg 254 | # err_log "REPLYING to WHO!!" 255 | nil -> nil 256 | # err_log "NOT REPLYING" 257 | other -> GenServer.reply(other, :ok) 258 | # err_log "REPLYING!!" 259 | end 260 | write_loop(send_fn, acc, acc_size, howlong) 261 | after howlong -> 262 | # err_log [">time flush", acc_size] 263 | :ok = send_fn.(acc) 264 | # err_log ["