├── .gitignore ├── LICENSE.md ├── README.md ├── background.md ├── config └── config.exs ├── examples ├── .gitignore ├── README.md ├── apps │ ├── anon_worker │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ └── anon_worker.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── anon_worker_test.exs │ │ │ └── test_helper.exs │ ├── named_worker │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ └── named_worker.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── named_worker_test.exs │ │ │ └── test_helper.exs │ └── pooled_named_worker │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ └── pooled_named_worker.ex │ │ ├── mix.exs │ │ └── test │ │ ├── pooled_named_worker_test.exs │ │ └── test_helper.exs ├── config │ └── config.exs ├── mix.exs └── mix.lock ├── lib ├── jeeves.ex └── jeeves │ ├── anonymous.ex │ ├── common.ex │ ├── named.ex │ ├── pooled.ex │ ├── scheduler.ex │ ├── scheduler │ ├── #pool_supervisor.ex# │ └── pool_supervisor.ex │ ├── service.ex │ └── util │ └── preprocessor_state.ex ├── mix.exs ├── mix.lock └── test ├── anonymous_test.exs ├── common_test.exs ├── named_test.exs ├── processor_state_test.exs ├── service_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | README.html 22 | 23 | .elixir_ls/ 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | #### Copyright © 2017 David Thomas (pragdave) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you 4 | may not use this file except in compliance with the License. You may 5 | obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an _as is_ basis, 11 | **without warranties or conditions of any kind**, either express or 12 | implied. See the License for the specific language governing 13 | permissions and limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _tl;dr_ 2 | 3 | * create anonymous, named, and pooled services by just specifying 4 | their functions 5 | * have them automatically wrapped in standard OTP GenServers and 6 | Supervisors. 7 | * simplify testing with automatically generated nonservice 8 | implementations. 9 | 10 | 11 | Here's a pool of between 2 and 5 Fibonacci number services, each 12 | supervised and run in a separate, parallel process: 13 | 14 | ~~~ elixir 15 | defmodule Fib do 16 | use Jeeves.Pooled 17 | 18 | def fib(n), do: _fib(n) 19 | 20 | defp _fib(0), do: 0 21 | defp _fib(1), do: 1 22 | defp _fib(n), do: _fib(n-1) + _fib(n-2) # terribly inefficient 23 | end 24 | ~~~ 25 | 26 | You'd start the pool using 27 | 28 | ~~~ elixir 29 | Fib.run 30 | ~~~ 31 | 32 | And invoke one of the pool of workers using 33 | 34 | ~~~ elixir 35 | Fib.fib(20) # => 6765 36 | ~~~ 37 | 38 | We can use GenServer state to cache already calculated values, making the 39 | process O(n) rather than O(1.6ⁿ). 40 | 41 | ~~~ elixir 42 | defmodule Fib do 43 | use Jeeves.Pooled, 44 | state: %{ 0 => 0, 1 => 1}, 45 | state_name: :cache 46 | 47 | def fib(n) do 48 | case cache[n] do 49 | nil -> 50 | fib_n = fib(n-2, cache) + fib(n-1, cache) 51 | update_state(Map.put(cache, n, fib_n)) do 52 | fib_n 53 | end 54 | cached_result -> 55 | cached_result 56 | end 57 | end 58 | end 59 | ~~~ 60 | 61 | In the previous example each worker maintains its own cache. In the 62 | Fibonacci example, that's fine, as the cost of loading the cache is 63 | small. But if we _wanted_ to share a single cache between all workers, 64 | we can add it as a named service: 65 | 66 | ~~~ elixir 67 | defmodule FibCache do 68 | use Jeeves.Named, state: %{ 0 => 0, 1 => 1 } 69 | 70 | def get(n), do: state[n] 71 | def put(n, fib_n) do 72 | state 73 | |> Map.put(n, fib_n) 74 | |> update_state(do: fib_n) 75 | end 76 | end 77 | 78 | defmodule Fib do 79 | use Jeeves.Pooled, 80 | 81 | def fib(n) do 82 | case FibCache.get(n) do 83 | nil -> 84 | with fib_n = fib(n-2) + fib(n-1), 85 | do: FibCache.put(n, fib_n) # => returns result 86 | cached_result -> 87 | cached_result 88 | end 89 | end 90 | end 91 | ~~~ 92 | 93 | 94 | end _tl;dr_ 95 | ---- 96 | 97 | # Jeeves—at your service 98 | 99 | Erlang encourages us to write our code as self-contained servers and 100 | applications. Elixir makes it even easier by removing much of the 101 | boilerplate needed to create an Erlang GenServer. 102 | 103 | However, creating these servers is often more work than it needs to 104 | be. And, unfortunately, following good design practices adds even more 105 | work, in the form of duplication. 106 | 107 | The Jeeves library aims to make it easier for newcomers to craft 108 | well designed services. It doesn't replace GenServer. It is simply a 109 | layer on top that handles the most common GenServer use cases. The 110 | intent is to remove any excuse that people might have for not writing 111 | their Elixir code using a ridiculously large number of trivial 112 | services. 113 | 114 | # Basic Example 115 | 116 | You can think of an Erlang process as a remarkably pure implementation 117 | of an object. It is self contained, with private state, and with an 118 | interface that is accessed by sending messages. This harks straight 119 | back to the early days of Smalltalk. 120 | 121 | Jeeves draws on that idea. When you include it in a module, that 122 | module's public functions become the interface to the service. You 123 | write the functions, and Jeeves rewrites them into a GenServer. 124 | 125 | Here's a simple service that implements a key-value store. 126 | 127 | ~~~ elixir 128 | defmodule KVStore do 129 | 130 | use Jeeves.Anonymous, state: %{} 131 | 132 | def put(store, key, value) do 133 | set_state(Map.put(store, key, value)) do 134 | value 135 | end 136 | end 137 | 138 | def get(store, key) do 139 | store[key] 140 | end 141 | end 142 | ~~~ 143 | 144 | The first parameter to `put` and `get` is the current state, and the second is 145 | the value being passed in. 146 | 147 | You'd call it using 148 | 149 | ~~~ elixir 150 | rt = KVStore.run() 151 | 152 | KVStore.put(rt, :name, "Elixir") 153 | KVStore.get(rt, :name) # => "Elixir" 154 | ~~~ 155 | 156 | Behind the scenes, Jeeves has created a pure implementation of our 157 | totaller, along with a GenServer that delegates to that 158 | implementation. What does that code look like? Add the `:show_code` 159 | option to our original source. 160 | 161 | ~~~ elixir 162 | defmodule KVStore do 163 | 164 | use Jeeves.Anonymous, 165 | state: %{}, 166 | show_code: true 167 | 168 | def put(store, key, value) do 169 | # . . . 170 | ~~~ 171 | 172 | During compilation, you'll see the code that will actually be run: 173 | 174 | 175 | ~~~ elixir 176 | 177 | # defmodule RunningTotal do 178 | 179 | import(Kernel, except: [def: 2]) 180 | import(Jeeves.Common, only: [def: 2, set_state: 2]) 181 | @before_compile({Jeeves.Anonymous, :generate_code_callback}) 182 | def run() do 183 | run(%{}) 184 | end 185 | def run(state) do 186 | {:ok, pid} = GenServer.start_link(__MODULE__, state, server_opts()) 187 | pid 188 | end 189 | def init(state) do 190 | {:ok, state} 191 | end 192 | def initial_state(default_state, _your_state) do 193 | default_state 194 | end 195 | def server_opts() do 196 | [] 197 | end 198 | defoverridable(initial_state: 2) 199 | 200 | use(GenServer) 201 | 202 | def put(store, key, value) do 203 | GenServer.call(store, {:put, key, value}) 204 | end 205 | def get(store, key) do 206 | GenServer.call(store, {:get, key}) 207 | end 208 | def handle_call({:put, key, value}, _, store) do 209 | __MODULE__.Implementation.put(store, key, value) 210 | |> Jeeves.Common.create_genserver_response(store) 211 | end 212 | def handle_call({:get, key}, _, store) do 213 | __MODULE__.Implementation.get(store, key) 214 | |> Jeeves.Common.create_genserver_response(store) 215 | end 216 | 217 | defmodule Implementation do 218 | def put(store, key, value) do 219 | set_state(Map.put(store, key, value)) do 220 | value 221 | end 222 | end 223 | def get(store, key) do 224 | store[key] 225 | end 226 | end 227 | 228 | # end 229 | ~~~ 230 | 231 | ### Testing 232 | 233 | We can test this implementation without starting a separate 234 | process by simply calling functions in `KVStore.Implementation`. You 235 | have to supply the state, and allow for the fact that the responses 236 | will include the updated state if `set_state` is called. 237 | 238 | ~~~ elixir 239 | alias KVI KVStore.Implementation 240 | 241 | @state %{} 242 | 243 | test "we can put a KV pair, then get it, retrieves the correct value" do 244 | { :reply, Elixir, state } = KVI.put(@state, :name, Elixir) 245 | assert KVI.get(state, name) == Elixir 246 | end 247 | ~~~ 248 | 249 | ## Creating a Named (Singleton) Service 250 | 251 | It's sometimes convenient to create a global, named, service. Logging 252 | is a good example of this, as are registry services, global caches, and 253 | the like. 254 | 255 | We can make out KV store a global _named service_ with some trivial changes: 256 | 257 | ~~~ elixir 258 | defmodule NamedKVStore do 259 | 260 | use Jeeves.Named, 261 | state_name: :kvs, 262 | state: %{}, 263 | 264 | def put(key, value) do 265 | set_state(Map.put(kvs, key, value)) do 266 | value 267 | end 268 | end 269 | 270 | def get(key) do 271 | kvs[key] 272 | end 273 | end 274 | ~~~ 275 | 276 | Notice there's a bit of magic here. A named service can be called from 277 | anywhere in your code. It doesn't require you to remember a PID or any 278 | other handle, as the service's API invokes the service process by 279 | name. However, the service process itself contains state (the map in 280 | the KVStore example). The client doesn't need to know about this 281 | internal state, so it is never exposed via the API. Instead, it is 282 | automatically made available inside the service's functions in a 283 | variable. By default, this variable is called `state`, but the 284 | NamedKVStore example changes this to something more meaningful, `kvs`. 285 | 286 | You'd call the named KV store service using 287 | 288 | ~~~ elixir 289 | NamedKVStore.put(:name, "Elixir") 290 | NamedKVStore.put(:engine, "BEAM") 291 | NamedKVStore.get(:name) # => "Elixir" 292 | ~~~ 293 | 294 | ## Pooled Services 295 | 296 | Named services can be turned into pools of workers by changing to `Jeeves.Pooled`. 297 | 298 | ~~~ elixir 299 | defmodule TwitterFeed do 300 | 301 | use Jeeves.Pooled, 302 | pool: [ min: 5, max: 20 ] 303 | 304 | def fetch(name) do 305 | # ... 306 | end 307 | end 308 | ~~~ 309 | 310 | Calls to `TwitterFeed.fetch` would run in parallel, up to a maximum of 311 | 20 processes. 312 | 313 | 314 | ## Inline code 315 | 316 | Finally, we can tell Jeeves not to generate a server at all. 317 | 318 | ~~~ elixir 319 | defmodule RunningTotal do 320 | 321 | use Jeeves.Inline, state: 0 322 | 323 | def add(total, value) do 324 | set_state(value+total) 325 | end 326 | end 327 | ~~~ 328 | 329 | The cool thing is we can switch between not running a process, running 330 | a single server, or running a pool of servers by changing a single 331 | declaration in the module. 332 | 333 | # More Information 334 | 335 | * Anonymous services: 336 | * documentation: `Jeeves.Anonymous` 337 | * [example](./examples/apps/anon_worker) 338 | 339 | * Named services: 340 | * documentation: `Jeeves.Named` 341 | * [example](./examples/apps/named_worker) 342 | 343 | * Pooled services: 344 | * documentation: `Jeeves.Pooled` 345 | * [example](./examples/apps/pooled_named_worker) 346 | 347 | * [Some background](./background.md) 348 | 349 | # To do 350 | 351 | * [ ] Implement anonymous pools 352 | * [ ] Add declarative supervision (when child_spec becomes available) 353 | * [ ] Tests! 354 | 355 | # Author 356 | 357 | Dave Thomas (dave@pragdave.me, @pragdave) 358 | 359 | License: see the file [LICENSE.md](./LICENSE.html) 360 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /background.md: -------------------------------------------------------------------------------- 1 | # Types of Services 2 | 3 | ## Named vs. Anonymous 4 | 5 | There's a fundamental difference between named and anonymous servers. 6 | The difference isn't one of implementation—the two forms are basically 7 | the same. It's a difference of intent. 8 | 9 | A named server is effectively a singleton—a global object. If it has 10 | state, that state is only meaningful globally—it is not the property 11 | of any one client process. Loggers, time services, and so on are all 12 | examples of named, singleton servers. 13 | 14 | Anonymous services have a different intent. Because they are accessed 15 | via a handle (a reference or) pid, they have an owner—the process that 16 | has that handle. Ownership can be shared, but that sharing is 17 | controlled by the original creator of the server. 18 | 19 | The state of anonymous servers can therefore be specific to a 20 | particular owner. If we use a server to have a conversation with a 21 | remote service, for example, we own that server, and it can maintain 22 | the state of the connection between requests. 23 | 24 | ## Solo vs Pooled 25 | 26 | A solo service is a single process. When a client makes a request, 27 | that process is executed. No other client can run code in that service 28 | until it is idle again. 29 | 30 | A pooled service contains zero or more workers. Each worker is 31 | equivalent to a solo service, in that it handles work requests for one 32 | client at a time. However, a pooled service also contains a pool 33 | manager (or scheduler). Requests for the pool service are not sent to 34 | the worker directly. Instead, they are sent to the scheduler. If it 35 | has a free worker process, it forwards on the request. If it does not, 36 | it delays execution of the caller until a worker becomes free (or a 37 | timeout occurs). 38 | 39 | A pooled service can be configured to support some maximum number of 40 | workers. It will dynamically create new workers to handle requests if 41 | all existing workers are busy _and_ if the worker count is less than 42 | the maximum. 43 | 44 | Pooled services are useful when you want to achieve parallelism and 45 | the work performed by the service is likely to run for a nontrivial 46 | time. They are also useful as a way to hold on to expensive resources 47 | (such as database connections). 48 | 49 | # Coding Model 50 | 51 | Permuting named and anonymous services with solo and pooled 52 | provisioning gives us four categories of service. 53 | 54 | From a coding standpoint, though, the most significant difference is 55 | between named and anonymous services (whether they are solo or 56 | pooled). 57 | 58 | A named service has its own state. That state is not passed to it on 59 | each request. 60 | 61 | An anonymous service (logically) does not have a single state. 62 | Instead, the state is established for it each time it is called 63 | (typically by passing it a server pid). 64 | 65 | This means that our APIs will be different. For a named service, such 66 | as a logger, we might write: 67 | 68 | ~~~ elixir 69 | Logger.info("Starting") 70 | ~~~ 71 | 72 | For an anonymous service, such as a database connection, we need to 73 | create it to get a handle, and then pass that handle to it on each 74 | request. 75 | 76 | ~~~ elixir 77 | { :ok, handle } = DBConnection.start_link(«params») 78 | # ... 79 | DB.insert(handle, «stuff») 80 | ~~~ 81 | 82 | Because of this, Jeeves comes in two flavors, one for named 83 | services and one for anonymous ones. 84 | 85 | ## Defining a Named Service 86 | 87 | ~~~ elixir 88 | defmodule MyLogger do 89 | 90 | defstruct device: :stdio 91 | 92 | use Jeeves.Named, 93 | state: [ logger: %MyLogger{} ] 94 | 95 | def info(msg) do 96 | IO.puts(logger.device, "-- #{msg}") 97 | end 98 | 99 | def set_device(new_device) do 100 | set_state(%{ logger | device: new_device}) do 101 | :ok 102 | end 103 | end 104 | 105 | end 106 | ~~~ 107 | 108 | By default, `MyLogger` will be spawned when the application starts. 109 | The service will (by default) have the same name as the module 110 | (`Elixir.MyLogger` in this case). 111 | 112 | Its state is defined by the clause: 113 | 114 | ~~~ elixir 115 | state: [ logger: %MyLogger{} ] 116 | ~~~ 117 | 118 | Inside the module's public functions, the state 119 | will be made available via the variable `logger`. The initial value of 120 | the state will be the struct defined in this same module. 121 | 122 | Yes, this is magic, and it's probably frowned on by José. However, 123 | doing this makes the module's API consistent. You call an API function 124 | using the signature in the module definition. It's really no different 125 | to the implicit `this` variable in OO code. 126 | 127 | ## Defining an Anonymous Service 128 | 129 | ~~~ elixir 130 | defmodule KVStore do 131 | 132 | use Jeeves.Anonymous 133 | state: %{} 134 | 135 | def put(store, key, value) do 136 | set_state(Map.put(store, key, value), do: value 137 | end 138 | 139 | def get(store, key) do 140 | store[key] 141 | end 142 | 143 | end 144 | ~~~ 145 | 146 | Here's how you'd use this: 147 | 148 | ~~~ elixir 149 | handle = KVStore.run() 150 | KVStore.put(handle, :name, "José") 151 | IO.puts KVStore.get(handle, :name) 152 | ~~~ 153 | 154 | What if you wanted to pass some initial values into the store? There 155 | are a couple of techniques. First, any parameter passed to `run()` 156 | will by default become the initial state. 157 | 158 | But if your service has some specific internal formatting that must be 159 | applied to this state, simply provide an `init_state()` function. This 160 | receives the default state and the parameter passed to `run()`, and 161 | returns the updated state. 162 | 163 | 164 | ~~~ elixir 165 | defmodule KVStore do 166 | 167 | use Jeeves.Anonymous 168 | state: %{} 169 | 170 | def init_state(default_state, initial_values) when is_list(initial_values) do 171 | initial_values |> Enum.into(default_state) 172 | end 173 | 174 | # ... 175 | end 176 | ~~~ 177 | 178 | Call this with: 179 | 180 | ~~~ elixir 181 | handle = KVStore.run(name: "Jose", language: "Elixir") 182 | 183 | KVStore.put(handle, :name, "José") 184 | 185 | IO.puts KVStore.get(handle, :name) # => José 186 | IO.puts KVStore.get(handle, :language) # => Elixir 187 | ~~~ 188 | 189 | ## Creating Pooled Services 190 | 191 | You can create both named and anonymous pooled services. Simply add 192 | the `pool:` option: 193 | 194 | ~~~ elixir 195 | defmodule DBConnection do 196 | defstruct conn: nil, created: fn () -> DateTime.utc_now() end 197 | 198 | use(Jeeves.Named, 199 | state: [ db: %DBConnection{} ], 200 | pool: [ min: 2, max: 10, retire_after: 5*60 ]) 201 | 202 | def init_state(state, _) do 203 | connection_params = Application.fetch_env(app, :db_params) 204 | conn = PG.connect(connection_params) 205 | %{ state | conn: conn } 206 | end 207 | 208 | def execute(stmt) do 209 | PG.execute(db.conn, stmt) 210 | end 211 | # ... 212 | end 213 | ~~~ 214 | 215 | This creates a pool of database connections. It will always have at 216 | least 2 workers running, and may have up to 10. Idle workers will be 217 | culled after 5 minutes. 218 | 219 | Here's how it is called: 220 | 221 | ~~~ elixir 222 | result = DBConnection.execute("select * from table1") 223 | ~~~ 224 | 225 | 226 | #### anon pools NYI 227 | You can also create anonymous pools. As with other anonymous services, 228 | you'll need to keep track of the handle. 229 | 230 | Here's the same database connection pool code, but set up to allow you 231 | to create different pools for different databases. 232 | 233 | ~~~ elixir 234 | defmodule DBConnection do 235 | defstruct conn: nil, created: fn () -> DateTime.utc_now() end 236 | 237 | use(Jeeves.Pool, 238 | state: [ db: %DBConnection{} ], 239 | pool: [ min: 2, max: 10, retire_after: 5*60 ]) 240 | 241 | def init_state(state, connection_params) do 242 | conn = PG.connect(connection_params) 243 | %{ state | conn: conn } 244 | end 245 | 246 | def execute(stmt) do 247 | PG.execute(db.conn, stmt) 248 | end 249 | # ... 250 | end 251 | ~~~ 252 | 253 | The only changes were to alter the type to `Jeeves.Anonymous` 254 | and to change the `new` function to accept the database connection to 255 | be used. 256 | 257 | 258 | # Supervision 259 | 260 | (to be implemented using JVs child_spec proposal) 261 | 262 | ## Pooled Supervision 263 | 264 | When you create a pooled service, Jeeves always creates a couple of 265 | supervisors and a scheduler process. This looks like the following: 266 | 267 | 268 | +--------------------+ 269 | | | 270 | | Pool Supervisor | 271 | / | |\ 272 | / +--------------------+ \ 273 | / \ 274 | +--------------------+ +--------------------+ 275 | | | | | 276 | | Scheduler | | Worker Supervisor | 277 | | | | | 278 | +--------------------+ +--------------------+ 279 | | 280 | | 281 | v 282 | +-----------------+ 283 | +------------------+ | 284 | +--------------------+ | | 285 | | | | | 286 | | Workers | |--+ 287 | | |--+ 288 | +--------------------+ 289 | 290 | 291 | When your client code accesses a pooled service, it is actually 292 | talking to the scheduler process. 293 | 294 | 295 | ## State Protection (NYI) 296 | 297 | If you add the option `protect_state: true`, Jeeves will automatically 298 | create an additional top-level supervisor and a vault process that does 299 | nothing but save the state of services between requests. Should a 300 | service crash, the supervisor will restart it using the saved state 301 | from the vault, rather than using the default state. 302 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :service, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:service, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | **TODO: Add description** 4 | 5 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/README.md: -------------------------------------------------------------------------------- 1 | # AnonWorker 2 | 3 | 4 | $ iex -S mix 5 | 6 | iex> handle = KVStore.run language: "elixir" 7 | #PID<> 8 | 9 | iex> KVStore.put handle, :name, "josé" 10 | "josé" 11 | 12 | iex> KVStore.get handle, :name 13 | "josé" 14 | 15 | iex> KVStore.get handle, :language 16 | "elixir" 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :anon_worker, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:anon_worker, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/lib/anon_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule KVStore do 2 | 3 | use Jeeves.Anonymous, state: %{}, show_code: true 4 | 5 | def put(store, key, value) do 6 | set_state(Map.put(store, key, value)) do 7 | value 8 | end 9 | end 10 | 11 | def get(store, key) do 12 | store[key] 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AnonWorker.Mixfile do 2 | use Mix.Project 3 | 4 | @name :anon_worker 5 | @version "0.1.0" 6 | @deps [ 7 | service: [ path: "../../.." ] 8 | ] 9 | 10 | ################################################## 11 | 12 | def project do 13 | [ 14 | app: @name, 15 | version: @version, 16 | deps: @deps, 17 | build_path: "../../_build", 18 | config_path: "../../config/config.exs", 19 | deps_path: "../../deps", 20 | lockfile: "../../mix.lock", 21 | elixir: "~> 1.5-dev", 22 | ] 23 | end 24 | 25 | def application do 26 | [ ] 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/test/anon_worker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnonWorkerTest do 2 | use ExUnit.Case 3 | 4 | test "the implementation works" do 5 | state = %{ name: "test" } 6 | 7 | assert KVStore.Implementation.get(state, :name) == "test" 8 | assert KVStore.Implementation.put(state, :name, "case") == {:reply, "case", %{name: "case"}} 9 | 10 | end 11 | 12 | test "the worker runs" do 13 | kv = KVStore.run() 14 | assert is_pid(kv) 15 | KVStore.put(kv, :name, :test) 16 | assert KVStore.get(kv, :name) == :test 17 | end 18 | 19 | test "initializing via run()" do 20 | kv = KVStore.run(%{ name: :initialized }) 21 | assert is_pid(kv) 22 | assert KVStore.get(kv, :name) == :initialized 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /examples/apps/anon_worker/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/named_worker/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /examples/apps/named_worker/README.md: -------------------------------------------------------------------------------- 1 | # Named Worker 2 | 3 | $ iex -S mix 4 | iex(1)> NamedKVStore.run %{name: "Dave"} 5 | #PID<0.186.0> 6 | 7 | iex(2)> NamedKVStore.put :born, "UK" 8 | "UK" 9 | 10 | iex(3)> NamedKVStore.get :name 11 | "Dave" 12 | 13 | iex(4)> NamedKVStore.get :born 14 | "UK" 15 | 16 | -------------------------------------------------------------------------------- /examples/apps/named_worker/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :anon_worker, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:anon_worker, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/apps/named_worker/lib/named_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule NamedKVStore do 2 | 3 | use Jeeves.Named, state_name: :kvs, state: %{}, service_name: :alfred 4 | 5 | def put(key, value) do 6 | set_state(Map.put(kvs, key, value)) do 7 | value 8 | end 9 | end 10 | 11 | def get(key) do 12 | kvs[key] 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /examples/apps/named_worker/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NamedWorker.Mixfile do 2 | use Mix.Project 3 | 4 | @name :named_worker 5 | @version "0.1.0" 6 | @deps [ 7 | service: [ path: "../../.." ] 8 | ] 9 | 10 | ################################################## 11 | 12 | def project do 13 | [ 14 | app: @name, 15 | version: @version, 16 | deps: @deps, 17 | build_path: "../../_build", 18 | config_path: "../../config/config.exs", 19 | deps_path: "../../deps", 20 | lockfile: "../../mix.lock", 21 | elixir: "~> 1.5-dev", 22 | ] 23 | end 24 | 25 | def application do 26 | [ ] 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /examples/apps/named_worker/test/named_worker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NamedWorkerTest do 2 | use ExUnit.Case 3 | 4 | alias NamedKVStore, as: KV 5 | alias NamedKVStore.Implementation, as: KVI 6 | 7 | test "the implementation works" do 8 | state = %{ name: "test" } 9 | 10 | assert KVI.get(state, :name) == "test" 11 | assert KVI.put(state, :name, "case") == {:reply, "case", %{name: "case"}} 12 | end 13 | 14 | test "the worker runs" do 15 | KV.run() 16 | KV.put(:name, :test) 17 | assert KV.get(:name) == :test 18 | end 19 | 20 | test "initializing via run()" do 21 | KV.run(%{ name: :initialized }) 22 | assert KV.get(:name) == :initialized 23 | end 24 | 25 | test "service name is correct" do 26 | assert Process.whereis(:alfred) == nil 27 | KV.run 28 | assert is_pid(Process.whereis(:alfred)) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /examples/apps/named_worker/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/README.md: -------------------------------------------------------------------------------- 1 | # Pooled Named Worker 2 | 3 | $ iex -S mix 4 | iex(1)> PooledNamedWorker.run 5 | [worker_module: PooledNamedWorker.Worker, pool_opts: [min: 1, max: 4], 6 | name: Vince, state: %{a: 1}] 7 | [worker_module: PooledNamedWorker.Worker, pool_opts: [min: 1, max: 4], 8 | name: Vince, state: %{a: 1}] 9 | [name: {:local, Vince}, worker_module: PooledNamedWorker.Worker, size: 1, 10 | max_overflow: 3] 11 | {:ok, #PID<0.214.0>} 12 | 13 | iex(2)> PooledNamedWorker.process 14 | in pool worker #PID<0.217.0> %{a: 1} 15 | "#PID<0.217.0> done" 16 | 17 | iex(3)> PooledNamedWorker.process 18 | in pool worker #PID<0.217.0> %{a: 2} 19 | "#PID<0.217.0> done" 20 | 21 | iex(4)> PooledNamedWorker.process 22 | in pool worker #PID<0.217.0> %{a: 3} 23 | "#PID<0.217.0> done" 24 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :anon_worker, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:anon_worker, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/lib/pooled_named_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule PooledNamedWorker do 2 | 3 | use( 4 | Jeeves.Pooled, 5 | state: %{ a: 1 }, 6 | service_name: Vince, 7 | pool: [ min: 1, max: 4 ], 8 | debug: true 9 | ) 10 | 11 | def process() do 12 | IO.puts "in pool worker #{inspect self()} #{inspect state}" 13 | :timer.sleep(1000) 14 | set_state(%{ state | a: state.a + 1 }) do 15 | "#{inspect self()} done" 16 | end 17 | end 18 | 19 | end 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PooledNamedWorker.Mixfile do 2 | use Mix.Project 3 | 4 | @name :pooled_named_worker 5 | @version "0.1.0" 6 | @deps [ 7 | service: [ path: "../../.." ] 8 | ] 9 | 10 | ################################################## 11 | 12 | def project do 13 | [ 14 | app: @name, 15 | version: @version, 16 | deps: @deps, 17 | build_path: "../../_build", 18 | config_path: "../../config/config.exs", 19 | deps_path: "../../deps", 20 | lockfile: "../../mix.lock", 21 | elixir: "~> 1.5-dev", 22 | ] 23 | end 24 | 25 | def application do 26 | [ 27 | # extra_applications: [:logger, :poolboy] 28 | ] 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/test/pooled_named_worker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PooledNamedWorkerTest do 2 | use ExUnit.Case 3 | 4 | alias NamedKVStore, as: KV 5 | alias NamedKVStore.Implementation, as: KVI 6 | 7 | test "the implementation works" do 8 | state = %{ name: "test" } 9 | 10 | assert KVI.get(state, :name) == "test" 11 | assert KVI.put(state, :name, "case") == {:reply, "case", %{name: "case"}} 12 | end 13 | 14 | test "the worker runs" do 15 | KV.run() 16 | KV.put(:name, :test) 17 | assert KV.get(:name) == :test 18 | end 19 | 20 | test "initializing via run()" do 21 | KV.run(%{ name: :initialized }) 22 | assert KV.get(:name) == :initialized 23 | end 24 | 25 | test "service name is correct" do 26 | assert Process.whereis(:alfred) == nil 27 | KV.run 28 | assert is_pid(Process.whereis(:alfred)) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /examples/apps/pooled_named_worker/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, ensuring 7 | # they all use the same configuration. While one could 8 | # configure all applications here, we prefer to delegate 9 | # back to each application for organization purposes. 10 | import_config "../apps/*/config/config.exs" 11 | 12 | # Sample configuration (overrides the imported configuration above): 13 | # 14 | # config :logger, :console, 15 | # level: :info, 16 | # format: "$date $time [$level] $metadata$message\n", 17 | # metadata: [:user_id] 18 | -------------------------------------------------------------------------------- /examples/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Examples.Mixfile do 2 | use Mix.Project 3 | 4 | @deps [ 5 | service: [ path: ".." ] 6 | ] 7 | 8 | ################################################## 9 | 10 | @in_production Mix.env == :prod 11 | 12 | def project do 13 | [ 14 | apps_path: "apps", 15 | build_embedded: @in_production, 16 | start_permanent: @in_production, 17 | deps: @deps 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/mix.lock: -------------------------------------------------------------------------------- 1 | %{"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}} 2 | -------------------------------------------------------------------------------- /lib/jeeves.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves do 2 | end 3 | -------------------------------------------------------------------------------- /lib/jeeves/anonymous.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Anonymous do 2 | 3 | @moduledoc """ 4 | Implement an anonymous service. 5 | 6 | ### Usage 7 | 8 | To create the service: 9 | 10 | * Create a module that implements the API you want. This API will be 11 | expressed as a set of public functions. Each function will be 12 | defined to accept the current state as its first parameter. If a 13 | function wants to change the state, it must end with a call to the 14 | `Jeeves.Common.update_state/2` function (which will have been 15 | imported into your module automatically). 16 | 17 | For this example, we'll call the module `MyService`. 18 | 19 | * Add the line `use Jeeves.Anonymous` to the top of this module. 20 | 21 | To consume the service: 22 | 23 | * Create an instance of the service with `MyJeeves.run()`. You can pass 24 | initial state to the service as an optional parameter. This call returns 25 | a handle to this service instance. 26 | 27 | * Call the API functions in the service, using the handle as a first parameter. 28 | 29 | 30 | ### Example 31 | 32 | defmodule Accumulator do 33 | using Jeeves.Anonymous, state: 0 34 | 35 | def current_value(acc), do: acc 36 | def increment(acc, by \\ 1) do 37 | update_state(acc + by) 38 | end 39 | end 40 | 41 | with acc = Accumulator.run(10) do 42 | Accumulator.increment(acc, 3) 43 | Accumulator.increment(acc, 2) 44 | Accumulator.current_value(acc) # => 15 45 | end 46 | 47 | 48 | ### Options 49 | 50 | You can pass a keyword list to `use Jeeves.Anonymous:` 51 | 52 | * `state:` _value_ 53 | 54 | Set the detail initial state of the service to `value`. This can be 55 | overridden by passing a different value to the `run` function. 56 | 57 | * `showcode:` _boolean_ 58 | 59 | If truthy, dump a representation of the generated code to STDOUT during 60 | compilation. 61 | 62 | """ 63 | 64 | @doc false 65 | defmacro __using__(opts \\ []) do 66 | Jeeves.Common.generate_common_code( 67 | __CALLER__.module, 68 | __MODULE__, 69 | opts, 70 | _name = nil) 71 | end 72 | 73 | @doc false 74 | defmacro generate_code_callback(_) do 75 | Jeeves.Common.generate_code(__CALLER__.module, __MODULE__) 76 | end 77 | 78 | @doc false 79 | def generate_api_call(_options, {call, _body}) do 80 | quote do 81 | def(unquote(call), do: unquote(api_body(call))) 82 | end 83 | end 84 | 85 | @doc false 86 | defp api_body(call) do 87 | { server, request } = call_signature(call) 88 | quote do 89 | GenServer.call(unquote(var!(server)), unquote(request)) 90 | end 91 | end 92 | 93 | @doc false 94 | def generate_handle_call(_options, {call, _body}) do 95 | { state, request } = call_signature(call) 96 | quote do 97 | def handle_call(unquote(request), _, unquote(var!(state))) do 98 | __MODULE__.Implementation.unquote(call) 99 | |> Jeeves.Common.create_genserver_response(unquote(var!(state))) 100 | end 101 | end 102 | end 103 | 104 | @doc false 105 | def generate_implementation(_options, {call, body}) do 106 | quote do 107 | def(unquote(call), unquote(body)) 108 | end 109 | end 110 | 111 | 112 | @doc !"only used for pools" 113 | def generate_delegator(_options, {_call, _body}), do: nil 114 | 115 | # given def fred(store, a, b) return { store, { :fred, a, b }} 116 | @doc false 117 | def call_signature({ name, _, [ server | args ] }) do 118 | { 119 | var!(server), 120 | { :{}, [], [ name | Enum.map(args, fn a -> var!(a) end) ] } 121 | } 122 | end 123 | 124 | end 125 | -------------------------------------------------------------------------------- /lib/jeeves/common.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Common do 2 | 3 | alias Jeeves.Util.PreprocessorState, as: PS 4 | 5 | 6 | @dont_override [ 7 | :initial_state, 8 | :setup_worker_state, 9 | :start, 10 | :start_link, 11 | :stop, 12 | ] 13 | 14 | @in_genserver [ 15 | :init, 16 | :handle_call, 17 | :handle_cast, 18 | :handle_info, 19 | ] 20 | 21 | 22 | @doc """ 23 | We replace the regular def with something that records the definition in 24 | a list. No code is emitted here—that happens in the before_compile hook 25 | """ 26 | defmacro def(call = {name, _, _}, body) when name in @dont_override do 27 | quote do 28 | Kernel.def(unquote(call), unquote(body)) 29 | end 30 | end 31 | 32 | defmacro def(call = {name, _, _}, body) when name in @in_genserver do 33 | quote do 34 | Kernel.def(unquote(call), unquote(body)) 35 | end 36 | end 37 | 38 | defmacro def(call, body), do: def_implementation(__CALLER__.module, call, body) 39 | 40 | # so I can test 41 | def def_implementation(caller, call, body) do 42 | PS.add_function(caller, { call, body }) 43 | nil 44 | end 45 | 46 | @doc """ 47 | Used at the end of a service function to indicate that 48 | the state should be updated, and to provide a return value. The 49 | new state is passed as a parameter, and a `do` block is 50 | evaluated to provide the return value. 51 | 52 | If not called in a service function, then the return value of the 53 | function will be the value returned to the client, and the state 54 | will not be updated. 55 | 56 | def put(store, key, value) do 57 | set_state(Map.put(store, key, value)) do 58 | value 59 | end 60 | end 61 | 62 | 63 | With no do: block, returns the new state as the reply value. 64 | """ 65 | 66 | defmacro set_state(new_state) do 67 | quote bind_quoted: [ state: new_state ] do 68 | { :reply, state, state } 69 | end 70 | end 71 | 72 | 73 | defmacro set_state(new_state, do: return) do 74 | quote do 75 | { :reply, unquote(return), unquote(new_state) } 76 | end 77 | end 78 | 79 | 80 | # The strategy is the module (Anonymous, Named, Pooled) 81 | 82 | @doc false 83 | def generate_common_code(caller, strategy, opts, name) do 84 | PS.start_link(caller, opts) 85 | 86 | default_state = Keyword.get(opts, :state, :no_state) 87 | server_opts = if name do [ name: name ] else [ ] end 88 | 89 | quote do 90 | import Kernel, except: [ def: 2 ] 91 | import Jeeves.Common, only: [ def: 2, set_state: 1, set_state: 2 ] 92 | 93 | @before_compile { unquote(strategy), :generate_code_callback } 94 | 95 | def run() do 96 | run(unquote(default_state)) 97 | end 98 | 99 | def run(state_override) do 100 | state = initial_state(state_override, unquote(default_state)) 101 | { :ok, pid } = GenServer.start_link(__MODULE__, state, server_opts()) 102 | pid 103 | end 104 | 105 | def start_link(state_override) do 106 | { :ok, run(state_override) } 107 | end 108 | 109 | def init(state) do 110 | { :ok, state } 111 | end 112 | 113 | def initial_state(override, _default) do 114 | override 115 | end 116 | 117 | def server_opts() do 118 | unquote(server_opts) 119 | end 120 | 121 | defoverridable [ initial_state: 2, init: 1 ] 122 | end 123 | |> maybe_show_generated_code(opts) 124 | end 125 | 126 | @doc false 127 | def generate_code(caller, strategy) do 128 | 129 | { options, apis, handlers, implementations, _delegators } = 130 | create_functions_from_originals(caller, strategy) 131 | 132 | PS.stop(caller) 133 | 134 | quote do 135 | use GenServer 136 | 137 | unquote_splicing(apis) 138 | unquote_splicing(handlers) 139 | defmodule Implementation do 140 | unquote_splicing(implementations) 141 | end 142 | end 143 | |> maybe_show_generated_code(options) 144 | end 145 | 146 | @doc false 147 | def create_functions_from_originals(caller, strategy) do 148 | options = PS.options(caller) 149 | 150 | PS.function_list(caller) 151 | |> Enum.reduce({nil, [], [], [], []}, &generate_functions(strategy, options, &1, &2)) 152 | end 153 | 154 | 155 | @doc !"public only for testing" 156 | def generate_functions( 157 | strategy, 158 | options, 159 | original_fn, 160 | {_, apis, handlers, impls, delegators} 161 | ) 162 | do 163 | { 164 | options, 165 | [ strategy.generate_api_call(options, original_fn) | apis ], 166 | [ strategy.generate_handle_call(options, original_fn) | handlers ], 167 | [ strategy.generate_implementation(options, original_fn) | impls ], 168 | [ strategy.generate_delegator(options, original_fn) | delegators ] 169 | } 170 | end 171 | 172 | 173 | @doc !"public only for testing" 174 | def create_genserver_response(response = {:reply, _, _}, _state) do 175 | response 176 | end 177 | 178 | @doc false 179 | def create_genserver_response(response, state) do 180 | { :reply, response, state } 181 | end 182 | 183 | @doc false 184 | def maybe_show_generated_code(code, opts) do 185 | if opts[:show_code] do 186 | IO.puts "" 187 | code 188 | |> Macro.to_string() 189 | |> String.replace(~r{^\w*\(}, "") 190 | |> String.replace(~r{\)\w*$}, "") 191 | |> String.replace(~r{def\((.*?)\)\)}, "def \\1)") 192 | |> IO.puts 193 | end 194 | code 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /lib/jeeves/named.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Named do 2 | 3 | @moduledoc """ 4 | Implement a singleton (global) named service. 5 | 6 | ### Usage 7 | 8 | To create the service: 9 | 10 | * Create a module that implements the API you want. This API will be 11 | expressed as a set of public functions. Each function will automatically 12 | receive the current state in a variable (by default named `state`). There is 13 | not need to declare this as a 14 | parameter.[[why?]](background.html#why-magic-state). 15 | If a function wants to change the state, it must end with a call to the 16 | `Jeeves.Common.update_state/2` function (which will have been 17 | imported into your module automatically). 18 | 19 | For this example, we'll call the module `NamedService`. 20 | 21 | * Add the line `use Jeeves.Named` to the top of this module. 22 | 23 | To consume the service: 24 | 25 | * Create an instance of the service with `NamedJeeves.run()`. You can pass 26 | initial state to the service as an optional parameter. This call returns 27 | a handle to this service instance, but you shouldn't use it. 28 | 29 | * Call the API functions in the service. 30 | 31 | 32 | ### Example 33 | 34 | defmodule KV do 35 | using Jeeves.Named, state: %{} 36 | 37 | def get(name), do: state[name] 38 | def put(name, value) do 39 | update_state(Map.put(state, name, value)) do 40 | value 41 | end 42 | end 43 | end 44 | 45 | KV.run(%{ name: "Elixir" }) 46 | KV.put(:type, "language") 47 | KV.get(:name) # => "Elixir" 48 | KV.get(:type) # => "language" 49 | 50 | 51 | ### Options 52 | 53 | You can pass a keyword list to `use Jeeves.Anonymous:` 54 | 55 | * `state:` _value_ 56 | 57 | * `state_name:` _atom_ 58 | 59 | The default name for the state variable is (unimaginatively) `state`. 60 | Use `state_name` to override this. For example, you could change the 61 | previous example to use `store` for the state with: 62 | 63 | defmodule KV do 64 | using Jeeves.Named, state: %{}, state_name: :store 65 | 66 | def get(name), do: store[name] 67 | def put(name, value) do 68 | update_state(Map.put(store, name, value)) do 69 | value 70 | end 71 | end 72 | end 73 | 74 | * `service_name:` _atom_ 75 | 76 | The default name for the service is the name of the module that defines it. 77 | Use `service_name:` to change this. 78 | 79 | * `showcode:` _boolean_ 80 | 81 | If truthy, dump a representation of the generated code to STDOUT during 82 | compilation. 83 | 84 | """ 85 | 86 | 87 | require Jeeves.Common 88 | 89 | @doc false 90 | defmacro __using__(opts \\ []) do 91 | Jeeves.Common.generate_common_code( 92 | __CALLER__.module, 93 | __MODULE__, 94 | opts, 95 | service_name(opts)) 96 | end 97 | 98 | @doc false 99 | defmacro generate_code_callback(_) do 100 | Jeeves.Common.generate_code(__CALLER__.module, __MODULE__) 101 | end 102 | 103 | @doc false 104 | def generate_api_call(options, {call, _body}) do 105 | quote do 106 | def(unquote(call), do: unquote(api_body(options, call))) 107 | end 108 | end 109 | 110 | @doc false 111 | defp api_body(options, call) do 112 | request = call_signature(call) 113 | quote do 114 | GenServer.call(unquote(service_name(options)), unquote(request)) 115 | end 116 | end 117 | 118 | @doc false 119 | def generate_handle_call(options, {call, _body}) do 120 | request = call_signature(call) 121 | api_call = api_signature(options, call) 122 | state_var = { state_name(options), [], nil } 123 | 124 | quote do 125 | def handle_call(unquote(request), _, unquote(state_var)) do 126 | __MODULE__.Implementation.unquote(api_call) 127 | |> Jeeves.Common.create_genserver_response(unquote(state_var)) 128 | end 129 | end 130 | end 131 | 132 | 133 | @doc false 134 | def generate_implementation(options, {call, body}) do 135 | quote do 136 | def(unquote(api_signature(options, call)), unquote(body)) 137 | end 138 | end 139 | 140 | # only used for pools 141 | @doc false 142 | def generate_delegator(_options, {_call, _body}), do: nil 143 | 144 | 145 | # given def fred(a, b) return { :fred, a, b } 146 | @doc false 147 | def call_signature({ name, _, args }) do 148 | { :{}, [], [ name | Enum.map(args, fn a -> var!(a) end) ] } 149 | end 150 | 151 | # given def fred(a, b) return def fred(«state name», a, b) 152 | 153 | @doc false 154 | def api_signature(options, { name, context, args }) do 155 | { name, context, [ { state_name(options), [], nil } | args ] } 156 | end 157 | 158 | @doc false 159 | def service_name(options) do 160 | options[:service_name] || quote(do: __MODULE__) 161 | end 162 | 163 | @doc false 164 | def state_name(options) do 165 | check_state_name(options[:state_name]) 166 | end 167 | 168 | defp check_state_name(nil), do: :state 169 | defp check_state_name(name) when is_atom(name), do: name 170 | defp check_state_name({name, _, _}) do 171 | raise CompileError, description: "state_name: “#{name}” should be an atom, not a variable" 172 | end 173 | defp check_state_name(name) do 174 | raise CompileError, description: "state_name: “#{inspect name}” should be an atom" 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/jeeves/pooled.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Pooled do 2 | 3 | @moduledoc """ 4 | Implement a singleton (global) named pool of services. 5 | 6 | Creates a dynamic pool of worker services. Each service shares an initial state, 7 | and each invocation of a service is independent from the previous one (so there 8 | is no concept of claiming a service for your dedicated use). 9 | 10 | ### Prerequisites 11 | 12 | You'll need to add poolboy to your project dependencies. 13 | 14 | ### Usage 15 | 16 | To create the service: 17 | 18 | * Create a module that implements the API you want. This API will be 19 | expressed as a set of public functions. Each function will automatically 20 | receive the current state in a variable (by default named `state`). There is 21 | not need to declare this as a parameter.[why?](#why-magic-state). 22 | If a function wants to change the state, it must end with a call to the 23 | `Jeeves.Common.update_state/2` function (which will have been 24 | imported into your module automatically). 25 | 26 | For this example, we'll call the module `PooledService`. 27 | 28 | * Add the line `use Jeeves.Pooled` to the top of this module. 29 | 30 | * Adjust the other options if required. 31 | 32 | To start the pool: 33 | 34 | PooledJeeves.run() 35 | 36 | or 37 | 38 | PooledJeeves.run(initial_state) 39 | 40 | To consume the service: 41 | 42 | * Call the API functions in the service. 43 | 44 | 45 | ### Example 46 | 47 | defmodule FaceDetector do 48 | using Jeeves.Pooled, 49 | state: %{ algorithm: ViolaJones }, 50 | state_name: :options, 51 | pool: [ min: 3, max: 10 ] 52 | 53 | def recognize(image) do 54 | # calls to OpenCV or whatever... 55 | end 56 | end 57 | 58 | ### Options 59 | 60 | You can pass a keyword list to `use Jeeves.Anonymous:` 61 | 62 | * `state:` _value_ 63 | 64 | The default value for the initial state of all workers. Can be overridden 65 | (again for all workers) by passing a value to `run()` 66 | 67 | * `state_name:` _atom_ 68 | 69 | The default name for the state variable is (unimaginatively) `state`. 70 | Use `state_name` to override this. For example, the previous 71 | example named the state `options`, and inside the `recognize` function 72 | your could write `options.algorithm` to look up the algorithm to use. 73 | 74 | * `name:` _atom_ 75 | 76 | The default name for the pool is the name of the module that defines it. 77 | Use `name:` to change this. 78 | 79 | * `pool: [ ` _options_ ` ]` 80 | 81 | Set options for the service pool. One or more of: 82 | 83 | * `min: n` 84 | 85 | The minimum number of workers that should be active, and by extension 86 | the number of workers started when the pool is run. Default is 2. 87 | 88 | * `max: n` 89 | 90 | The maximum number of workers. If all workers are busy and a new request 91 | arrives, a new worker will be started to handle it if the current worker 92 | count is less than `max`. Excess idle workers will be quietly killed off 93 | in the background. Default value is `(min+1)*2`. 94 | 95 | * `showcode:` _boolean_ 96 | 97 | If truthy, dump a representation of the generated code to STDOUT during 98 | compilation. 99 | 100 | * `timeout:` integer or float 101 | 102 | Specify the timeout to be used when the client calls workers in the pool. 103 | If all workers are busy, and none becomes free in that time, an OTP 104 | exception is raised. An integer specifies the timeout in milliseconds, and 105 | a float in seconds (so 1.5 is the same as 1500). 106 | 107 | """ 108 | 109 | 110 | alias Jeeves.Util.PreprocessorState, as: PS 111 | 112 | @doc false 113 | defmacro __using__(opts \\ []) do 114 | generate_pooled_service(__CALLER__.module, opts) 115 | end 116 | 117 | @doc false 118 | def generate_pooled_service(caller, opts) do 119 | name = Keyword.get(opts, :service_name, :no_name) 120 | state = Keyword.get(opts, :state, :no_state) 121 | 122 | PS.start_link(caller, opts) 123 | 124 | quote do 125 | import Kernel, except: [ def: 2 ] 126 | import Jeeves.Common, only: [ def: 2, set_state: 1, set_state: 2 ] 127 | 128 | @before_compile { unquote(__MODULE__), :generate_code } 129 | 130 | @name unquote(name) 131 | 132 | def run() do 133 | run(unquote(state)) 134 | end 135 | 136 | def run(state) do 137 | Jeeves.Scheduler.start_new_pool(worker_module: __MODULE__.Worker, 138 | pool_opts: unquote(opts[:pool] || [ min: 1, max: 4]), 139 | name: @name, 140 | state: state) 141 | end 142 | end 143 | |> Jeeves.Common.maybe_show_generated_code(opts) 144 | end 145 | 146 | @doc false 147 | defmacro generate_code(_) do 148 | 149 | { options, apis, handlers, implementations, delegators } = 150 | Jeeves.Common.create_functions_from_originals(__CALLER__.module, __MODULE__) 151 | 152 | PS.stop(__CALLER__.module) 153 | 154 | quote do 155 | 156 | unquote_splicing(delegators) 157 | 158 | defmodule Worker do 159 | use GenServer 160 | 161 | def start_link(args) do 162 | GenServer.start_link(__MODULE__, args) 163 | end 164 | 165 | unquote_splicing(apis) 166 | unquote_splicing(handlers) 167 | defmodule Implementation do 168 | unquote_splicing(implementations) 169 | end 170 | 171 | end 172 | end 173 | |> Jeeves.Common.maybe_show_generated_code(options) 174 | end 175 | 176 | @doc false 177 | defdelegate generate_api_call(options,function), to: Jeeves.Named 178 | @doc false 179 | defdelegate generate_handle_call(options,function), to: Jeeves.Named 180 | @doc false 181 | defdelegate generate_implementation(options,function), to: Jeeves.Named 182 | 183 | @doc false 184 | def generate_delegator(options, {call, _body}) do 185 | quote do 186 | def unquote(call), do: unquote(delegate_body(options, call)) 187 | end 188 | end 189 | 190 | @doc false 191 | def delegate_body(options, call) do 192 | timeout = options[:timeout] || 5000 193 | request = Jeeves.Named.call_signature(call) 194 | quote do 195 | Jeeves.Scheduler.run(@name, unquote(request), unquote(timeout)) 196 | end 197 | end 198 | 199 | end 200 | -------------------------------------------------------------------------------- /lib/jeeves/scheduler.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Scheduler do 2 | 3 | @moduledoc """ 4 | 5 | This is the runtime support for pooled workers. 6 | 7 | Each original module that specifies a pool arg will be associated with 8 | its own pool, and that pool is run by the scheduler code below. 9 | 10 | """ 11 | 12 | 13 | defdelegate start_new_pool(args), 14 | to: Jeeves.Scheduler.PoolSupervisor, 15 | as: :start_link 16 | 17 | 18 | @doc """ 19 | Run an instance of our pool worker with the given command. The 20 | optional third parameter is a timeout. If a float, the value is in 21 | seconds; otherwise it's an integer number of milliseconds. 22 | 23 | This is called by the delegating function in the code generated by 24 | Jeeves.Pooled—it isn't called directly by the client. 25 | """ 26 | 27 | def run(pool, what_to_run, timeout) when is_float(timeout) do 28 | run(pool, what_to_run, round(timeout * 1000 + 0.4999)) 29 | end 30 | 31 | def run(pool, what_to_run, timeout) do 32 | :poolboy.transaction(pool, &GenServer.call(&1, what_to_run, timeout), timeout) 33 | end 34 | 35 | end 36 | 37 | -------------------------------------------------------------------------------- /lib/jeeves/scheduler/#pool_supervisor.ex#: -------------------------------------------------------------------------------- 1 | defmodule Service.Scheduler.PoolSupervisor do 2 | 3 | use Supervisor 4 | 5 | @doc """ 6 | opts is a Keyword list containing: 7 | 8 | `name`: the name for this pool 9 | `worker_module`: the module containing the code for the worker_module 10 | `pool`: a specification for the pool 11 | `min`: number of prestarted workers 12 | `max`: can add upto `max - min` demand-based workers 13 | """ 14 | def start_link(opts) do 15 | IO.inspect opts, pretty: true 16 | Supervisor.start_link(__MODULE__, opts) 17 | end 18 | 19 | def init(opts) do 20 | exit_if_no_poolboy() 21 | 22 | worker_module = opts[:worker_module] || raise("missing worker module name") 23 | 24 | name = opts[:name] || MISSING_POOL_NAME 25 | pool = opts[:pool_opts] || [] 26 | min = pool[:min] || 2 27 | max = pool[:max] || (min+1) * 2 28 | state = opts[:state] || %{} 29 | 30 | poolboy_config = [ 31 | name: { :local, name }, 32 | worker_module: worker_module, 33 | size: min, 34 | max_overflow: max - min, 35 | ] 36 | 37 | children = [ 38 | :poolboy.child_spec(name, poolboy_config, state), 39 | ] 40 | 41 | options = [ 42 | strategy: :one_for_one, 43 | name: :pb_supervisor, 44 | ] 45 | supervise(children, options) 46 | end 47 | 48 | defp exit_if_no_poolboy() do 49 | try do 50 | :poolboy.child_spec(:a, [], []) 51 | rescue 52 | UndefinedFunctionError -> 53 | raise(""" 54 | 55 | You are trying to create a pooled service, but you don't have `poolboy` 56 | listed as a dependency. 57 | 58 | You can add 59 | 60 | b{ :poolboy, "~> 1.5.0" } # or a later version… 61 | 62 | to your dependencies to include it in your project. 63 | 64 | """) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/jeeves/scheduler/pool_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Scheduler.PoolSupervisor do 2 | 3 | use Supervisor 4 | 5 | @doc """ 6 | opts is a Keyword list containing: 7 | 8 | `name`: the name for this pool 9 | `worker_module`: the module containing the code for the worker_module 10 | `pool`: a specification for the pool 11 | `min`: number of prestarted workers 12 | `max`: can add upto `max - min` demand-based workers 13 | """ 14 | def start_link(opts) do 15 | IO.inspect opts, pretty: true 16 | Supervisor.start_link(__MODULE__, opts) 17 | end 18 | 19 | @dialyzer { :no_return, init: 1 } 20 | 21 | def init(opts) do 22 | exit_if_no_poolboy() 23 | 24 | worker_module = opts[:worker_module] || raise("missing worker module name") 25 | 26 | name = opts[:name] || MISSING_POOL_NAME 27 | pool = opts[:pool_opts] || [] 28 | min = pool[:min] || 2 29 | max = pool[:max] || (min+1) * 2 30 | state = opts[:state] || %{} 31 | 32 | poolboy_config = [ 33 | name: { :local, name }, 34 | worker_module: worker_module, 35 | size: min, 36 | max_overflow: max - min, 37 | ] 38 | 39 | IO.inspect opts 40 | IO.inspect poolboy_config 41 | 42 | children = [ 43 | :poolboy.child_spec(name, poolboy_config, state), 44 | ] 45 | 46 | options = [ 47 | strategy: :one_for_one, 48 | name: :pb_supervisor, 49 | ] 50 | supervise(children, options) 51 | end 52 | 53 | defp exit_if_no_poolboy() do 54 | try do 55 | :poolboy.child_spec(:a, [], []) 56 | rescue 57 | UndefinedFunctionError -> 58 | raise(""" 59 | 60 | You are trying to create a pooled service, but you don't have `poolboy` 61 | listed as a dependency. 62 | 63 | You can add 64 | 65 | { :poolboy, "~> 1.5.0" } # or a later version… 66 | 67 | to your dependencies to include it in your project. 68 | 69 | """) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/jeeves/service.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Service do 2 | 3 | @moduledoc """ 4 | Implement a service consisting of a pool of workers, all running in 5 | their own application. 6 | 7 | 8 | ### Prerequisites 9 | 10 | You'll need to add poolboy to your project dependencies. 11 | 12 | ### Usage 13 | 14 | To create the service: 15 | 16 | * Create a module that implements the API you want. This API will be 17 | expressed as a set of public functions. Each function will automatically 18 | receive the current state in a variable (by default named `state`). There is 19 | no need to declare this as a parameter.[why?](#why-magic-state). 20 | If a function wants to change the state, it must end with a call to the 21 | `Jeeves.Common.update_state/2` function (which will have been 22 | imported into your module automatically). 23 | 24 | * Add the line `use Jeeves.Service` to the top of this module. 25 | 26 | ### Options 27 | 28 | You can pass a keyword list to `use Jeeves.Service:` 29 | 30 | * `state_name:` _atom_ 31 | 32 | The default name for the state variable is (unimaginatively) `state`. 33 | Use `state_name` to override this. For example, the previous 34 | example named the state `options`, and inside the `recognize` function 35 | your could write `options.algorithm` to look up the algorithm to use. 36 | 37 | * `pool: [ ` _options_ ` ]` 38 | 39 | Set options for the service pool. One or more of: 40 | 41 | * `min: n` 42 | 43 | The minimum number of workers that should be active, and by extension 44 | the number of workers started when the pool is run. Default is 2. 45 | 46 | * `max: n` 47 | 48 | The maximum number of workers. If all workers are busy and a new request 49 | arrives, a new worker will be started to handle it if the current worker 50 | count is less than `max`. Excess idle workers will be quietly killed off 51 | in the background. Default value is `(min+1)*2`. 52 | 53 | * `showcode:` _boolean_ 54 | 55 | If truthy, dump a representation of the generated code to STDOUT during 56 | compilation. 57 | 58 | * `timeout:` integer or float 59 | 60 | Specify the timeout to be used when the client calls workers in the pool. 61 | If all workers are busy, and none becomes free in that time, an OTP 62 | exception is raised. An integer specifies the timeout in milliseconds, and 63 | a float in seconds (so 1.5 is the same as 1500). 64 | 65 | 66 | ## Consuming the Service 67 | 68 | Each service runs in an independent application. These applications 69 | are referenced by the main application. 70 | 71 | The main application lists the services it uses in its `mix.exs` 72 | file. 73 | 74 | «todo: finish this» 75 | 76 | ### State 77 | 78 | Each worker has independent state. This state is initialized in two stages. 79 | 80 | First, the main application maintains a list of services it uses in its 81 | `mix.exs` file: 82 | 83 | 84 | @services [ 85 | prime_factors: [ 86 | args: [ max_calc_time: 10_000 ] 87 | ] 88 | ] 89 | 90 | When the main application starts, it starts each service application in turn. 91 | As each starts, it passes the arguments in the `args` list to the function 92 | `setup_worker_state` in the service. This function does what is required to 93 | create a state that can be passed to each worker when it is started. 94 | 95 | For example, our PrimeFactors service might want to maintain a cache 96 | of previously calculated results, shared between all the workers. It 97 | could dothis by creating an agent in the 98 | `setup_worker_state`function and adding its pid to the state it 99 | returns. 100 | 101 | def setup_worker_state(initial_state) do 102 | { :ok, pid } = Agent.start_link(fn -> %{} end) 103 | initial_state 104 | |> Enum.info(%{ cache: pid }) 105 | end 106 | 107 | Each worker would be able to access that agent via the state it 108 | receives: 109 | 110 | def factor(n) do 111 | # yes, two workers could calculate the same value in parallel... :) 112 | 113 | case Agent.get(state.cache, fn map -> map[n] end) do 114 | 115 | nil -> 116 | result = complex_calculation(n) 117 | Agent.update(state.cache, fn map -> Map.put(map, n, result) end) 118 | result 119 | 120 | result -> 121 | result 122 | end 123 | end 124 | 125 | 126 | """ 127 | 128 | 129 | alias Jeeves.Util.PreprocessorState, as: PS 130 | 131 | @doc false 132 | defmacro __using__(opts \\ []) do 133 | generate_application_service(__CALLER__.module, opts) 134 | end 135 | 136 | @doc false 137 | def generate_application_service(caller, opts) do 138 | name = Keyword.get(opts, :service_name, nil) 139 | state = Keyword.get(opts, :state, :no_state) 140 | 141 | PS.start_link(caller, opts) 142 | 143 | quote do 144 | import Kernel, except: [ def: 2 ] 145 | import Jeeves.Common, only: [ def: 2, set_state: 1, set_state: 2 ] 146 | use Application 147 | 148 | @before_compile { unquote(__MODULE__), :generate_code } 149 | 150 | @name unquote(name) || Module.concat( __MODULE__, PoolSupervisor) 151 | 152 | def start(_, _) do 153 | { :ok, self() } 154 | end 155 | 156 | def run() do 157 | run(unquote(state)) 158 | end 159 | 160 | def run(state) do 161 | Jeeves.Scheduler.start_new_pool(worker_module: __MODULE__.Worker, 162 | pool_opts: unquote(opts[:pool] || [ min: 1, max: 4]), 163 | name: @name, 164 | state: setup_worker_state(state)) 165 | end 166 | 167 | def setup_worker_state(initial_state), do: initial_state 168 | 169 | defoverridable setup_worker_state: 1 170 | 171 | end 172 | |> Jeeves.Common.maybe_show_generated_code(opts) 173 | end 174 | 175 | @doc false 176 | defmacro generate_code(_) do 177 | 178 | { options, apis, handlers, implementations, delegators } = 179 | Jeeves.Common.create_functions_from_originals(__CALLER__.module, __MODULE__) 180 | 181 | PS.stop(__CALLER__.module) 182 | 183 | quote do 184 | 185 | unquote_splicing(delegators) 186 | 187 | defmodule Worker do 188 | use GenServer 189 | 190 | def start_link(args) do 191 | GenServer.start_link(__MODULE__, args) 192 | end 193 | 194 | unquote_splicing(apis) 195 | unquote_splicing(handlers) 196 | defmodule Implementation do 197 | unquote_splicing(implementations) 198 | end 199 | 200 | end 201 | end 202 | |> Jeeves.Common.maybe_show_generated_code(options) 203 | end 204 | 205 | @doc false 206 | defdelegate generate_api_call(options,function), to: Jeeves.Named 207 | @doc false 208 | defdelegate generate_handle_call(options,function), to: Jeeves.Named 209 | @doc false 210 | defdelegate generate_implementation(options,function), to: Jeeves.Named 211 | 212 | @doc false 213 | def generate_delegator(options, {call, _body}) do 214 | quote do 215 | def unquote(call), do: unquote(delegate_body(options, call)) 216 | end 217 | end 218 | 219 | @doc false 220 | def delegate_body(options, call) do 221 | timeout = options[:timeout] || 5000 222 | request = Jeeves.Named.call_signature(call) 223 | quote do 224 | Jeeves.Scheduler.run(@name, unquote(request), unquote(timeout)) 225 | end 226 | end 227 | 228 | end 229 | -------------------------------------------------------------------------------- /lib/jeeves/util/preprocessor_state.ex: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Util.PreprocessorState do 2 | 3 | defstruct( 4 | functions: [], # the list of { call, body }s from each def 5 | options: [] # the options from `use` 6 | ) 7 | 8 | 9 | def start_link(name, options) do 10 | { :ok, _ } = Agent.start_link( 11 | fn -> 12 | %__MODULE__{options: options} 13 | end, 14 | name: name_for(name) 15 | ) 16 | end 17 | 18 | 19 | def stop(name) do 20 | Agent.stop(name_for(name)) 21 | end 22 | 23 | def options(name) do 24 | Agent.get(name_for(name), &(&1.options)) 25 | end 26 | 27 | def add_function(name, func) do 28 | Agent.update(name_for(name), fn state -> 29 | %{ state | functions: [ func | state.functions ] } 30 | end) 31 | end 32 | 33 | def function_list(name) do 34 | Agent.get(name_for(name), &(&1.functions)) 35 | end 36 | 37 | # only for testing 38 | def name_for(name), do: :"-- pragdave.me.preprocessor.state.#{name} --" 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Jeeves.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.1.3" 5 | 6 | @deps [ 7 | { :poolboy, "~> 1.5.0" }, 8 | { :ex_doc, "~> 0.14", only: :dev, runtime: false } 9 | ] 10 | 11 | @description """ 12 | Jeeves is library that transforms regular modules into named or anonymous 13 | singleton or pooled GenServers. Just write your business functions, and Jeeves 14 | will convert them into an API, a server, and potentially a pooled set of workers. 15 | """ 16 | 17 | 18 | # ------------------------------------------------------------ 19 | 20 | 21 | def project do 22 | in_production = Mix.env == :prod 23 | [ 24 | app: :jeeves, 25 | version: @version, 26 | elixir: ">= 1.4.2", 27 | deps: @deps, 28 | package: package(), 29 | description: @description, 30 | build_embedded: in_production, 31 | start_permanent: in_production, 32 | 33 | # Docs 34 | name: "Jeeves", 35 | source_url: "https://github.com/pragdave/jeeves", 36 | homepage_url: "https://github.com/pragdave/jeeves", 37 | docs: [ 38 | main: "README", 39 | # logo: "path/to/logo.png", 40 | extras: [ "README.md", "background.md", "LICENSE.md" ], 41 | ] 42 | ] 43 | end 44 | 45 | def application do 46 | [ 47 | extra_applications: [:logger ], 48 | ] 49 | end 50 | 51 | defp package do 52 | [ 53 | files: [ 54 | "lib", "mix.exs", "README.md", "LICENSE.md" 55 | ], 56 | maintainers: [ 57 | "Dave Thomas " 58 | ], 59 | licenses: [ 60 | "Apache 2 (see the file LICENSE.md for details)" 61 | ], 62 | links: %{ 63 | "GitHub" => "https://github.com/pragdave/jeeves", 64 | } 65 | ] 66 | end 67 | 68 | 69 | end 70 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 3 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}} 4 | -------------------------------------------------------------------------------- /test/anonymous_test.exs: -------------------------------------------------------------------------------- 1 | defmodule T1 do 2 | use Jeeves.Anonymous, state: 99 3 | 4 | def api1(state, p1) do 5 | state + p1 6 | end 7 | 8 | def api2(state, p1) do 9 | set_state(state + p1) do 10 | state 11 | end 12 | end 13 | 14 | def get_state(state), do: state 15 | end 16 | 17 | defmodule AnonymousTest do 18 | use ExUnit.Case 19 | 20 | test "Top level module looks right" do 21 | info = T1.module_info() 22 | assert info[:attributes][:behaviour] == [GenServer] 23 | assert Enum.member?(info[:exports], { :api1, 2 }) 24 | assert Enum.member?(info[:exports], { :run, 0 }) 25 | assert Enum.member?(info[:exports], { :run, 1 }) 26 | end 27 | 28 | test "Implementation module looks right" do 29 | info = T1.Implementation.module_info() 30 | assert Enum.member?(info[:exports], { :api1, 2 }) 31 | end 32 | 33 | test "Running the module starts a GenServer" do 34 | handle = T1.run() 35 | assert is_pid(handle) 36 | end 37 | 38 | test "The initial state is set" do 39 | handle = T1.run() 40 | assert T1.get_state(handle) == 99 41 | end 42 | 43 | test "The GenServer delegates to the implementation" do 44 | handle = T1.run() 45 | assert T1.api1(handle, 2) == 101 46 | assert T1.api1(handle, 3) == 102 47 | end 48 | 49 | test "The GenServer maintains state" do 50 | handle = T1.run() 51 | assert T1.api2(handle, 1) == 99 52 | assert T1.get_state(handle) == 100 53 | assert T1.api2(handle, 1) == 100 54 | assert T1.get_state(handle) == 101 55 | end 56 | 57 | end 58 | 59 | -------------------------------------------------------------------------------- /test/common_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CommonTest do 2 | use ExUnit.Case 3 | alias Jeeves.Util.PreprocessorState, as: PS 4 | require Jeeves.Common 5 | alias Jeeves.Common, as: SC 6 | 7 | @name __MODULE__ 8 | 9 | test "calling `def` adds the function to the preprocessor state, but emits no code" do 10 | PS.start_link(@name, nil) 11 | result = SC.def_implementation(@name, :dave, :body) 12 | assert result == nil 13 | assert [ function ] = PS.function_list(@name) 14 | assert elem(function, 0) == :dave 15 | assert elem(function, 1) == :body 16 | end 17 | 18 | 19 | test "set_state() returns an OTP reply with the new state" do 20 | assert { :reply, :value, :new_state } == SC.set_state(:new_state, do: :value) 21 | end 22 | 23 | test "set_state() with no block returns an OTP reply with the new state and value" do 24 | assert { :reply, :new_state, :new_state } == SC.set_state(:new_state) 25 | end 26 | 27 | defmodule TestStrategy do 28 | def generate_api_call(_options, _func), do: :api 29 | def generate_handle_call(_options, _func), do: :handle 30 | def generate_implementation(_options, _func), do: :impl 31 | def generate_delegator(_options, _func), do: :delegator 32 | end 33 | 34 | test "the strategy is called to generate functions" do 35 | PS.start_link(@name, []) 36 | func = quote do 37 | def name(a1, a2) do 38 | a1 + a2 39 | end 40 | end 41 | 42 | { options, apis, handlers, impls, delegators } = 43 | SC.generate_functions(TestStrategy, :options, func, {nil, [], [], [], []}) 44 | 45 | assert options == :options 46 | assert apis == [:api] 47 | assert impls == [:impl] 48 | assert handlers == [:handle] 49 | assert delegators == [:delegator] 50 | end 51 | 52 | test "if a return value is not a genserver reply, wrap it in one" do 53 | state = 123 54 | 55 | assert { :reply, :return_value, ^state } = 56 | SC.create_genserver_response(:return_value, state) 57 | end 58 | 59 | test "if a return value is a genserver reply, don't wrap it in another" do 60 | state = 0 61 | assert { :reply, :return_value, :new_state } = 62 | SC.create_genserver_response({:reply, :return_value, :new_state}, state) 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/named_test.exs: -------------------------------------------------------------------------------- 1 | defmodule N1 do 2 | use Jeeves.Named, state: 99, name: Fred 3 | 4 | def api1(p1) do 5 | state + p1 6 | end 7 | 8 | def api2(p1) do 9 | set_state(state + p1) do 10 | state 11 | end 12 | end 13 | 14 | def get_state(), do: state 15 | end 16 | 17 | defmodule NamedTest do 18 | use ExUnit.Case, async: true 19 | 20 | def stop_if_running do 21 | case Process.whereis(N1) do 22 | nil -> nil 23 | pid -> GenServer.stop(pid) 24 | end 25 | end 26 | 27 | setup do 28 | stop_if_running() 29 | on_exit fn -> 30 | stop_if_running() 31 | end 32 | 33 | end 34 | 35 | test "Top level module looks right" do 36 | info = N1.module_info() 37 | assert info[:attributes][:behaviour] == [GenServer] 38 | assert Enum.member?(info[:exports], { :api1, 1 }) 39 | assert Enum.member?(info[:exports], { :run, 0 }) 40 | assert Enum.member?(info[:exports], { :run, 1 }) 41 | end 42 | 43 | test "Implementation module looks right" do 44 | info = N1.Implementation.module_info() 45 | assert Enum.member?(info[:exports], { :api1, 2 }) 46 | end 47 | 48 | test "Running the module starts a GenServer" do 49 | handle = N1.run() 50 | assert is_pid(handle) 51 | end 52 | 53 | test "The initial state is set" do 54 | N1.run() 55 | assert N1.get_state() == 99 56 | end 57 | 58 | test "The GenServer delegates to the implementation" do 59 | N1.run() 60 | assert N1.api1(2) == 101 61 | assert N1.api1(3) == 102 62 | end 63 | 64 | test "The GenServer maintains state" do 65 | N1.run() 66 | assert N1.api2(1) == 99 67 | assert N1.get_state() == 100 68 | assert N1.api2(1) == 100 69 | assert N1.get_state() == 101 70 | end 71 | 72 | end 73 | 74 | -------------------------------------------------------------------------------- /test/processor_state_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ProcessorStateTest do 2 | use ExUnit.Case 3 | 4 | alias Jeeves.Util.PreprocessorState, as: PS 5 | 6 | @some_options %{ name: "Vince", status: "playa" } 7 | 8 | @name PS.name_for(__MODULE__) 9 | 10 | test "can be started and stopped" do 11 | assert Process.whereis(@name) == nil 12 | PS.start_link(__MODULE__, @some_options) 13 | assert is_pid(Process.whereis(@name)) 14 | PS.stop(__MODULE__) 15 | assert Process.whereis(@name) == nil 16 | end 17 | 18 | describe "Once started" do 19 | 20 | setup do 21 | PS.start_link(__MODULE__, @some_options) # linked to test process, so no need to stop 22 | :ok 23 | end 24 | 25 | test "maintains initial options" do 26 | assert PS.options(__MODULE__) == @some_options 27 | end 28 | 29 | test "maintains starts with no functions" do 30 | assert PS.function_list(__MODULE__) == [] 31 | end 32 | 33 | test "records functions" do 34 | PS.add_function(__MODULE__, :one) 35 | PS.add_function(__MODULE__, :two) 36 | assert PS.function_list(__MODULE__) == [ :two, :one ] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/service_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JeevesTest do 2 | use ExUnit.Case 3 | doctest Jeeves 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------