├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib └── exactor │ ├── behaviour │ ├── strict.ex │ └── tolerant.ex │ ├── delegator.ex │ ├── empty.ex │ ├── gen_server.ex │ ├── helper.ex │ ├── operations.ex │ ├── responders.ex │ ├── strict.ex │ └── tolerant.ex ├── mix.exs ├── mix.lock └── test ├── basic_test.exs ├── cluster_test.exs ├── dynamic_test.exs ├── predefines_test.exs ├── registration_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin/ 2 | /_build/ 3 | /doc/ 4 | /deps/ 5 | /cover/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.2.4 2 | - `@impl GenServer` attribute is included for all autogenerated callback functions. 3 | 4 | # v2.2.3 5 | - removed Elixir 1.4 warnings 6 | - Bugfix: `export: __MODULE__` didn't work in operations macros 7 | 8 | # v2.2.2 9 | - fix compund matches with structs 10 | 11 | # v2.2.1 12 | - Bugfix: support compound matches 13 | - Bugfix: support ExActor macros from other macros 14 | 15 | # v2.2.0 16 | - Support interface & handler specific guards (see [docs](http://hexdocs.pm/exactor/2.2.0/ExActor.Operations.html) for details) 17 | - Remove warnings on Elixir 1.1 18 | 19 | # v2.1.2 20 | - Bugfix: pattern matching on `nil` state didn't work properly 21 | 22 | # v2.1.1 23 | - Relaxed Elixir version dependency to `~> 1.0` 24 | - Bugfix: Proper lineno generation in client code. 25 | - Improve pattern matching handling (see [docs](http://hexdocs.pm/exactor/2.1.1/ExActor.Operations.html#defcall/3) for details) 26 | 27 | # v2.1.0 28 | - Added parameterizable timeout in calls (see [docs](http://hexdocs.pm/exactor/2.1.0/ExActor.Operations.html#defcall/3) for defcall) 29 | 30 | # v2.0.1 31 | - Fixed the docs link in `mix.exs` 32 | 33 | # v2.0.0 34 | 35 | ## New features 36 | - `defstart` macro which simplifies definition of starters. 37 | - `defmulticall` and `defabcast` macros for distributed support. 38 | - `defhandlecall`, `defhandlecast`, and `defhandleinfo` for implementation of handlers only 39 | - default arguments can be specified via `\\` 40 | - `defcall` and `defcast` can be called without specifying the body 41 | - Support for `timeout` and `hibernate` replies. 42 | 43 | ## Breaking changes 44 | - Pattern matching now works on interface functions as well (previously it was done only in handler functions). 45 | - Starter functions are not automatically generated anymore. You can use `defstart` macro to create them. 46 | - When calling `use ExActor.*` options `:initial_state`, and `:starters` are not available anymore. 47 | - `definfo` is renamed to `defhandleinfo` 48 | - Option `export: false` is not available in `defcall` and `defcast`. If you want to implement just handlers, use `defhandle*` 49 | 50 | ## Migration from 1.0 51 | 52 | For migration examples, check [here](https://github.com/sasa1977/con_cache/commit/2ef8151d43dc9c4816814ffa6c4135ff453c59e1) and [here](https://github.com/sasa1977/workex/commit/7f32aad492b25d89d1f5c2f285c624f16a02022e) 53 | 54 | A non exhaustive list of changes you may have to do in your project: 55 | 56 | - Add explicit starters via `defstart`. If you need to support `start` and `start_link`, you can do it like this: 57 | 58 | ```elixir 59 | defstart start(x, y) 60 | defstart start_link(x, y) do 61 | # initialization runs for both start/2 and start_link/2 62 | end 63 | ``` 64 | 65 | - Replace all `definit` with `defstart` if possible, or use body-less `defstart` with an explicit `definit`. 66 | - If you used `initial_state`, set the state explicitly in `defstart`. 67 | - Replace `definfo` with `defhandleinfo`. 68 | 69 | # v1.0.0 70 | - migrated to Elixir 1.0.0 71 | 72 | # v0.7.0 73 | - migrated to Elixir 1.0.0-rc1 74 | - introduced `defcastp` and `defcallp` (see [docs](http://sasa1977.github.io/exactor/ExActor.Operations.html) for more details) 75 | 76 | # v0.6.0 77 | - migrated to Elixir v0.15.0 78 | 79 | # v0.5.0 80 | - migrated to Elixir v0.14.0 81 | - various internal refactorings (API and behavior remain unchanged) 82 | 83 | # v0.4.0 84 | - migrated to Elixir v0.13.3 (backwards incompatible with earlier versions) 85 | - add `name` option to allow dynamic specification of registered alias while starting 86 | - make autogenerated start functions overridable 87 | - add `starters: false` option to allow omitting generated start functions 88 | - add support for `export: {:via, module, name}` 89 | - migrate to new `GenServer` 90 | 91 | # v.0.3.2 92 | - adapted to Elixir v0.13.1 (backwards compatible with 0.13.0) 93 | 94 | # v.0.3.1 95 | - "dummy" empty version due to Hex limitations 96 | 97 | # v.0.3.0 98 | - removed implicit "smart" returns 99 | - removed default `use ExActor` 100 | 101 | # v0.2.1 102 | 103 | - migrated to Elixir v0.13.0 104 | 105 | # v0.2.0 106 | 107 | This version introduces some significant changes. If you don't want to incorporate them, there is a tag for 0.1. Keep in mind that I won't maintain the 0.1, so your best option is to fork it and maintain it yourself. 108 | 109 | On the plus side, the migration to the new version should not be very complicated (unless you're relying on tuple modules support). Here are quick pointers for migrating from old API to the new one: 110 | 111 | ## Predefines 112 | 113 | The point of predefines is to make the decision about default implementations explicit. Earlier, you used `use ExActor` and the default implementation was implicit. Now you have to decide which implementation do you want, or you can easily make your own. If you want to keep the status quo, just use `use ExActor.GenServer`. 114 | 115 | ## Deprecated implicit "smart" returns 116 | 117 | ```elixir 118 | definit do: some_state # deprecated 119 | definit do: initial_state(some_state) 120 | 121 | defcall op, do: response # deprecated 122 | defcall op, do: reply(response) 123 | 124 | defcast op, do: :ok # deprecated 125 | defcast op, do: noreply 126 | 127 | definfo msg, do: :ok # deprecated 128 | definfo msg, do: noreply 129 | ``` 130 | 131 | Beside these "special" responses, you can use standard `gen_server` responses. The reason for this change was again to make things more explicit, and aligned with standard `gen_server` approach. 132 | 133 | ## Dropped tuple modules support 134 | 135 | There's no way around this - if you use ExActor tuple modules support, you need to change the code to classical functional approach, or remain on version 0.1. The tuple modules support was more a hack, to make some code in blog articles more OO-ish. In hindsight, this was a mistake that made the ExActor code more complicated, so I decided to drop it. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013. Saša Jurić 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExActor 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/exactor.svg?style=flat-square)](https://hex.pm/packages/exactor) 4 | [![Hex.pm](https://img.shields.io/hexpm/dt/exactor.svg?style=flat-square)](https://hex.pm/packages/exactor) 5 | 6 | **I don't maintain this project anymore. In hindsight, I don't think it was a good idea in the first place. I haven't been using ExActor myself for years, and I recommend sticking with regular GenServer instead :-)** 7 | 8 | Simplifies implementation of `GenServer` based processes in Elixir. 9 | 10 | `ExActor` helps removing the boilerplate that typically occurs when using `GenServer` behaviour. In particular, `ExActor` can be useful in following situations: 11 | 12 | - `start` function just packs all arguments into a tuple which it forwards to `init/1` via `GenServer.start`. 13 | - Calls and casts interface functions just forward all arguments to the server process via `GenServer.call` and `GenServer.cast`. 14 | - Process is registered and all interface functions rely on this property. 15 | - Some `handle_*` functions don't need the state. 16 | - All handlers need to specify timeout or hibernate. 17 | - More liberal grouping of handler functions (you don't need to group calls and casts separately) 18 | 19 | For other cases, you may need to use plain `GenServer` functions (which can be used together with `ExActor` macros). `ExActor` is not meant to fully replace `GenServer`. It just tries to reduce boilerplate in most common cases. 20 | 21 | If you're new to Elixir, Erlang, and OTP, and are not familiar on how `GenServer` works, I strongly suggest you learn about it first. It's really not that hard, and you can use [Elixir docs](https://hexdocs.pm/elixir/GenServer.html) as the starting point. It's also worth going through [Mix/OTP getting started guide](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). 22 | Once you're familiar with `GenServer`, you can consider using `ExActor` to reduce the boilerplate. 23 | 24 | Online documentation is available [here](http://hexdocs.pm/exactor). 25 | 26 | The stable package is available on [hex](https://hex.pm/packages/exactor). 27 | 28 | ## Basic usage 29 | 30 | Be sure to include a dependency in your `mix.exs`: 31 | 32 | ```elixir 33 | deps: [{:exactor, "~> 2.2.4", warn_missing: false}, ...] 34 | ``` 35 | 36 | `ExActor` is a compile-time dependency only. No need to add it into the list of dependent applications. All code transformations are performed at compile time. If you're using exrm to build OTP releases, you may need to supply the `warn_missing: false` option to prevent warnings about a missing application dependency. 37 | 38 | 39 | ```elixir 40 | defmodule Calculator do 41 | use ExActor.GenServer 42 | 43 | defstart start_link, do: initial_state(0) 44 | 45 | defcast inc(x), state: state, do: new_state(state + x) 46 | defcast dec(x), state: state, do: new_state(state - x) 47 | 48 | defcall get, state: state, do: reply(state) 49 | 50 | defcast stop, do: stop_server(:normal) 51 | end 52 | ``` 53 | 54 | This module be used in a typical fashion: 55 | 56 | ```elixir 57 | {:ok, calculator} = Calculator.start_link 58 | Calculator.inc(calculator, 10) 59 | Calculator.dec(calculator, 3) 60 | Calculator.get(calculator) 61 | 62 | Calculator.stop(calculator) 63 | ``` 64 | 65 | The module definition above is translated at compile-time into something like: 66 | 67 | ```elixir 68 | defmodule Calculator do 69 | use GenServer 70 | 71 | def start_link, do: GenServer.start_link(__MODULE__, nil) 72 | def stop(pid), do: GenServer.cast(pid, :stop) 73 | 74 | def inc(pid, x), do: GenServer.cast(pid, {:inc, x}) 75 | def dec(pid, x), do: GenServer.cast(pid, {:dec, x}) 76 | def get(pid), do: GenServer.call(pid, :get) 77 | 78 | def init(_), do: {:ok, 0} 79 | 80 | def handle_cast({:inc, x}, state), do: {:noreply, state + x} 81 | def handle_cast({:dec, x}, state), do: {:noreply, state - x} 82 | def handle_cast(:stop, state), do: {:stop, :normal, state} 83 | 84 | def handle_call(:get, _, state), do: {:reply, state, state} 85 | end 86 | ``` 87 | 88 | A bit more complex and feature rich example is presented [here](#a-more-involved-example). 89 | 90 | ## Predefines 91 | 92 | To use `ExActor` macros, you must choose a predefine module and `use` it into your own module. A predefine is an `ExActor` module that provides some default implementations for `GenServer` callbacks. 93 | 94 | Following predefines are currently provided: 95 | 96 | * `ExActor.GenServer` - All `GenServer` callbacks are provided by `GenServer` from Elixir standard library. 97 | * `ExActor.Strict` - All `GenServer` callbacks are provided. The default implementations for all except `code_change` and `terminate` will cause the server to be stopped. 98 | * `ExActor.Tolerant` - All `GenServer` callbacks are provided. The default implementations ignore all messages without stopping the server. 99 | * `ExActor.Empty` - No default implementation for `GenServer` callbacks are provided. 100 | 101 | It is up to you to decide which predefine you want to use. See online docs for detailed description. 102 | You can also build your own predefine. Refer to the source code of the existing ones as a template. 103 | 104 | ## Process registration 105 | 106 | ```elixir 107 | defmodule Calculator do 108 | use ExActor.GenServer, export: :calculator 109 | 110 | # you can also use via, and global 111 | # use ExActor.GenServer, export: {:global, :calculator} 112 | # use ExActor.GenServer, export: {:via, :gproc, :calculator} 113 | 114 | ... 115 | end 116 | 117 | # all functions defined via defcall and defcast will take 118 | # advantage of the export option 119 | Calculator.start 120 | Calculator.inc(5) 121 | Calculator.get 122 | ``` 123 | 124 | ## Handling of return values 125 | 126 | ```elixir 127 | defstart start_link, do: initial_state(arg) 128 | 129 | defcall foo, do: set_and_reply(new_state, response) 130 | defcast bar, do: new_state(new_state) 131 | 132 | defhandleinfo :stop, do: stop(normal) 133 | defhandleinfo _, do: noreply 134 | ``` 135 | 136 | See [here](https://hexdocs.pm/exactor/ExActor.Responders.html#summary) for detailed list. 137 | 138 | ## Simplified initialization 139 | 140 | ```elixir 141 | defstart start_link(x, y, z) do 142 | # Generates start_link function and `init/1` clause. The code runs in init/1 function. 143 | initial_state(x + y + z) 144 | end 145 | ``` 146 | 147 | By default, corresponding `GenServer` function is deduced from the function name, so you can use either `start_link` or `start`. If you want a custom function name, you need to provide explicit `:link` option: 148 | 149 | ```elixir 150 | defstart my_start(...), link: true do 151 | ... 152 | end 153 | ``` 154 | 155 | ### Dynamic start parameters 156 | 157 | ```elixir 158 | defmodule Calculator do 159 | use ExActor.GenServer 160 | 161 | # gen_server_opts: :runtime will add additional argument to the start 162 | # function. This argument will be passed as options to the `GenServer` start 163 | # function. 164 | defstart start_link(x), gen_server_opts: :runtime, do: ... 165 | end 166 | 167 | # You can pass `name: :foo` due to `gen_server_opts: :runtime` option in the starter 168 | Calculator.start_link(x, name: :foo) 169 | 170 | # Or in the supervisor specification: 171 | Supervisor.start_link( 172 | [ 173 | worker(Calculator, [x, [name: :foo]]), 174 | # ... 175 | ] 176 | ) 177 | ``` 178 | 179 | ## Cluster support 180 | 181 | ```elixir 182 | defmodule Database do 183 | use ExActor.GenServer, export: :database 184 | 185 | defabcast store(key, value), do: ... 186 | defmulticall get(key), do: ... 187 | end 188 | 189 | # called on all nodes 190 | Database.store(key, value) 191 | Database.get(key) 192 | 193 | # called on specified nodes 194 | Database.store(some_nodes, key, value) 195 | Database.get(some_nodes, key) 196 | ``` 197 | 198 | ## Private interface functions 199 | 200 | There are private versions available in form of `defstartp`, `defcallp`, `defcastp`, `defmulticallp`, and `defabcastp`. The only difference here is that interface functions are defined with `defp`. This can help you when you need to include some custom logic before or after the operation. See [here](#a-more-involved-example) for an example. 201 | 202 | ## Pattern matching 203 | 204 | ```elixir 205 | defstart start_link(1), do: 206 | defstart start_link(2), do: 207 | defstart start_link(x), when: x < 5, do: 208 | 209 | defcall a(1), do: ... 210 | defcall a(2), do: ... 211 | defcall a(x), state: 1, do: ... 212 | defcall a(x), when: x > 1, do: ... 213 | defcall a(_), do: ... 214 | 215 | defhandleinfo :msg, state: {...}, when: ..., do: ... 216 | ``` 217 | 218 | All matches take place on both interface and handler functions. 219 | 220 | Default arguments are also supported: 221 | 222 | ```elixir 223 | defcall inc(x \\ 1), ... 224 | ``` 225 | 226 | In this case, we'll end up with two `inc` interface functions, and a single `handle_call` function that matches on `{:inc, x}`. 227 | 228 | ## Implementing just handlers 229 | 230 | Can be useful do handle messages: 231 | 232 | ```elixir 233 | defhandleinfo :some_message, do: 234 | defhandleinfo :another_message, state: ..., do: 235 | ``` 236 | 237 | Or to pattern match on the state: 238 | 239 | ```elixir 240 | # Body-less clause defines only the interface function 241 | defcast inc 242 | 243 | # Handle clauses pattern match on the state 244 | defhandlecast inc, state: state, when: is_number(state), 245 | do: new_state(state + 1) 246 | 247 | defhandlecast inc, do: new_state(0) 248 | ``` 249 | 250 | ## Using from 251 | 252 | ```elixir 253 | defcall my_request(...), from: from do 254 | ... 255 | spawn_link(fn -> 256 | ... 257 | GenServer.reply(from, ...) 258 | end) 259 | 260 | noreply 261 | end 262 | ``` 263 | 264 | ## Server-wide timeouts and hibernate 265 | 266 | Timeout: 267 | 268 | ```elixir 269 | defstart ... do 270 | # Instructs `ExActor` to include timeout in all responses made via responder 271 | # macros, such as `new_state` or `noreply`. As the result, a `:timeout` message 272 | # will be sent to the server after specified inactivity time. 273 | timeout_after(:timer.seconds(10)) 274 | end 275 | ``` 276 | 277 | Hibernation: 278 | 279 | ```elixir 280 | defstart ... do 281 | # Instructs `ExActor` to include `:hibernate` in all responses made via responder 282 | # macros, such as `new_state` or `noreply`. 283 | hibernate 284 | end 285 | ``` 286 | 287 | ## Dynamic code generation friendliness 288 | 289 | May be useful if you need to dynamically generate your requests. For example, if calls/casts simply delegate to some module, we could do something like: 290 | 291 | ```elixir 292 | defmodule DynActor do 293 | use ExActor.GenServer 294 | 295 | for op <- [:op1, :op2] do 296 | defcall unquote(op), state: state do 297 | SomeModule.unquote(op)(state) 298 | end 299 | end 300 | end 301 | ``` 302 | 303 | ## A more involved example 304 | 305 | In the following code, `ExActor` is used to implement a simple ETS based cache with basic cluster replication: 306 | 307 | ```elixir 308 | defmodule Cache do 309 | use ExActor.GenServer 310 | 311 | # Starter allows clients to specify cache name. Notice how this is used 312 | # in `gen_server_opts` as a registered name of the server. 313 | defstart start(cache_name, timeout_after \\ :infinity), 314 | gen_server_opts: [name: cache_name] 315 | do 316 | # Specifies timeout which will be used in all handler responses 317 | timeout_after(timeout_after) 318 | :ets.new(cache_name, [:named_table, :set, :protected]) 319 | initial_state(cache_name) 320 | end 321 | 322 | # Looks up the cache in the client process 323 | def get(cache_name, key) do 324 | case :ets.lookup(cache_name, key) do 325 | [{^key, value}] -> value 326 | [] -> nil 327 | end 328 | end 329 | 330 | # An example of a more complex interface function. A get attempt is made 331 | # in the client process, and then we optionally issue a private call request. 332 | def get_or_create(cache_name, key, fun) do 333 | case get(cache_name, key) do 334 | nil -> server_get_or_create(cache_name, key, fun) 335 | existing -> existing 336 | end 337 | end 338 | 339 | # Private call request used from `get_or_create` 340 | defcallp server_get_or_create(key, fun), state: cache_name do 341 | case get(cache_name, key) do 342 | nil -> 343 | new = fun.() 344 | store(cache_name, key, new) 345 | # Makes a distributed call to all other nodes 346 | set(Node.list, cache_name, key, new) 347 | new 348 | 349 | existing -> existing 350 | end 351 | |> reply 352 | end 353 | 354 | # Distributed setter - stores to all nodes in the cluster 355 | defmulticall set(key, value), state: cache_name do 356 | store(cache_name, key, value) 357 | reply(:ok) 358 | end 359 | 360 | defp store(cache_name, key, value) do 361 | :ets.insert(cache_name, {key, value}) 362 | end 363 | 364 | # Stops the server on timeout message 365 | defhandleinfo :timeout, do: stop_server(:normal) 366 | defhandleinfo _, do: noreply 367 | end 368 | ``` 369 | -------------------------------------------------------------------------------- /lib/exactor/behaviour/strict.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Behaviour.Strict do 2 | @moduledoc false 3 | 4 | @doc false 5 | defmacro __using__(_) do 6 | quote location: :keep do 7 | @behaviour GenServer 8 | 9 | @doc false 10 | def init(args) do 11 | { :stop, :badinit } 12 | end 13 | 14 | @doc false 15 | def handle_call(request, _from, state) do 16 | { :stop, { :bad_call, request }, state } 17 | end 18 | 19 | @doc false 20 | def handle_info(msg, state) do 21 | { :stop, { :bad_info, msg }, state } 22 | end 23 | 24 | @doc false 25 | def handle_cast(msg, state) do 26 | { :stop, { :bad_cast, msg }, state } 27 | end 28 | 29 | @doc false 30 | def terminate(_reason, _state) do 31 | :ok 32 | end 33 | 34 | @doc false 35 | def code_change(_old, state, _extra) do 36 | { :ok, state } 37 | end 38 | 39 | defoverridable [init: 1, handle_call: 3, handle_info: 2, 40 | handle_cast: 2, terminate: 2, code_change: 3] 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/exactor/behaviour/tolerant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Behaviour.Tolerant do 2 | @moduledoc false 3 | 4 | @doc false 5 | defmacro __using__(_) do 6 | quote location: :keep do 7 | @behaviour GenServer 8 | 9 | @doc false 10 | def init(args) do 11 | { :ok, args } 12 | end 13 | 14 | @doc false 15 | def handle_call(_request, _from, state) do 16 | { :noreply, state } 17 | end 18 | 19 | @doc false 20 | def handle_info(_msg, state) do 21 | { :noreply, state } 22 | end 23 | 24 | @doc false 25 | def handle_cast(_msg, state) do 26 | { :noreply, state } 27 | end 28 | 29 | @doc false 30 | def terminate(_reason, _state) do 31 | :ok 32 | end 33 | 34 | @doc false 35 | def code_change(_old, state, _extra) do 36 | { :ok, state } 37 | end 38 | 39 | defoverridable [init: 1, handle_call: 3, handle_info: 2, 40 | handle_cast: 2, terminate: 2, code_change: 3] 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/exactor/delegator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Delegator do 2 | @moduledoc """ 3 | Provides `delegate_to/2` macro that can be used to simplify cases when 4 | call/cast operations delegate to another module. 5 | """ 6 | 7 | @doc """ 8 | Creates wrapper operations around the `target_module`. 9 | 10 | For example: 11 | 12 | defmodule HashDictServer do 13 | use ExActor.GenServer 14 | import ExActor.Delegator 15 | 16 | defstart start_link, do: initial_state(HashDict.new) 17 | 18 | delegate_to HashDict do 19 | query get/2 20 | trans put/3 21 | end 22 | end 23 | 24 | This is the same as: 25 | 26 | defmodule HashDictServer do 27 | use ExActor.GenServer 28 | 29 | defstart start_link, do: initial_state(HashDict.new) 30 | 31 | defcall get(k), state: state do 32 | HashDict.get(state, k) 33 | |> reply 34 | end 35 | 36 | defcast put(k, v), state:state do 37 | HashDict.put(state, k, v) 38 | |> new_state 39 | end 40 | end 41 | """ 42 | defmacro delegate_to(target_module, opts) do 43 | statements(opts[:do]) 44 | |> Enum.map(&(parse_instruction(target_module, &1))) 45 | end 46 | 47 | defp statements({:__block__, _, statements}), do: statements 48 | defp statements(statement), do: [statement] 49 | 50 | 51 | defp parse_instruction(target_module, {:init, _, _}) do 52 | quote do 53 | definit do 54 | unquote(target_module).new 55 | |> initial_state 56 | end 57 | end 58 | end 59 | 60 | defp parse_instruction(target_module, {:query, _, [{:/, _, [{fun, _, _}, arity]}]}) do 61 | make_delegate(:defcall, fun, arity, 62 | quote do 63 | unquote(forward_call(target_module, fun, arity)) 64 | |> reply 65 | end 66 | ) 67 | end 68 | 69 | defp parse_instruction(target_module, {:trans, _, [{:/, _, [{fun, _, _}, arity]}]}) do 70 | make_delegate(:defcast, fun, arity, 71 | quote do 72 | unquote(forward_call(target_module, fun, arity)) 73 | |> new_state 74 | end 75 | ) 76 | end 77 | 78 | 79 | defp make_delegate(type, fun, arity, code) do 80 | quote do 81 | unquote(type)( 82 | unquote(fun)(unquote_splicing(make_args(arity))), 83 | state: state, 84 | do: unquote(code) 85 | ) 86 | end 87 | end 88 | 89 | 90 | defp forward_call(target_module, fun, arity) do 91 | full_args = [quote(do: state) | make_args(arity)] 92 | 93 | quote do 94 | unquote(target_module).unquote(fun)(unquote_splicing(full_args)) 95 | end 96 | end 97 | 98 | 99 | defp make_args(arity) when arity > 0 do 100 | 1..arity 101 | |> Enum.map(&{:"arg#{&1}", [], nil}) 102 | |> tl 103 | end 104 | end -------------------------------------------------------------------------------- /lib/exactor/empty.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Empty do 2 | @moduledoc """ 3 | Empty predefine. Imports all ExActor macros, but doesn't provide any default 4 | implementation. The declaring module must define all required functions 5 | of the `gen_server` behaviour. 6 | 7 | Example: 8 | 9 | defmodule MyServer do 10 | use ExActor.Empty 11 | 12 | # define all gen_server required functions 13 | end 14 | 15 | # Locally registered name: 16 | use ExActor.Empty, export: :some_registered_name 17 | 18 | # Globally registered name: 19 | use ExActor.Empty, export: {:global, :global_registered_name} 20 | """ 21 | defmacro __using__(opts) do 22 | quote do 23 | @behaviour GenServer 24 | 25 | @generated_funs MapSet.new 26 | 27 | import ExActor.Operations 28 | import ExActor.Responders 29 | 30 | unquote(ExActor.Helper.init_generation_state(opts)) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/exactor/gen_server.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.GenServer do 2 | @moduledoc """ 3 | Predefine that relies on `GenServer` provided by Elixir standard 4 | lib. All ExActor macros are imported. 5 | 6 | Example: 7 | 8 | defmodule MyServer do 9 | use ExActor.GenServer 10 | ... 11 | end 12 | 13 | # Locally registered name: 14 | use ExActor.GenServer, export: :some_registered_name 15 | 16 | # Globally registered name: 17 | use ExActor.GenServer, export: {:global, :global_registered_name} 18 | """ 19 | defmacro __using__(opts) do 20 | quote do 21 | use GenServer 22 | 23 | @generated_funs MapSet.new 24 | 25 | import ExActor.Operations 26 | import ExActor.Responders 27 | 28 | unquote(ExActor.Helper.init_generation_state(opts)) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/exactor/helper.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Helper do 2 | @moduledoc false 3 | 4 | def inject_to_module(quoted, module, env) do 5 | Module.eval_quoted( 6 | module, quoted, [], 7 | aliases: env.aliases, 8 | requires: env.requires, 9 | functions: env.functions, 10 | macros: env.macros, 11 | file: env.file, 12 | line: env.line 13 | ) 14 | end 15 | 16 | def init_generation_state(opts) do 17 | quote do 18 | @exactor_global_options unquote(opts) 19 | end 20 | end 21 | 22 | def state_var do 23 | Macro.var(:state_var, ExActor) 24 | end 25 | end -------------------------------------------------------------------------------- /lib/exactor/operations.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Operations do 2 | @moduledoc """ 3 | Macros that can be used for simpler definition of `GenServer` operations 4 | such as casts or calls. 5 | 6 | For example: 7 | 8 | defcall request(x, y), state: state do 9 | set_and_reply(state + x + y, :ok) 10 | end 11 | 12 | will generate two functions: 13 | 14 | def request(server, x, y) do 15 | GenServer.call(server, {:request, x, y}) 16 | end 17 | 18 | def handle_call({:request, x, y}, _, state) do 19 | {:reply, :ok, state + x + y} 20 | end 21 | 22 | There are various helper macros available for specifying responses. For more details 23 | see `ExActor.Responders`. 24 | 25 | ## Request format (passed to `handle_call/3` and `handle_cast/2`) 26 | 27 | - no arguments -> `:my_request` 28 | - one arguments -> `{:my_request, x}` 29 | - more arguments -> `{:my_request, x, y, ...}` 30 | 31 | ## Common options 32 | 33 | - `:when` - specifies guards (see __Pattern matching__ below for details) 34 | - `:export` - applicable in `defcall/3` and `defcast/3`. If provided, specifies 35 | the server alias. In this case, interface functions will not accept the server 36 | as the first argument, and will insted use the provided alias. The alias 37 | can be an atom (for locally registered processes), `{:global, global_alias}` or 38 | a via tuple (`{:via, registration_module, alias}`). 39 | 40 | ## Pattern matching 41 | 42 | defcall a(1), do: ... 43 | defcall a(x), when: x > 1, do: ... 44 | defcall a(x), when: [interface: x > 1, handler: x < state], do: ... 45 | defcall a(x), state: 1, do: ... 46 | defcall a(_), state: state, do: ... 47 | 48 | ### Details 49 | 50 | `defcall` and other similar constructs usually define a clause for two 51 | functions: the interface function and the handler function. If you're writing 52 | multi-clauses, the following rules apply: 53 | 54 | - Arguments are pattern-matched in the interface and in the handler function. 55 | - The `:state` pattern is used in the handler function. 56 | - The `:when` option by default applies to both, the interface and the handler function. 57 | You can however specify separate guards with `when: [interface: ..., handler: ...]`. 58 | It's not necessary to provide both options to `when`. 59 | 60 | `ExActor` will try to be smart to some extent, and defer from generating the 61 | interface clause if it's not needed. 62 | 63 | For example: 64 | 65 | defcall foo(_, _), state: nil, do: ... 66 | defcall foo(x, y), state: state, do: ... 67 | 68 | will generate only a single interface function that always matches its arguments 69 | and sends them to the server process. There will be of course two `handle_call` 70 | clauses. 71 | 72 | The same holds for more elaborate pattern-matches: 73 | 74 | defcall foo(1, 2), ... 75 | defcall foo(x, y), when: x > y, ... 76 | defcall foo(_, _), state: nil, do: ... 77 | defcall foo(x, y), state: state, do: ... 78 | 79 | The example above will generate three interface clauses: 80 | 81 | - `def foo(1, 2)` 82 | - `def foo(x, y) when x > y` 83 | - `def foo(x, y)` 84 | 85 | Of course, there will be four `handle_call` clauses, each with the corresponding 86 | body provided via `do` option. 87 | 88 | ### Separating interface and handler clauses 89 | 90 | If you want to be more explicit about pattern matching, you can use a body-less 91 | construct: 92 | 93 | defcall foo(x, y) 94 | 95 | This will generate only the interface clause that issues a call (or a cast in 96 | the case of `defcast`) to the server process. 97 | 98 | You can freely use multiple `defcall` body-less clauses if you need to pattern 99 | match arguments. 100 | 101 | To generate handler clauses you can use `defhandlecall/3`: 102 | 103 | defhandlecall foo(_, _), state: nil, do: ... 104 | defhandlecall foo(x, y), state: state, do: ... 105 | 106 | This approach requires some more typing, but it's more explicit. If you need to 107 | perform a complex combination of pattern matches on arguments and the state, it's 108 | probably better to use this technique as it gives you more control over what is 109 | matched at which point. 110 | """ 111 | 112 | 113 | @doc """ 114 | Defines the starter function and initializer body. 115 | 116 | # defines and export start/2 117 | defstart start(x, y) do 118 | # runs in init/1 callback 119 | initial_state(x + y) 120 | end 121 | 122 | # defines and export start_link/2 123 | defstart start_link(x, y) do 124 | # runs in init/1 callback 125 | initial_state(x + y) 126 | end 127 | 128 | You can also provide additional `GenServer` options via `:gen_server_opts` option. 129 | 130 | defstart start(x, y), gen_server_opts: [spawn_opts: [min_heap_size: 10000]], do: ... 131 | 132 | If you need to set `GenServer` options at runtime, use `gen_server_opts: :runtime` and 133 | then the starter function will receive one more argument where you can pass options: 134 | 135 | defstart start(x, y), gen_server_opts: :runtime do 136 | ... 137 | end 138 | 139 | ... 140 | 141 | MyServer.start(x, y, name: :foo, spawn_opts: [min_heap_size: 10000]) 142 | 143 | Body can be omitted. In this case, just the interface function is generated. 144 | This can be useful if you want to define both `start` and `start_link`: 145 | 146 | defstart start(x, y) 147 | defstart start_link(x, y) do 148 | # runs for both cases 149 | end 150 | 151 | Keep in mind that generated `init/1` matches on the number of arguments, so this won't work: 152 | 153 | defstart start_link(x) 154 | defstart start_link(x, y) do 155 | # doesn't handle start_link(x) 156 | end 157 | 158 | If you want to handle various versions, you can just define start heads without the body, 159 | and then use `definit/2` or just implement `init/1`. 160 | 161 | ## Other notes 162 | 163 | - If the `export` option is set while using `ExActor`, it will be used in starters, and 164 | the server process will be registered under a given alias. 165 | - For each specified clause, there will be one corresponding interface function clause. 166 | 167 | ### Request format (arg passed to `init/1`) 168 | 169 | - no arguments -> `nil` 170 | - one arguments -> `{x}` 171 | - more arguments -> `{x, y, ...}` 172 | """ 173 | defmacro defstart(definition, opts \\ [], body \\ []) do 174 | {fun, args} = Macro.decompose_call(definition) 175 | define_starter(false, fun, args, opts ++ body) 176 | end 177 | 178 | @doc """ 179 | Same as `defstart/2` but the interface function is private. 180 | 181 | Can be useful when you need to do pre/post processing in the caller process. 182 | 183 | defmodule MyServer do 184 | def start_link(x, y) do 185 | ... 186 | 187 | do_start_link(x, y) 188 | 189 | ... 190 | end 191 | 192 | defstartp do_start_link(x, y), link: true do 193 | ... 194 | end 195 | end 196 | """ 197 | defmacro defstartp(definition, options \\ [], body \\ []) do 198 | {fun, args} = Macro.decompose_call(definition) 199 | define_starter(true, fun, args, options ++ body) 200 | end 201 | 202 | defp define_starter(private, fun, args, options) do 203 | quote bind_quoted: [ 204 | private: private, 205 | fun: Macro.escape(fun, unquote: true), 206 | args: Macro.escape(args || [], unquote: true), 207 | options: escape_options(options) 208 | ] do 209 | {interface_matches, payload, match_pattern} = ExActor.Operations.start_args(args) 210 | 211 | {arity, interface_matches, gen_server_fun, gen_server_opts} = 212 | ExActor.Operations.prepare_start_interface(fun, interface_matches, options, @exactor_global_options) 213 | 214 | unless private do 215 | case ExActor.Operations.guard(options, :interface) do 216 | nil -> 217 | def unquote(fun)(unquote_splicing(interface_matches)) do 218 | GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) 219 | end 220 | guard -> 221 | def unquote(fun)(unquote_splicing(interface_matches)) when unquote(guard) do 222 | GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) 223 | end 224 | end 225 | else 226 | case ExActor.Operations.guard(options, :interface) do 227 | nil -> 228 | defp unquote(fun)(unquote_splicing(interface_matches)) do 229 | GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) 230 | end 231 | guard -> 232 | defp unquote(fun)(unquote_splicing(interface_matches)) when unquote(guard) do 233 | GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) 234 | end 235 | end 236 | end 237 | 238 | if options[:do] do 239 | definit( 240 | unquote(match_pattern), 241 | unquote(Keyword.take(options, [:when]) ++ [do: options[:do]]) 242 | ) 243 | end 244 | end 245 | end 246 | 247 | @doc false 248 | def extract_args(args) do 249 | arg_names = 250 | for {arg, index} <- Enum.with_index(args), do: extract_arg(arg, index) 251 | 252 | interface_matches = for {arg, arg_name} <- Enum.zip(args, arg_names) do 253 | case arg do 254 | {:\\, context, [match, default]} -> 255 | {:\\, context, [quote(do: unquote(match) = unquote(arg_name)), default]} 256 | match -> quote(do: unquote(match) = unquote(arg_name)) 257 | end 258 | end 259 | 260 | args = for arg <- args do 261 | case arg do 262 | {:\\, _, [match, _]} -> match 263 | _ -> arg 264 | end 265 | end 266 | {arg_names, interface_matches, args} 267 | end 268 | 269 | defmacrop var_name?(arg_name) do 270 | quote do 271 | is_atom(unquote(arg_name)) and not (unquote(arg_name) in [:_, :\\, :=, :%, :%{}, :{}, :<<>>]) 272 | end 273 | end 274 | 275 | defp extract_arg({:\\, _, [inner_arg, _]}, index), 276 | do: extract_arg(inner_arg, index) 277 | defp extract_arg({:=, _, [{arg_name, _, _} = arg, _]}, _index) when var_name?(arg_name), 278 | do: arg 279 | defp extract_arg({:=, _, [_, {arg_name, _, _} = arg]}, _index) when var_name?(arg_name), 280 | do: arg 281 | defp extract_arg({:=, _, [_, {:=, _, _} = submatch]}, index), 282 | do: extract_arg(submatch, index) 283 | defp extract_arg({arg_name, _, _} = arg, _index) when var_name?(arg_name), 284 | do: arg 285 | defp extract_arg(_, index), 286 | do: Macro.var(:"arg#{index}", __MODULE__) 287 | 288 | @doc false 289 | def start_args(args) do 290 | {arg_names, interface_matches, args} = extract_args(args) 291 | 292 | {payload, match_pattern} = 293 | case args do 294 | [] -> {nil, nil} 295 | [_|_] -> 296 | { 297 | quote(do: {unquote_splicing(arg_names)}), 298 | quote(do: {unquote_splicing(args)}) 299 | } 300 | end 301 | 302 | {interface_matches, payload, match_pattern} 303 | end 304 | 305 | @doc false 306 | def prepare_start_interface(fun, interface_matches, options, global_options) do 307 | interface_matches = 308 | unless options[:gen_server_opts] == :runtime do 309 | interface_matches 310 | else 311 | interface_matches ++ [quote(do: unquote(Macro.var(:gen_server_opts, __MODULE__)) \\ [])] 312 | end 313 | 314 | arity = length(interface_matches) 315 | 316 | gen_server_fun = case (options[:link]) do 317 | true -> :start_link 318 | false -> :start 319 | nil -> 320 | if fun in [:start, :start_link] do 321 | fun 322 | else 323 | raise "Function name must be either start or start_link. If you need another name, provide explicit :link option." 324 | end 325 | end 326 | 327 | gen_server_opts = 328 | unless options[:gen_server_opts] == :runtime do 329 | case global_options[:export] do 330 | default when default in [nil, false] -> [] 331 | name -> [name: Macro.escape(name)] 332 | end ++ (options[:gen_server_opts] || []) 333 | else 334 | Macro.var(:gen_server_opts, __MODULE__) 335 | end 336 | 337 | {arity, interface_matches, gen_server_fun, gen_server_opts} 338 | end 339 | 340 | 341 | 342 | @doc """ 343 | Similar to `defstart/3` but generates just the `init` clause. 344 | 345 | Note: keep in mind that `defstart` wraps arguments in a tuple. If you want to 346 | handle `defstart start(x)`, you need to define `definit {x}` 347 | """ 348 | defmacro definit(arg \\ quote(do: _), opts), do: do_definit([{:arg, arg} | opts]) 349 | 350 | defp do_definit(opts) do 351 | quote bind_quoted: [opts: Macro.escape(opts, unquote: true)] do 352 | case ExActor.Operations.guard(opts, :handler) do 353 | nil -> 354 | @impl GenServer 355 | def init(unquote_splicing([opts[:arg]])), do: unquote(opts[:do]) 356 | guard -> 357 | @impl GenServer 358 | def init(unquote_splicing([opts[:arg]])) when unquote(guard), do: unquote(opts[:do]) 359 | end 360 | end 361 | end 362 | 363 | 364 | @doc """ 365 | Defines the cast callback clause and the corresponding interface fun. 366 | """ 367 | defmacro defcast(req_def, options \\ [], body \\ []) do 368 | generate_funs(:defcast, req_def, options ++ body) 369 | end 370 | 371 | @doc """ 372 | Same as `defcast/3` but the interface function is private. 373 | 374 | Can be useful when you need to do pre/post processing in the caller process. 375 | 376 | def exported_interface(...) do 377 | # do some client side preprocessing here 378 | my_request(...) 379 | # do some client side post processing here 380 | end 381 | 382 | # Not available outside of this module 383 | defcastp my_request(...), do: ... 384 | """ 385 | defmacro defcastp(req_def, options \\ [], body \\ []) do 386 | generate_funs(:defcast, req_def, [{:private, true} | options] ++ body) 387 | end 388 | 389 | 390 | @doc """ 391 | Defines the call callback clause and the corresponding interface fun. 392 | 393 | Call-specific options: 394 | 395 | - `:timeout` - specifies the timeout used in `GenServer.call` (see below for 396 | details) 397 | - `:from` - matches the caller in `handle_call`. 398 | 399 | ## Timeout 400 | 401 | defcall long_call, state: state, timeout: :timer.seconds(10), do: ... 402 | 403 | You can also make the timeout parameterizable 404 | 405 | defcall long_call(...), timeout: some_variable, do: ... 406 | 407 | This will generate the interface function as: 408 | 409 | def long_call(..., some_variable) 410 | 411 | where `some_variable` will be used as the timeout in `GenServer.call`. You 412 | won't have the access to this variable in your body though, since the body 413 | specifies the handler function. Default timeout value can also be provided via 414 | standard `\\\\` syntax. 415 | """ 416 | defmacro defcall(req_def, options \\ [], body \\ []) do 417 | generate_funs(:defcall, req_def, options ++ body) 418 | end 419 | 420 | @doc """ 421 | Same as `defcall/3` but the interface function is private. 422 | 423 | Can be useful when you need to do pre/post processing in the caller process. 424 | 425 | def exported_interface(...) do 426 | # do some client side preprocessing here 427 | my_request(...) 428 | # do some client side post processing here 429 | end 430 | 431 | # Not available outside of this module 432 | defcallp my_request(...), do: ... 433 | """ 434 | defmacro defcallp(req_def, options \\ [], body \\ []) do 435 | generate_funs(:defcall, req_def, [{:private, true} | options] ++ body) 436 | end 437 | 438 | @doc """ 439 | Similar to `defcall/3`, but generates just the `handle_call` clause, 440 | without creating the interface function. 441 | """ 442 | defmacro defhandlecall(req_def, options \\ [], body \\ []) do 443 | generate_request_def(:defcall, req_def, options ++ body) 444 | end 445 | 446 | @doc """ 447 | Similar to `defcast/3`, but generates just the `handle_call` clause, 448 | without creating the interface function. 449 | """ 450 | defmacro defhandlecast(req_def, options \\ [], body \\ []) do 451 | generate_request_def(:defcast, req_def, options ++ body) 452 | end 453 | 454 | 455 | 456 | # Generation of call/cast functions. Essentially, this is just 457 | # deferred to be evaluated in the module context. 458 | defp generate_funs(type, req_def, options) do 459 | quote bind_quoted: [ 460 | type: type, 461 | req_def: Macro.escape(req_def, unquote: true), 462 | options: escape_options(options) 463 | ] do 464 | ExActor.Operations.def_request(type, req_def, Keyword.merge(options, @exactor_global_options)) 465 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 466 | end 467 | end 468 | 469 | @doc false 470 | def guard(options, type) do 471 | case options[:when] do 472 | nil -> nil 473 | list when is_list(list) -> list[type] 474 | other -> other 475 | end 476 | end 477 | 478 | @doc false 479 | def def_request(type, req_def, options) do 480 | {req_name, interface_matches, payload, _} = req_args(req_def) 481 | 482 | quote do 483 | req_id = unquote(Macro.escape(req_id(req_def, options))) 484 | unless MapSet.member?(@generated_funs, req_id) do 485 | unquote(define_interface(type, req_name, interface_matches, payload, options)) 486 | @generated_funs MapSet.put(@generated_funs, req_id) 487 | end 488 | 489 | unquote(if options[:do] do 490 | implement_request(type, req_def, options) 491 | end) 492 | end 493 | end 494 | 495 | defp req_id({_, _, _} = definition, options) do 496 | {req_name, args} = Macro.decompose_call(definition) 497 | { 498 | req_name, 499 | Enum.map( 500 | strip_context(args || []), 501 | fn 502 | {var_name, _, scope} when is_atom(var_name) and is_atom(scope) -> :matchall 503 | other -> other 504 | end 505 | ), 506 | strip_context(guard(options, :interface)) 507 | } 508 | end 509 | 510 | defp req_id(req_name, options) when is_atom(req_name) do 511 | req_id({req_name, [], []}, options) 512 | end 513 | 514 | defp strip_context(ast) do 515 | Macro.prewalk(ast, 516 | fn 517 | {a, _context, b} -> {a, [], b} 518 | other -> other 519 | end 520 | ) 521 | end 522 | 523 | defp generate_request_def(type, req_def, options) do 524 | quote bind_quoted: [ 525 | type: type, 526 | req_def: Macro.escape(req_def, unquote: true), 527 | options: escape_options(options) 528 | ] do 529 | ExActor.Operations.implement_request(type, req_def, Keyword.merge(options, @exactor_global_options)) 530 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 531 | end 532 | end 533 | 534 | @doc false 535 | def implement_request(type, req_def, options) do 536 | {_, _, _, match_pattern} = req_args(req_def) 537 | 538 | quote do 539 | unquote(implement_handler(type, options, match_pattern)) 540 | end 541 | end 542 | 543 | 544 | defp req_args(req_def) do 545 | {req_name, args} = parse_req_def(req_def) 546 | {arg_names, interface_matches, args} = extract_args(args) 547 | 548 | {payload, match_pattern} = 549 | case args do 550 | [] -> {req_name, req_name} 551 | [_|_] -> 552 | { 553 | quote(do: {unquote_splicing([req_name | arg_names])}), 554 | quote(do: {unquote_splicing([req_name | args])}) 555 | } 556 | end 557 | 558 | {req_name, interface_matches, payload, match_pattern} 559 | end 560 | 561 | defp parse_req_def(req_name) when is_atom(req_name), do: {req_name, []} 562 | defp parse_req_def({_, _, _} = definition) do 563 | Macro.decompose_call(definition) 564 | end 565 | 566 | # Defines the interface function to call/cast 567 | defp define_interface(type, req_name, interface_matches, payload, options) do 568 | quote bind_quoted: [ 569 | private: options[:private], 570 | type: type, 571 | req_name: req_name, 572 | server_fun: server_fun(type), 573 | interface_args: Macro.escape(interface_args(interface_matches, options), unquote: true), 574 | gen_server_args: Macro.escape(gen_server_args(options, type, payload), unquote: true), 575 | guard: Macro.escape(guard(options, :interface), unquote: true) 576 | ] do 577 | {interface_args, gen_server_args} = 578 | unless type in [:multicall, :abcast] do 579 | {interface_args, gen_server_args} 580 | else 581 | { 582 | [quote(do: nodes \\ [node() | :erlang.nodes()]) | interface_args], 583 | [quote(do: nodes) | gen_server_args] 584 | } 585 | end 586 | 587 | arity = length(interface_args) 588 | unless private do 589 | if guard do 590 | def unquote(req_name)(unquote_splicing(interface_args)) 591 | when unquote(guard) 592 | do 593 | GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) 594 | end 595 | else 596 | def unquote(req_name)(unquote_splicing(interface_args)) do 597 | GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) 598 | end 599 | end 600 | else 601 | if guard do 602 | defp unquote(req_name)(unquote_splicing(interface_args)) 603 | when unquote(guard) 604 | do 605 | GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) 606 | end 607 | else 608 | defp unquote(req_name)(unquote_splicing(interface_args)) do 609 | GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) 610 | end 611 | end 612 | end 613 | end 614 | end 615 | 616 | defp server_fun(:defcast), do: :cast 617 | defp server_fun(:defcall), do: :call 618 | defp server_fun(:multicall), do: :multi_call 619 | defp server_fun(:abcast), do: :abcast 620 | 621 | defp interface_args(args, options) do 622 | server_match(options[:export]) ++ args ++ timeout_match(options[:timeout]) 623 | end 624 | 625 | defp server_match(export) when export == nil or export == true, do: [quote(do: server)] 626 | defp server_match(_), do: [] 627 | 628 | defp timeout_match(nil), do: [] 629 | defp timeout_match(:infinity), do: [] 630 | defp timeout_match(timeout) when is_integer(timeout), do: [] 631 | defp timeout_match(pattern), do: [pattern] 632 | 633 | defp gen_server_args(options, type, msg) do 634 | [server_ref(options, type), msg] ++ timeout_arg(options, type) 635 | end 636 | 637 | defp server_ref(options, op) when op in [:multicall, :abcast] do 638 | case options[:export] do 639 | local when is_atom(local) and local != nil and local != false -> local 640 | {:local, local} -> local 641 | _ -> quote(do: server) 642 | end 643 | end 644 | 645 | defp server_ref(options, _) do 646 | case options[:export] do 647 | default when default in [nil, false, true] -> quote(do: server) 648 | local when is_atom(local) -> local 649 | {:local, local} -> local 650 | {:global, _} = global -> global 651 | {:via, _, _} = via -> Macro.escape(via) 652 | end 653 | end 654 | 655 | defp timeout_arg(options, type) when type in [:defcall, :multicall] do 656 | case options[:timeout] do 657 | {:\\, _, [var, _default]} -> 658 | [var] 659 | timeout when timeout != nil -> 660 | [timeout] 661 | _ -> [] 662 | end 663 | end 664 | 665 | defp timeout_arg(_, _), do: [] 666 | 667 | 668 | @doc false 669 | # Implements the handler function (handle_call, handle_cast, handle_timeout) 670 | def implement_handler(type, options, msg) do 671 | state_arg = get_state_identifier(Keyword.fetch(options, :state)) 672 | {handler_name, handler_args} = handler_sig(type, options, msg, state_arg) 673 | 674 | quote bind_quoted: [ 675 | type: type, 676 | handler_name: handler_name, 677 | handler_args: Macro.escape(handler_args, unquote: true), 678 | guard: Macro.escape(guard(options, :handler), unquote: true), 679 | body: Macro.escape(options[:do], unquote: true) 680 | ] do 681 | if guard do 682 | @impl GenServer 683 | def unquote(handler_name)(unquote_splicing(handler_args)) 684 | when unquote(guard), 685 | do: unquote(body) 686 | else 687 | @impl GenServer 688 | def unquote(handler_name)(unquote_splicing(handler_args)), 689 | do: unquote(body) 690 | end 691 | end 692 | end 693 | 694 | defp get_state_identifier({:ok, match}), 695 | do: quote(do: unquote(match) = unquote(ExActor.Helper.state_var)) 696 | defp get_state_identifier(:error), do: get_state_identifier({:ok, quote(do: _)}) 697 | 698 | defp handler_sig(:defcall, options, msg, state_arg), 699 | do: {:handle_call, [msg, options[:from] || quote(do: _from), state_arg]} 700 | defp handler_sig(:defcast, _, msg, state_arg), 701 | do: {:handle_cast, [msg, state_arg]} 702 | defp handler_sig(:definfo, _, msg, state_arg), 703 | do: {:handle_info, [msg, state_arg]} 704 | 705 | 706 | 707 | @doc """ 708 | Defines the info callback clause. Responses work just like with casts. 709 | 710 | defhandleinfo :some_message, do: ... 711 | defhandleinfo :another_message, state: ..., do: 712 | """ 713 | defmacro defhandleinfo(msg, opts \\ [], body) do 714 | impl_defhandleinfo(msg, opts ++ body) 715 | end 716 | 717 | # Implements handle_info 718 | defp impl_defhandleinfo(msg, options) do 719 | quote bind_quoted: [ 720 | msg: Macro.escape(msg, unquote: true), 721 | options: escape_options(options) 722 | ] do 723 | options = Keyword.merge(options, @exactor_global_options) 724 | 725 | ExActor.Operations.implement_handler(:definfo, options, msg) 726 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 727 | end 728 | end 729 | 730 | @doc """ 731 | Defines a multicall operation. 732 | 733 | defmulticall my_request(x, y), do: ... 734 | 735 | ... 736 | 737 | # If the process is locally registered via `:export` option 738 | MyServer.my_request(2, 3) 739 | MyServer.my_request(nodes, 2, 3) 740 | 741 | # The process is not locally registered via `:export` option 742 | MyServer.my_request(:local_alias, 2, 3) 743 | MyServer.my_request(nodes, :local_alias, 2, 3) 744 | 745 | Request format is the same as in `defcall/3`. Timeout option works just like 746 | with `defcall/3`. 747 | """ 748 | defmacro defmulticall(req_def, options \\ [], body \\ []) do 749 | do_defmulticall(req_def, options ++ body) 750 | end 751 | 752 | @doc """ 753 | Same as `defmulticall/3` but the interface function is private. 754 | """ 755 | defmacro defmulticallp(req_def, options \\ [], body \\ []) do 756 | do_defmulticall(req_def, [{:private, true} | options] ++ body) 757 | end 758 | 759 | defp do_defmulticall(req_def, options) do 760 | quote bind_quoted: [ 761 | req_def: Macro.escape(req_def, unquote: true), 762 | options: escape_options(options) 763 | ] do 764 | options = Keyword.merge(options, @exactor_global_options) 765 | 766 | ExActor.Operations.implement_request(:defcall, req_def, options) 767 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 768 | 769 | ExActor.Operations.def_request(:multicall, req_def, Keyword.drop(options, [:do])) 770 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 771 | end 772 | end 773 | 774 | 775 | @doc """ 776 | Defines an abcast operation. 777 | 778 | defabcast my_request(x, y), do: ... 779 | 780 | ... 781 | 782 | # If the process is locally registered via `:export` option 783 | MyServer.my_request(2, 3) 784 | MyServer.my_request(nodes, 2, 3) 785 | 786 | # The process is not locally registered via `:export` option 787 | MyServer.my_request(:local_alias, 2, 3) 788 | MyServer.my_request(nodes, :local_alias, 2, 3) 789 | """ 790 | defmacro defabcast(req_def, options \\ [], body \\ []) do 791 | do_defabcast(req_def, options ++ body) 792 | end 793 | 794 | @doc """ 795 | Same as `defabcast/3` but the interface function is private. 796 | """ 797 | defmacro defabcastp(req_def, options \\ [], body \\ []) do 798 | do_defabcast(req_def, [{:private, true} | options] ++ body) 799 | end 800 | 801 | defp do_defabcast(req_def, options) do 802 | quote bind_quoted: [ 803 | req_def: Macro.escape(req_def, unquote: true), 804 | options: escape_options(options) 805 | ] do 806 | options = Keyword.merge(options, @exactor_global_options) 807 | 808 | ExActor.Operations.implement_request(:defcast, req_def, options) 809 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 810 | 811 | ExActor.Operations.def_request(:abcast, req_def, Keyword.drop(options, [:do])) 812 | |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) 813 | end 814 | end 815 | 816 | defp escape_options(options) do 817 | Enum.map(options, 818 | fn 819 | {:export, export} -> {:export, export} 820 | other -> Macro.escape(other, unquote: true) 821 | end 822 | ) 823 | end 824 | end 825 | -------------------------------------------------------------------------------- /lib/exactor/responders.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Responders do 2 | @moduledoc """ 3 | Helper macros that can be used for simpler responses from init/call/cast/info 4 | handlers. 5 | """ 6 | 7 | @doc """ 8 | Sets the initial state. 9 | 10 | Applicable in: 11 | 12 | - `ExActor.Operations.defstart/3` 13 | - `ExActor.Operations.definit/2` 14 | """ 15 | defmacro initial_state(state, timeout \\ nil) do 16 | timeout = timeout || quote(do: Process.get(ExActor.ResponseDecoration) || :infinity) 17 | quote do 18 | {:ok, unquote(state), unquote(timeout)} 19 | end 20 | end 21 | 22 | @doc """ 23 | Ensures that timeout will be included in each return tuple. 24 | 25 | Must be called from `ExActor.Operations.defstart/2` (or `definit`). 26 | 27 | This works only for return tuples made by macros from this module. If you're 28 | creating standard `gen_server` response manually, it's your responsibility to 29 | include the timeout, or override it if you want to. 30 | """ 31 | defmacro timeout_after(time_ms) do 32 | quote do 33 | Process.put(ExActor.ResponseDecoration, unquote(time_ms)) 34 | end 35 | end 36 | 37 | @doc """ 38 | Ensures that `:hibernate` will be included in each return tuple. 39 | 40 | Must be called from `ExActor.Operations.defstart/2` (or `definit`). 41 | 42 | This works only for return tuples made by macros from this module. If you're 43 | creating standard `gen_server` response manually, it's your responsibility to 44 | include the `:hibernate`, or override it if you want to. 45 | """ 46 | defmacro hibernate do 47 | quote do 48 | Process.put(ExActor.ResponseDecoration, :hibernate) 49 | end 50 | end 51 | 52 | @doc """ 53 | Replies without changing the state. 54 | 55 | Applicable in: 56 | 57 | - `ExActor.Operations.defcall/3` 58 | - `ExActor.Operations.defmulticall/3` 59 | """ 60 | defmacro reply(response, timeout \\ nil) do 61 | timeout = timeout || quote(do: Process.get(ExActor.ResponseDecoration) || :infinity) 62 | quote do 63 | {:reply, unquote(response), unquote(ExActor.Helper.state_var), unquote(timeout)} 64 | end 65 | end 66 | 67 | @doc """ 68 | Replies and sets the new state 69 | 70 | Applicable in: 71 | 72 | - `ExActor.Operations.defcall/3` 73 | - `ExActor.Operations.defmulticall/3` 74 | """ 75 | defmacro set_and_reply(new_state, response, timeout \\ nil) do 76 | timeout = timeout || quote(do: Process.get(ExActor.ResponseDecoration) || :infinity) 77 | quote do 78 | {:reply, unquote(response), unquote(new_state), unquote(timeout)} 79 | end 80 | end 81 | 82 | @doc """ 83 | Sets the new state. 84 | 85 | Applicable in: 86 | 87 | - `ExActor.Operations.defcall/3` 88 | - `ExActor.Operations.defcast/3` 89 | - `ExActor.Operations.defabcast/3` 90 | - `ExActor.Operations.defmulticall/3` 91 | - `ExActor.Operations.defhandleinfo/3` 92 | """ 93 | defmacro new_state(state, timeout \\ nil) do 94 | timeout = timeout || quote(do: Process.get(ExActor.ResponseDecoration) || :infinity) 95 | quote do 96 | {:noreply, unquote(state), unquote(timeout)} 97 | end 98 | end 99 | 100 | @doc """ 101 | Leaves the state unchanged. 102 | 103 | Applicable in: 104 | 105 | - `ExActor.Operations.defcall/3` 106 | - `ExActor.Operations.defcast/3` 107 | - `ExActor.Operations.defabcast/3` 108 | - `ExActor.Operations.defmulticall/3` 109 | - `ExActor.Operations.defhandleinfo/3` 110 | """ 111 | defmacro noreply(timeout \\ nil) do 112 | timeout = timeout || quote(do: Process.get(ExActor.ResponseDecoration) || :infinity) 113 | quote do 114 | {:noreply, unquote(ExActor.Helper.state_var), unquote(timeout)} 115 | end 116 | end 117 | 118 | @doc """ 119 | Stops the server. 120 | 121 | Applicable in: 122 | 123 | - `ExActor.Operations.defcall/3` 124 | - `ExActor.Operations.defcast/3` 125 | - `ExActor.Operations.defabcast/3` 126 | - `ExActor.Operations.defmulticall/3` 127 | - `ExActor.Operations.defhandleinfo/3` 128 | """ 129 | defmacro stop_server(reason) do 130 | quote do 131 | {:stop, unquote(reason), unquote(ExActor.Helper.state_var)} 132 | end 133 | end 134 | end -------------------------------------------------------------------------------- /lib/exactor/strict.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Strict do 2 | @moduledoc """ 3 | Predefine that provides strict default implementation for `gen_server` 4 | required functions. Default implementation will cause the `gen_server` to 5 | be stopped. This predefine is useful if you want to need only some parts of 6 | the server to be implemented, and want to fail for everything else that 7 | happens on the server. 8 | 9 | All ExActor macros are imported. 10 | 11 | Example: 12 | 13 | defmodule MyServer do 14 | use ExActor.Strict 15 | 16 | # without this the server can't be started 17 | definit do: ... 18 | 19 | ... 20 | end 21 | 22 | # Locally registered name: 23 | use ExActor.Strict, export: :some_registered_name 24 | 25 | # Globally registered name: 26 | use ExActor.Strict, export: {:global, :global_registered_name} 27 | """ 28 | defmacro __using__(opts) do 29 | quote do 30 | use ExActor.Behaviour.Strict 31 | 32 | @generated_funs MapSet.new 33 | 34 | import ExActor.Operations 35 | import ExActor.Responders 36 | 37 | unquote(ExActor.Helper.init_generation_state(opts)) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/exactor/tolerant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Tolerant do 2 | @moduledoc """ 3 | Predefine that provides tolerant default implementation for `gen_server` 4 | required functions. Default implementation will cause the `gen_server` to 5 | ignore messages (e.g. calls/casts). 6 | 7 | All ExActor macros are imported. 8 | 9 | Example: 10 | 11 | defmodule MyServer do 12 | use ExActor.Tolerant 13 | ... 14 | end 15 | 16 | # Locally registered name: 17 | use ExActor.Tolerant, export: :some_registered_name 18 | 19 | # Globally registered name: 20 | use ExActor.Tolerant, export: {:global, :global_registered_name} 21 | """ 22 | 23 | defmacro __using__(opts) do 24 | quote do 25 | use ExActor.Behaviour.Tolerant 26 | 27 | @generated_funs MapSet.new 28 | 29 | import ExActor.Operations 30 | import ExActor.Responders 31 | 32 | unquote(ExActor.Helper.init_generation_state(opts)) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExActor.Mixfile do 2 | use Mix.Project 3 | 4 | @version "2.2.4" 5 | 6 | def project do 7 | [ 8 | project: "ExActor", 9 | version: @version, 10 | elixir: "~> 1.0", 11 | app: :exactor, 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps(), 15 | package: [ 16 | maintainers: ["Saša Jurić"], 17 | licenses: ["MIT"], 18 | links: %{ 19 | "Github" => "https://github.com/sasa1977/exactor", 20 | "Docs" => "http://hexdocs.pm/exactor", 21 | "Changelog" => "https://github.com/sasa1977/exactor/blob/#{@version}/CHANGELOG.md#v#{String.replace(@version, ".", "")}" 22 | } 23 | ], 24 | description: "Simplified creation of GenServer based processes in Elixir.", 25 | name: "ExActor", 26 | docs: [ 27 | extras: ["README.md"], 28 | main: "ExActor.Operations", 29 | source_url: "https://github.com/sasa1977/exactor/", 30 | source_ref: @version 31 | ] 32 | ] 33 | end 34 | 35 | def application, do: [applications: [:logger]] 36 | 37 | defp deps do 38 | [ 39 | {:ex_doc, "~> 0.14.5", only: :dev} 40 | ] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} 3 | -------------------------------------------------------------------------------- /test/basic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BasicTest do 2 | use ExUnit.Case 3 | 4 | defmodule TestServer do 5 | use ExActor.Tolerant 6 | 7 | defstart start, do: initial_state(nil) 8 | defstart start(x), do: initial_state(x) 9 | defstart start(x,y,z), do: initial_state(x+y+z) 10 | defstart my_start(x,y), link: false, do: initial_state(x+y) 11 | 12 | defstart start_link 13 | 14 | defcast set(x), do: new_state(x) 15 | defcall get, state: state, do: reply(state) 16 | 17 | defcast pm_set, state: nil, do: new_state(:two) 18 | defcast pm_set, state: 3, do: new_state(:three) 19 | 20 | defcast pm_set(1), do: new_state(:one) 21 | defcast pm_set(x), when: x == 2, do: new_state(:two) 22 | defcast pm_set(_), state: :two, do: new_state(:three) 23 | defcast pm_set(_), state: state, when: [handler: state == :three], do: new_state(:four) 24 | defcast pm_set(x), do: new_state(x) 25 | 26 | defcall timeout1, timeout: 10, do: (:timer.sleep(100); reply(:ok)) 27 | defcall timeout2, timeout: foo, do: (:timer.sleep(100); reply(:ok)) 28 | defcall timeout3, timeout: foo \\ 10, do: (:timer.sleep(100); reply(:ok)) 29 | 30 | defhandlecall unexported, do: reply(:unexported) 31 | def my_unexported(server), do: GenServer.call(server, :unexported) 32 | 33 | defcall reply_leave_state, do: reply(3) 34 | defcast leave_state, do: noreply() 35 | defcall full_reply, do: set_and_reply(6, 5) 36 | 37 | def callp_interface(server), do: private_call(server) 38 | defcallp private_call, do: reply(:private_call) 39 | 40 | def castp_interface(server), do: private_cast(server) 41 | defcastp private_cast, do: new_state(:private) 42 | 43 | defcall test_exc do 44 | try do 45 | throw(__ENV__.line) 46 | catch _,line -> 47 | reply({line, hd(System.stacktrace) |> elem(3)}) 48 | end 49 | end 50 | 51 | defcall test_from, from: {from, _} do 52 | send(from, :from_ok) 53 | reply(:ok) 54 | end 55 | 56 | defcall test_composite_match(%{} = arg) do 57 | reply(arg) 58 | end 59 | 60 | defhandleinfo {:msg1, from} do 61 | send(from, :reply_msg1) 62 | noreply() 63 | end 64 | 65 | defhandleinfo {:msg_get, from}, state: state do 66 | send(from, state) 67 | noreply() 68 | end 69 | 70 | defhandleinfo sender, when: is_pid(sender) do 71 | send(sender, :echo) 72 | noreply() 73 | end 74 | end 75 | 76 | test "basic" do 77 | {:ok, pid} = TestServer.start(1) 78 | assert is_pid(pid) 79 | assert TestServer.get(pid) == 1 80 | 81 | TestServer.set(pid, 2) 82 | assert TestServer.get(pid) == 2 83 | 84 | TestServer.set(pid, nil) 85 | TestServer.pm_set(pid) 86 | assert TestServer.get(pid) == :two 87 | 88 | TestServer.pm_set(pid, 1) 89 | assert TestServer.get(pid) == :one 90 | 91 | TestServer.pm_set(pid, 2) 92 | assert TestServer.get(pid) == :two 93 | 94 | TestServer.pm_set(pid, 3) 95 | assert TestServer.get(pid) == :three 96 | 97 | TestServer.pm_set(pid, 4) 98 | assert TestServer.get(pid) == :four 99 | 100 | TestServer.pm_set(pid, 5) 101 | assert TestServer.get(pid) == 5 102 | 103 | assert TestServer.callp_interface(pid) == :private_call 104 | assert catch_error(TestServer.private_call(pid)) == :undef 105 | 106 | TestServer.castp_interface(pid) 107 | assert TestServer.get(pid) == :private 108 | assert catch_error(TestServer.private_cast(pid)) == :undef 109 | 110 | {:timeout, _} = catch_exit(TestServer.timeout1(pid)) 111 | {:timeout, _} = catch_exit(TestServer.timeout2(pid, 10)) 112 | assert :ok == TestServer.timeout2(pid, 2000) 113 | {:timeout, _} = catch_exit(TestServer.timeout3(pid)) 114 | {:timeout, _} = catch_exit(TestServer.timeout3(pid, 10)) 115 | assert :ok == TestServer.timeout3(pid, 2000) 116 | 117 | assert catch_error(TestServer.unexported(pid)) == :undef 118 | assert TestServer.my_unexported(pid) == :unexported 119 | 120 | TestServer.set(pid, 2) 121 | assert TestServer.reply_leave_state(pid) == 3 122 | assert TestServer.get(pid) == 2 123 | 124 | TestServer.leave_state(pid) 125 | assert TestServer.get(pid) == 2 126 | 127 | assert TestServer.full_reply(pid) == 5 128 | assert TestServer.get(pid) == 6 129 | 130 | {line, exception} = TestServer.test_exc(pid) 131 | assert (exception[:file] |> Path.basename |> to_string) == "basic_test.exs" 132 | assert exception[:line] == line 133 | 134 | assert TestServer.test_from(pid) == :ok 135 | assert_receive :from_ok 136 | 137 | send(pid, {:msg1, self()}) 138 | assert_receive :reply_msg1 139 | 140 | TestServer.set(pid, 10) 141 | send(pid, {:msg_get, self()}) 142 | assert_receive 10 143 | 144 | send(pid, self()) 145 | assert_receive :echo 146 | end 147 | 148 | test "start" do 149 | {:ok, pid} = TestServer.start 150 | assert TestServer.get(pid) == nil 151 | 152 | {:ok, pid} = TestServer.start(1) 153 | assert TestServer.get(pid) == 1 154 | 155 | {:ok, pid} = TestServer.start(1,2,3) 156 | assert TestServer.get(pid) == 6 157 | 158 | {:ok, pid} = TestServer.my_start(3,4) 159 | assert TestServer.get(pid) == 7 160 | 161 | Process.exit(pid, :kill) 162 | refute_receive {:EXIT, ^pid, :killed} 163 | end 164 | 165 | test "start_link" do 166 | {:ok, pid} = TestServer.start_link 167 | assert TestServer.get(pid) == nil 168 | 169 | Process.flag(:trap_exit, true) 170 | Process.exit(pid, :kill) 171 | assert_receive {:EXIT, ^pid, :killed} 172 | Process.flag(:trap_exit, false) 173 | end 174 | 175 | 176 | defmodule PrivateStarterServer do 177 | use ExActor.GenServer 178 | 179 | def my_start, do: start(5) 180 | defstartp start(x), do: initial_state(x) 181 | defcall get, state: state, do: reply(state) 182 | end 183 | 184 | test "private starter" do 185 | assert catch_error(PrivateStarterServer.start(5)) == :undef 186 | 187 | {:ok, pid} = PrivateStarterServer.my_start 188 | assert PrivateStarterServer.get(pid) == 5 189 | end 190 | 191 | 192 | defmodule RuntimeGenServerOptsServer do 193 | use ExActor.GenServer 194 | 195 | defstart start(x), gen_server_opts: :runtime, do: initial_state(x) 196 | defcall get, state: state, do: reply(state) 197 | end 198 | 199 | test "runtime gen_server_opts" do 200 | RuntimeGenServerOptsServer.start(5, name: :foo) 201 | assert RuntimeGenServerOptsServer.get(:foo) == 5 202 | 203 | RuntimeGenServerOptsServer.start(3, name: :bar) 204 | assert RuntimeGenServerOptsServer.get(:bar) == 3 205 | end 206 | 207 | defmodule InitialState2 do 208 | use ExActor.Tolerant 209 | 210 | defstart start(1), do: initial_state(:one) 211 | defstart start(x), when: x < 3, do: initial_state(:two) 212 | defstart start(_), do: initial_state(:rest) 213 | 214 | defcall get, state: state, do: reply(state) 215 | end 216 | 217 | test "initial state" do 218 | assert (InitialState2.start(1) |> elem(1) |> InitialState2.get) == :one 219 | assert (InitialState2.start(2) |> elem(1) |> InitialState2.get) == :two 220 | assert (InitialState2.start(3) |> elem(1) |> InitialState2.get) == :rest 221 | end 222 | 223 | 224 | defmodule PatternMatch do 225 | use ExActor.Tolerant 226 | 227 | defstart start, do: initial_state(nil) 228 | defstart start(state), do: initial_state(state) 229 | 230 | defcall test(1), do: reply(:one) 231 | defcall test(2), do: reply(:two) 232 | defcall test(x), when: x < 4, do: reply(:three) 233 | defcall test(_) 234 | defhandlecall test(_), state: 4, do: reply(:four) 235 | defhandlecall test(_), state: state, when: state < 6, do: reply(:five) 236 | defhandlecall test(_), do: reply(:rest) 237 | end 238 | 239 | test "pattern matching" do 240 | assert (PatternMatch.start |> elem(1) |> PatternMatch.test(1)) == :one 241 | assert (PatternMatch.start |> elem(1) |> PatternMatch.test(2)) == :two 242 | assert (PatternMatch.start |> elem(1) |> PatternMatch.test(3)) == :three 243 | assert (PatternMatch.start(4) |> elem(1) |> PatternMatch.test(4)) == :four 244 | assert (PatternMatch.start(5) |> elem(1) |> PatternMatch.test(4)) == :five 245 | assert (PatternMatch.start(6) |> elem(1) |> PatternMatch.test(4)) == :rest 246 | end 247 | 248 | defmodule TestDefaultsServer do 249 | use ExActor.GenServer 250 | 251 | defstart start(a, b \\ 0, c, _ = d, e = _, _ = _ = f), do: initial_state(a + b + c + d + e + f) 252 | defcall get(_ = x \\ nil), state: state, do: reply(x || state) 253 | defcast set(x = _ \\ 0), do: new_state(x) 254 | end 255 | 256 | test "defaults" do 257 | {:ok, pid} = TestDefaultsServer.start(1, 2, 3, 4, 5, 6) 258 | assert TestDefaultsServer.get(pid) == 21 259 | 260 | {:ok, pid} = TestDefaultsServer.start(1, 2, 3, 4, 5) 261 | assert TestDefaultsServer.get(pid) == 15 262 | 263 | assert TestDefaultsServer.get(pid, 4) == 4 264 | 265 | TestDefaultsServer.set(pid) 266 | assert TestDefaultsServer.get(pid) == 0 267 | 268 | TestDefaultsServer.set(pid, 5) 269 | assert TestDefaultsServer.get(pid) == 5 270 | end 271 | 272 | 273 | defmodule TestGlobalTimeout do 274 | use ExActor.GenServer, export: TestGlobalTimeout 275 | 276 | defstart start(hibernate? \\ false) do 277 | if hibernate? do 278 | hibernate() 279 | else 280 | timeout_after(50) 281 | end 282 | initial_state(nil) 283 | end 284 | 285 | defcast noexpire, do: noreply(:infinity) 286 | defcast expire, do: noreply() 287 | 288 | defhandleinfo :timeout, do: stop_server(:normal) 289 | end 290 | 291 | test "timeout" do 292 | TestGlobalTimeout.start 293 | TestGlobalTimeout.noexpire 294 | :timer.sleep(100) 295 | assert is_pid(Process.whereis(TestGlobalTimeout)) 296 | 297 | TestGlobalTimeout.expire 298 | :timer.sleep(100) 299 | assert nil == Process.whereis(TestGlobalTimeout) 300 | 301 | TestGlobalTimeout.start(true) 302 | assert is_pid(Process.whereis(TestGlobalTimeout)) 303 | end 304 | 305 | 306 | defmodule InterfaceReqsServer do 307 | use ExActor.GenServer 308 | 309 | defstart start_link, do: initial_state(nil) 310 | 311 | defcall foo 312 | defcast bar 313 | defcall baz(x, y) 314 | 315 | @impl GenServer 316 | def handle_call({:baz, x, y}, _, state) do 317 | {:reply, x+y, state} 318 | end 319 | end 320 | 321 | test "just interfaces" do 322 | Logger.remove_backend(:console) 323 | Process.flag(:trap_exit, true) 324 | 325 | {:ok, pid} = InterfaceReqsServer.start_link 326 | assert InterfaceReqsServer.baz(pid, 1, 2) == 3 327 | 328 | try do InterfaceReqsServer.foo(pid) catch _,_ -> nil end 329 | assert_receive {:EXIT, ^pid, {:function_clause, _}} 330 | 331 | {:ok, pid} = InterfaceReqsServer.start_link 332 | try do InterfaceReqsServer.bar(pid) catch _,_ -> nil end 333 | {:ok, vsn} = :application.get_key(:elixir, :vsn) 334 | case Version.compare(to_string(vsn), "1.4.0-rc.0") do 335 | :lt -> assert_receive {:EXIT, ^pid, {:bad_cast, :bar}} 336 | _ -> assert_receive {:EXIT, ^pid, {%RuntimeError{}, _}} 337 | end 338 | Process.flag(:trap_exit, false) 339 | :timer.sleep(100) 340 | Logger.add_backend(:console) 341 | end 342 | 343 | defmodule FragmentModule do 344 | defmacro __using__(_) do 345 | quote do 346 | defstart start_link, do: initial_state(nil) 347 | defcall operation, do: reply(:ok) 348 | end 349 | end 350 | end 351 | 352 | defmodule ComposedModule do 353 | use ExActor.Tolerant 354 | use FragmentModule 355 | end 356 | 357 | test "composed module" do 358 | {:ok, pid} = ComposedModule.start_link() 359 | assert :ok == ComposedModule.operation(pid) 360 | end 361 | 362 | defmodule Struct do 363 | defstruct [:x, :y] 364 | end 365 | 366 | defmodule MatchTest do 367 | use ExActor.Tolerant 368 | 369 | defstart start, do: initial_state(nil) 370 | 371 | defcall call(%Struct{} = s) do 372 | reply(s) 373 | end 374 | end 375 | 376 | test "matching a struct" do 377 | {:ok, pid} = MatchTest.start() 378 | assert MatchTest.call(pid, %Struct{x: 1, y: 2}) == %Struct{x: 1, y: 2} 379 | end 380 | end 381 | -------------------------------------------------------------------------------- /test/cluster_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClusterTest do 2 | use ExUnit.Case 3 | 4 | defmodule ClusterServer do 5 | use ExActor.GenServer, export: :cluster_pid 6 | 7 | defstart start, do: initial_state(nil) 8 | defmulticall sum(x, y), do: reply(x + y) 9 | 10 | defmulticall timeout1, timeout: 10, do: (:timer.sleep(100); reply(:ok)) 11 | defmulticall timeout2, timeout: foo, do: (:timer.sleep(100); reply(:ok)) 12 | defmulticall timeout3, timeout: foo \\ 10, do: (:timer.sleep(100); reply(:ok)) 13 | 14 | def diff(x, y), do: do_diff(x, y) 15 | defmulticallp do_diff(x, y), do: reply(x - y) 16 | 17 | defcall get, state: state, do: reply(state) 18 | defabcast set(x), do: new_state(x) 19 | 20 | def set2(x), do: do_set2(x) 21 | defabcastp do_set2(x), do: new_state(2 * x) 22 | end 23 | 24 | test "cluster pid" do 25 | ClusterServer.start 26 | assert ClusterServer.sum(3, 2) == {[{:"nonode@nohost", 5}], []} 27 | assert ClusterServer.sum([node()], 3, 2) == {[{:"nonode@nohost", 5}], []} 28 | assert ClusterServer.sum([:unknown_node], 3, 2) == {[], [:unknown_node]} 29 | assert ClusterServer.sum([], 3, 2) == {[], []} 30 | assert catch_error(ClusterServer.do_diff(3, 2)) == :undef 31 | assert ClusterServer.diff(3, 2) == {[{:"nonode@nohost", 1}], []} 32 | 33 | assert ClusterServer.timeout1 == {[], [node()]} 34 | assert ClusterServer.timeout1 == {[], [node()]} 35 | assert ClusterServer.timeout2(10) == {[], [node()]} 36 | assert ClusterServer.timeout2(2000) == {[{node(), :ok}], []} 37 | assert ClusterServer.timeout3 == {[], [node()]} 38 | assert ClusterServer.timeout3([node()], 10) == {[], [node()]} 39 | assert ClusterServer.timeout3([node()], 2000) == {[{node(), :ok}], []} 40 | 41 | assert ClusterServer.set(4) == :abcast 42 | assert ClusterServer.get == 4 43 | 44 | assert catch_error(ClusterServer.do_set2(3)) == :undef 45 | assert ClusterServer.set2(4) == :abcast 46 | assert ClusterServer.get == 8 47 | end 48 | 49 | 50 | 51 | defmodule NonRegisteredClusterServer do 52 | use ExActor.GenServer 53 | 54 | defstart start do 55 | Process.register(self(), :nrca) 56 | initial_state(nil) 57 | end 58 | 59 | defmulticall sum(x, y), do: reply(x + y) 60 | 61 | def diff(x, y), do: do_diff(:nrca, x, y) 62 | defmulticallp do_diff(x, y), do: reply(x - y) 63 | 64 | defcall get, state: state, do: reply(state) 65 | defabcast set(x), do: new_state(x) 66 | 67 | def set2(x), do: do_set2(:nrca, x) 68 | defabcastp do_set2(x), do: new_state(2 * x) 69 | end 70 | 71 | test "non registered cluster pid" do 72 | NonRegisteredClusterServer.start 73 | assert NonRegisteredClusterServer.sum(:nrca, 3, 2) == {[{:"nonode@nohost", 5}], []} 74 | assert NonRegisteredClusterServer.sum([node()], :nrca, 3, 2) == {[{:"nonode@nohost", 5}], []} 75 | assert NonRegisteredClusterServer.sum([:unknown_node], :nrca, 3, 2) == {[], [:unknown_node]} 76 | assert NonRegisteredClusterServer.sum([], :nrca, 3, 2) == {[], []} 77 | assert catch_error(NonRegisteredClusterServer.do_diff(:nrca, 3, 2)) == :undef 78 | assert NonRegisteredClusterServer.diff(3, 2) == {[{:"nonode@nohost", 1}], []} 79 | 80 | assert NonRegisteredClusterServer.set(:nrca, 4) == :abcast 81 | assert NonRegisteredClusterServer.get(:nrca) == 4 82 | 83 | assert catch_error(NonRegisteredClusterServer.do_set2(:nrca, 3)) == :undef 84 | assert NonRegisteredClusterServer.set2(4) == :abcast 85 | assert NonRegisteredClusterServer.get(:nrca) == 8 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/dynamic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DynamicTest do 2 | use ExUnit.Case 3 | 4 | defmodule DynServer do 5 | use ExActor.Tolerant 6 | 7 | defstart start 8 | 9 | for op <- [:get] do 10 | defcall unquote(op), state: state do 11 | reply(state) 12 | end 13 | end 14 | 15 | for op <- [:set] do 16 | defcast unquote(op)(arg) do 17 | new_state(arg) 18 | end 19 | end 20 | end 21 | 22 | test "dynamic" do 23 | {:ok, pid} = DynServer.start 24 | DynServer.set(pid, 1) 25 | assert DynServer.get(pid) == 1 26 | end 27 | 28 | 29 | defmodule MapServer do 30 | use ExActor.Tolerant 31 | import ExActor.Delegator 32 | 33 | defstart start, do: initial_state(Map.new) 34 | 35 | delegate_to Map do 36 | query get/2 37 | query size/1 38 | trans put/3 39 | end 40 | 41 | defcall normal_call, do: reply(2) 42 | end 43 | 44 | test "wrapper" do 45 | {:ok, pid} = MapServer.start 46 | 47 | assert MapServer.get(pid, :a) == nil 48 | assert MapServer.size(pid) == 0 49 | 50 | MapServer.put(pid, :a, 1) 51 | assert MapServer.get(pid, :a) == 1 52 | assert MapServer.size(pid) == 1 53 | assert MapServer.normal_call(pid) == 2 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/predefines_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PredefinesTest do 2 | use ExUnit.Case 3 | 4 | defmodule TolerantServer do 5 | use ExActor.Tolerant 6 | defstart start 7 | end 8 | 9 | test "tolerant" do 10 | {:ok, pid} = TolerantServer.start 11 | GenServer.cast(pid, :undefined_message) 12 | send(pid, :undefined_message) 13 | assert match?( 14 | {:timeout, _}, 15 | catch_exit(GenServer.call(pid, :undefined_message, 10)) 16 | ) 17 | end 18 | 19 | 20 | defmodule NonStartableStrictServer do 21 | use ExActor.Strict 22 | end 23 | 24 | 25 | defmodule StrictServer do 26 | use ExActor.Strict 27 | defstart start, do: initial_state(nil) 28 | end 29 | 30 | setup do 31 | Logger.remove_backend(:console) 32 | on_exit fn -> 33 | Logger.add_backend(:console) 34 | end 35 | end 36 | 37 | test "strict" do 38 | assert match?({:error, :badinit}, GenServer.start(NonStartableStrictServer, nil)) 39 | 40 | assert_invalid(StrictServer, &GenServer.cast(&1, :undefined_message)) 41 | assert_invalid(StrictServer, &send(&1, :undefined_message)) 42 | assert_invalid(StrictServer, 43 | fn(pid) -> 44 | assert match?( 45 | {{:bad_call, :undefined_message}, _}, 46 | catch_exit(GenServer.call(pid, :undefined_message, 10)) 47 | ) 48 | end 49 | ) 50 | end 51 | 52 | defp assert_invalid(module, fun) do 53 | {:ok, pid} = module.start 54 | 55 | fun.(pid) 56 | 57 | :timer.sleep(20) 58 | assert Process.info(pid) == nil 59 | end 60 | 61 | 62 | 63 | defmodule GenServerServer do 64 | use ExActor.GenServer 65 | defstart start 66 | end 67 | 68 | test "gen_server" do 69 | assert_invalid(GenServerServer, &GenServer.cast(&1, :undefined_message)) 70 | 71 | assert_invalid(GenServerServer, 72 | fn(pid) -> 73 | send(pid, :undefined_message) 74 | {:ok, vsn} = :application.get_key(:elixir, :vsn) 75 | case Version.compare(to_string(vsn), "1.4.0-rc.0") do 76 | :lt -> 77 | assert match?( 78 | {{:bad_call, :undefined_message}, _}, 79 | catch_exit(GenServer.call(pid, :undefined_message, 10)) 80 | ) 81 | _ -> 82 | assert match?( 83 | {{%RuntimeError{}, _}, _}, 84 | catch_exit(GenServer.call(pid, :undefined_message, 10)) 85 | ) 86 | end 87 | end 88 | ) 89 | end 90 | 91 | 92 | 93 | defmodule EmptyServer do 94 | use ExActor.Empty 95 | defstart start 96 | 97 | def init(args), do: { :ok, args } 98 | def handle_call(_msg, _from, state), do: {:reply, 1, state} 99 | def handle_info(_msg, state), do: {:noreply, state} 100 | def handle_cast(_msg, state), do: {:noreply, state} 101 | def terminate(_reason, _state), do: :ok 102 | def code_change(_old, state, _extra), do: { :ok, state } 103 | end 104 | 105 | test "empty" do 106 | {:ok, pid} = EmptyServer.start 107 | GenServer.cast(pid, :undefined_message) 108 | send(pid, :undefined_message) 109 | assert GenServer.call(pid, :undefined_message) == 1 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /test/registration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RegistrationTest do 2 | use ExUnit.Case 3 | 4 | defmodule SingletonServer do 5 | use ExActor.Tolerant, export: :singleton 6 | defstart start(x), do: initial_state(x) 7 | 8 | defcall get, state: state, do: reply(state) 9 | defcast set(x), do: new_state(x) 10 | end 11 | 12 | test "singleton" do 13 | {:ok, _} = SingletonServer.start(0) 14 | SingletonServer.set(5) 15 | assert SingletonServer.get == 5 16 | end 17 | 18 | 19 | defmodule GlobalSingletonServer do 20 | use ExActor.Tolerant, export: {:global, :global_singleton} 21 | defstart start(x), do: initial_state(x) 22 | 23 | defcall get, state: state, do: reply(state) 24 | defcast set(x), do: new_state(x) 25 | end 26 | 27 | test "global singleton" do 28 | {:ok, _} = GlobalSingletonServer.start(0) 29 | GlobalSingletonServer.set(3) 30 | assert GlobalSingletonServer.get == 3 31 | end 32 | 33 | 34 | defmodule ViaSingletonServer do 35 | use ExActor.Tolerant, export: {:via, :global, :global_singleton2} 36 | 37 | defstart start(x), do: initial_state(x) 38 | 39 | defcall get, state: state, do: reply(state) 40 | defcast set(x), do: new_state(x) 41 | end 42 | 43 | test "via singleton" do 44 | {:ok, _} = ViaSingletonServer.start(0) 45 | ViaSingletonServer.set(4) 46 | assert ViaSingletonServer.get == 4 47 | end 48 | end -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start --------------------------------------------------------------------------------