├── .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 |
--------------------------------------------------------------------------------