├── .gitignore
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── TODO.org
├── doc
├── README.md
├── edoc-info
├── erlang.png
├── erlang07g-wiger.pdf
├── jobs.md
├── jobs_app.md
├── jobs_info.md
├── jobs_lib.md
├── jobs_prod_simple.md
├── jobs_queue.md
├── jobs_queue_list.md
├── jobs_sampler.md
├── jobs_sampler_cpu.md
├── jobs_sampler_history.md
├── jobs_sampler_mnesia.md
├── jobs_server.md
├── jobs_stateful_simple.md
├── overview.edoc
└── stylesheet.css
├── examples
├── jobs_cpu.gnu
├── performance_logger.erl
└── performer.erl
├── include
└── jobs.hrl
├── rebar.config
├── src
├── jobs.app.src
├── jobs.erl
├── jobs_app.erl
├── jobs_info.erl
├── jobs_lib.erl
├── jobs_prod_simple.erl
├── jobs_queue.erl
├── jobs_queue_list.erl
├── jobs_sampler.erl
├── jobs_sampler_cpu.erl
├── jobs_sampler_history.erl
├── jobs_sampler_mnesia.erl
├── jobs_server.erl
└── jobs_stateful_simple.erl
└── test
├── jobs_eqc_queue.erl
├── jobs_queue_model.erl
├── jobs_sampler_slave.erl
├── jobs_server_tests.erl
├── jobs_tests.erl
└── t.erl
/.gitignore:
--------------------------------------------------------------------------------
1 | /deps/
2 | /deps/
3 | *.beam
4 | ebin/jobs.app
5 |
6 | /.jobs.plt
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | DIALYZER=dialyzer
2 |
3 | .PHONY: all test clean plt analyze doc test-console
4 |
5 | all: deps compile
6 |
7 | deps:
8 | rebar get-deps
9 |
10 | compile:
11 | rebar compile
12 |
13 | test: all
14 | rebar eunit skip_deps=true
15 |
16 | clean:
17 | rebar clean
18 |
19 | doc:
20 | rebar doc
21 |
22 | test-console:
23 | erlc -I include -o test test/*.erl
24 | erl -pa deps/*/ebin ebin test
25 |
26 | plt: deps compile
27 | $(DIALYZER) --build_plt --output_plt .jobs.plt \
28 | -pa deps/*/ebin \
29 | deps/*/ebin \
30 | --apps kernel stdlib sasl inets crypto \
31 | public_key ssl runtime_tools erts \
32 | compiler tools syntax_tools hipe webtool
33 |
34 | analyze: compile
35 | $(DIALYZER) --no_check_plt \
36 | ebin \
37 | --plt .jobs.plt
38 |
39 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Jobs, copyright 2010 Erlang Solutions
2 |
3 | This product contains code developed at Erlang Solutions.
4 | (http://www.erlang-solutions.com/)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # jobs - a Job scheduler for load regulation #
4 |
5 | Copyright (c) 2010 Erlang Solutions Ltd.
6 |
7 | __Version:__ 0.1
8 |
9 | JOBS
10 | ====
11 |
12 | Jobs is a job scheduler for load regulation of Erlang applications.
13 | It provides a queueing framework where each queue can be configured
14 | for throughput rate, credit pool and feedback compensation.
15 | Queues can be added and modified at runtime, and customizable
16 | "samplers" propagate load status across all nodes in the system.
17 |
18 | Specifically, jobs provides three features:
19 |
20 | * Job scheduling: A job is scheduled according to certain constraints.
21 | For instance, you may want to define that no more than 9 jobs of a
22 | certain type can execute simultaneously and the maximal rate at
23 | which you can start such jobs are 300 per second.
24 | * Job queueing: When load is higher than the scheduling limits
25 | additional jobs are *queued* by the system to be run later when load
26 | clears. Certain rules govern queues: are they dequeued in FIFO or
27 | LIFO order? How many jobs can the queue take before it is full? Is
28 | there a deadline after which jobs should be rejected. When we hit
29 | the queue limits we reject the job. This provides a feedback
30 | mechanism on the client of the queue so you can take action.
31 | * Sampling and dampening: Periodic samples of the Erlang VM can
32 | provide information about the health of the system in general. If we
33 | have high CPU load or high memory usage, we apply dampening to the
34 | scheduling rules: we may lower the concurrency count or the rate at
35 | which we execute jobs. When the health problem clears, we remove the
36 | dampener and run at full speed again.
37 |
38 | Examples
39 | --------
40 |
41 | To be done
42 |
43 | Prerequisites
44 | -------------
45 | This application requires 'exprecs'.
46 | The 'exprecs' module is part of http://github.com/esl/parse_trans
47 |
48 | Contribute
49 | ----------
50 | For issues, comments or feedback please [create an issue!] [1]
51 | [1]: http://github.com/esl/jobs/issues "jobs issues"
52 |
53 |
54 | ## Modules ##
55 |
56 |
57 |
71 |
72 |
--------------------------------------------------------------------------------
/TODO.org:
--------------------------------------------------------------------------------
1 | * DONE Change jobs_eqc_model so it understands LIFO and FIFO.
2 | * DONE Change test so we always generate fifo with jobs queue
3 | * DONE Change test so we always generate life with jobs queue list
4 | * TODO Add tests for the "timedout" parameter.
5 | ** DONE Implement timedout in the model
6 | ** TODO Introduce meck
7 | ** TODO Meck up jobs_lib:timestamp()
8 |
9 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | #jobs - a Job scheduler for load regulation#
4 |
5 |
6 | Copyright (c) 2010 Erlang Solutions Ltd.
7 |
8 | __Version:__ 0.1
9 |
10 |
11 |
12 | JOBS
13 | ====
14 |
15 |
16 |
17 | Jobs is a job scheduler for load regulation of Erlang applications.
18 | It provides a queueing framework where each queue can be configured
19 | for throughput rate, credit pool and feedback compensation.
20 | Queues can be added and modified at runtime, and customizable
21 | "samplers" propagate load status across all nodes in the system.
22 |
23 |
24 |
25 | Specifically, jobs provides three features:
26 |
27 |
28 |
29 | * Job scheduling: A job is scheduled according to certain constraints.
30 | For instance, you may want to define that no more than 9 jobs of a
31 | certain type can execute simultaneously and the maximal rate at
32 | which you can start such jobs are 300 per second.
33 | * Job queueing: When load is higher than the scheduling limits
34 | additional jobs are *queued* by the system to be run later when load
35 | clears. Certain rules govern queues: are they dequeued in FIFO or
36 | LIFO order? How many jobs can the queue take before it is full? Is
37 | there a deadline after which jobs should be rejected. When we hit
38 | the queue limits we reject the job. This provides a feedback
39 | mechanism on the client of the queue so you can take action.
40 | * Sampling and dampening: Periodic samples of the Erlang VM can
41 | provide information about the health of the system in general. If we
42 | have high CPU load or high memory usage, we apply dampening to the
43 | scheduling rules: we may lower the concurrency count or the rate at
44 | which we execute jobs. When the health problem clears, we remove the
45 | dampener and run at full speed again.
46 |
47 |
48 |
49 | Examples
50 | --------
51 |
52 |
53 |
54 | To be done
55 |
56 |
57 |
58 | Prerequisites
59 | -------------
60 | This application requires 'exprecs'.
61 | The 'exprecs' module is part of http://github.com/esl/parse_trans
62 |
63 |
64 |
65 | Contribute
66 | ----------
67 | For issues, comments or feedback please [create an issue!] [1]
68 |
69 | [1]: http://github.com/esl/jobs/issues "jobs issues"
70 |
71 |
72 | ##Modules##
73 |
74 |
75 |
87 |
88 |
--------------------------------------------------------------------------------
/doc/edoc-info:
--------------------------------------------------------------------------------
1 | %% encoding: UTF-8
2 | {application,jobs}.
3 | {packages,[]}.
4 | {modules,[jobs,jobs_app,jobs_info,jobs_lib,jobs_prod_simple,jobs_queue,
5 | jobs_queue_list,jobs_sampler,jobs_sampler_cpu,jobs_sampler_history,
6 | jobs_sampler_mnesia,jobs_server,jobs_stateful_simple]}.
7 |
--------------------------------------------------------------------------------
/doc/erlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esl/jobs/6800e98e4e172521e5ffd50ee2032ea922c8f36b/doc/erlang.png
--------------------------------------------------------------------------------
/doc/erlang07g-wiger.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esl/jobs/6800e98e4e172521e5ffd50ee2032ea922c8f36b/doc/erlang07g-wiger.pdf
--------------------------------------------------------------------------------
/doc/jobs.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs #
4 | * [Description](#description)
5 | * [Function Index](#index)
6 | * [Function Details](#functions)
7 |
8 |
9 |
10 | This is the public API of the JOBS framework.
11 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
12 |
13 |
14 | ## Description ##
15 |
16 |
17 | ## Function Index ##
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ## Function Details ##
26 |
27 |
28 |
29 | ### add_counter/2 ###
30 |
31 |
32 |
33 | add_counter(Name, Options) -> ok
34 |
35 |
36 |
37 |
38 |
39 | Adds a named counter to the load regulator on the current node.
40 | Fails if there already is a counter the name `Name`.
41 |
42 |
43 | ### add_group_rate/2 ###
44 |
45 |
46 |
47 | add_group_rate(Name, Options) -> ok
48 |
49 |
50 |
51 |
52 |
53 | Adds a group rate regulator to the load regulator on the current node.
54 | Fails if there is already a group rate regulator of the same name.
55 |
56 |
57 | ### add_queue/2 ###
58 |
59 |
60 |
61 | add_queue(Name::any(), Options::[{Key, Value}]) -> ok
62 |
63 |
64 |
65 |
66 |
67 | Installs a new queue in the load regulator on the current node.
68 |
69 |
70 | ### ask/1 ###
71 |
72 |
73 |
74 | ask(Type) -> {ok, Opaque} | {error, Reason}
75 |
76 |
77 |
78 |
79 |
80 |
81 | Asks permission to run a job of Type. Returns when permission granted.
82 |
83 |
84 | The simplest way to have jobs regulated is to spawn a request per job.
85 | The process should immediately call this function, and when granted
86 | permission, execute the job, and then terminate.
87 | If for some reason the process needs to remain, to execute more jobs,
88 | it should explicitly call `jobs:done(Opaque)`.
89 | This is not strictly needed when regulation is rate-based, but as the
90 | regulation strategy may change over time, it is the prudent thing to do.
91 |
92 |
93 | ### ask_queue/2 ###
94 |
95 |
96 |
97 | ask_queue(QueueName, Request) -> Reply
98 |
99 |
100 |
101 |
102 |
103 |
104 | Sends a synchronous request to a specific queue.
105 |
106 |
107 | This function is mainly intended to be used for back-end processes that act
108 | as custom extensions to the load regulator itself. It should not be used by
109 | regular clients. Sophisticated queue behaviours could export gen_server-like
110 | logic allowing them to respond to synchronous calls, either for special
111 | inspection, or for influencing the queue state.
112 |
113 |
114 | ### delete_counter/1 ###
115 |
116 |
117 |
118 | delete_counter(Name) -> boolean()
119 |
120 |
121 |
122 |
123 |
124 | Deletes a named counter from the load regulator on the current node.
125 | Returns `true` if there was in fact such a counter; `false` otherwise.
126 |
127 |
128 | ### delete_group_rate/1 ###
129 |
130 | `delete_group_rate(Name) -> any()`
131 |
132 |
133 |
134 |
135 | ### delete_queue/1 ###
136 |
137 |
138 |
139 | delete_queue(Name) -> boolean()
140 |
141 |
142 |
143 |
144 |
145 | Deletes the named queue from the load regulator on the current node.
146 | Returns `true` if there was in fact such a queue; `false` otherwise.
147 |
148 |
149 | ### dequeue/2 ###
150 |
151 | `dequeue(Queue, N) -> any()`
152 |
153 |
154 |
155 |
156 | ### done/1 ###
157 |
158 |
159 |
160 | done(Opaque) -> ok
161 |
162 |
163 |
164 |
165 |
166 |
167 | Signals completion of an executed task.
168 |
169 |
170 | This is used when the current process wants to submit more jobs to load
171 | regulation. It is mandatory when performing counter-based regulation
172 | (unless the process terminates after completing the task). It has no
173 | effect if the job type is purely rate-regulated.
174 |
175 |
176 | ### enqueue/2 ###
177 |
178 | `enqueue(Queue, Item) -> any()`
179 |
180 |
181 |
182 |
183 | ### info/1 ###
184 |
185 | `info(Item) -> any()`
186 |
187 |
188 |
189 |
190 | ### job_info/1 ###
191 |
192 |
193 |
194 | job_info(X1::Opaque) -> undefined | Info
195 |
196 |
197 |
198 |
199 |
200 |
201 | Retrieves job-specific information from the `Opaque` data object.
202 |
203 |
204 | The queue could choose to return specific information that is passed to a
205 | granted job request. This could be used e.g. for load-balancing strategies.
206 |
207 |
208 | ### modify_counter/2 ###
209 |
210 | `modify_counter(CName, Opts) -> any()`
211 |
212 |
213 |
214 |
215 | ### modify_group_rate/2 ###
216 |
217 | `modify_group_rate(GRName, Opts) -> any()`
218 |
219 |
220 |
221 |
222 | ### modify_regulator/4 ###
223 |
224 | `modify_regulator(Type, QName, RegName, Opts) -> any()`
225 |
226 |
227 |
228 |
229 | ### queue_info/1 ###
230 |
231 | `queue_info(Name) -> any()`
232 |
233 |
234 |
235 |
236 | ### queue_info/2 ###
237 |
238 | `queue_info(Name, Item) -> any()`
239 |
240 |
241 |
242 |
243 | ### run/2 ###
244 |
245 |
246 |
247 | run(Queue::Type, Function::function()) -> Result
248 |
249 |
250 |
251 |
252 |
253 |
254 | Executes Function() when permission has been granted by job regulator.
255 |
256 |
257 | This is equivalent to performing the following sequence:
258 |
259 | ```
260 |
261 | case jobs:ask(Type) of
262 | {ok, Opaque} ->
263 | try Function()
264 | after
265 | jobs:done(Opaque)
266 | end;
267 | {error, Reason} ->
268 | erlang:error(Reason)
269 | end.
270 | ```
271 |
272 |
--------------------------------------------------------------------------------
/doc/jobs_app.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_app #
4 | * [Description](#description)
5 | * [Function Index](#index)
6 | * [Function Details](#functions)
7 |
8 |
9 | Application module for JOBS.
10 |
11 |
12 |
13 | ## Description ##
14 |
15 | Normally, JOBS is configured at startup, using a static configuration.
16 | There is a reconfiguration API [`jobs`](jobs.md), which is mainly for evolution
17 | of the system.
18 |
19 |
20 |
21 |
22 | ### Configuring JOBS ###
23 |
24 |
25 | A static configuration can be provided via application environment
26 | variables for the `jobs` application. The following is a list of
27 | recognised configuration parameters.
28 |
29 |
30 |
31 |
32 | #### {config, Filename} ####
33 |
34 |
35 | Evaluate a file using [`//kernel/file:script/1`](/Users/uwiger/FL/git/kernel/doc/file.md#script-1), treating the data
36 | returned from the script as a list of configuration options.
37 |
38 |
39 |
40 |
41 | #### {queues, QueueOptions} ####
42 |
43 |
44 | Configure a list of queues according to the provided QueueOptions.
45 | If no queues are specified, a queue named `default` will be created
46 | with default characteristics.
47 |
48 |
49 |
50 | Below are the different queue configuration options:
51 |
52 |
53 |
54 |
55 |
56 |
57 | This is the generic queue configuration pattern.
58 | `Name :: any()` is used to identify the queue.
59 |
60 |
61 |
62 | Options:
63 |
64 |
65 |
66 | `{mod, Module::atom()}` provides the name of the queueing module.
67 | The default module is `jobs_queue`.
68 |
69 |
70 |
71 | `{type, fifo | lifo | approve | reject | {producer, F}}`
72 | specifies the semantics of the queue. Note that the specified queue module
73 | may be limited to only one type (e.g. the `jobs_queue_list` module only
74 | supports `lifo` semantics).
75 |
76 |
77 |
78 | If the type is `{producer, F}`, it doesn't matter which queue module is
79 | used, as it is not possible to submit job requests to a producer queue.
80 | The producer queue will initiate jobs using `spawn_monitor(F)` at the
81 | rate given by the regulators for the queue.
82 |
83 |
84 |
85 | If the type is `approve` or `reject`, respectively, all other options will
86 | be irrelevant. Any request to the queue will either be immediately approved
87 | or immediately rejected.
88 |
89 |
90 |
91 | `{max_time, integer() | undefined}` specifies the longest time that a job
92 | request may spend in the queue. If `undefined`, no limit is imposed.
93 |
94 |
95 |
96 | `{max_size, integer() | undefined}` specifies the maximum length (number
97 | of job requests) of the queue. If the queue has reached the maximum length,
98 | subsequent job requests will be rejected unless it is possible to remove
99 | enough requests that have exceeded the maximum allowed time in the queue.
100 |
101 |
102 |
103 | `{regulators, [{regulator_type(), Opts]}` specifies the regulation
104 | characteristics of the queue.
105 |
106 |
107 |
108 | The following types of regulator are supported:
109 |
110 |
111 |
112 | `regulator_type() :: rate | counter | group_rate`
113 |
114 |
115 |
116 | It is possible to combine different types of regulator on the same queue,
117 | e.g. a queue may have both rate- and counter regulation. It is not possible
118 | to have two different rate regulators for the same queue.
119 |
120 |
121 |
122 | Common regulator options:
123 |
124 |
125 |
126 | `{name, term()}` names the regulator; by default, a name will be generated.
127 |
128 |
129 |
130 | `{limit, integer()}` defines the limit for the regulator. If it is a rate
131 | regulator, the value represents the maximum number of jobs/second; if it
132 | is a counter regulator, it represents the total number of "credits"
133 | available.
134 |
135 |
136 |
137 | `{modifiers, [modifier()]}`
138 |
139 |
140 |
141 | ```
142 |
143 | modifier() :: {IndicatorName :: any(), unit()}
144 | | {Indicator, local_unit(), remote_unit()}
145 | | {Indicator, Fun}
146 | local_unit() :: unit() :: integer()
147 | remote_unit() :: {avg, unit()} | {max, unit()}
148 | ```
149 |
150 |
151 |
152 | Feedback indicators are sent from the sampler framework. Each indicator
153 | has the format `{IndicatorName, LocalLoadFactor, Remote}`.
154 |
155 |
156 |
157 | `Remote :: [{Node, LoadFactor}]`
158 |
159 |
160 |
161 | `IndicatorName` defines the type of indicator. It could be e.g. `cpu`,
162 | `memory`, `mnesia`, or any other name defined by one of the sampler plugins.
163 |
164 |
165 |
166 | The effect of a modifier is calculated as the sum of the effects from local
167 | and remote load. As the remote load is represented as a list of
168 | `{Node,Factor}` it is possible to multiply either the average or the max
169 | load on the remote nodes with the given factor: `{avg,Unit} | {max, Unit}`.
170 |
171 |
172 |
173 | For custom interpretation of the feedback indicator, it is possible to
174 | specify a function `F(LocalFactor, Remote) -> Effect`, where Effect is a
175 | positive integer.
176 |
177 |
178 |
179 | The resulting effect value is used to reduce the predefined regulator limit
180 | with the given number of percentage points, e.g. if a rate regulator has
181 | a predefined limit of 100 jobs/sec, and `Effect = 20`, the current rate
182 | limit will become 80 jobs/sec.
183 |
184 |
185 |
186 | `{rate, Opts}` - rate regulation
187 |
188 |
189 |
190 | Currently, no special options exist for rate regulators.
191 |
192 |
193 |
194 | `{counter, Opts}` - counter regulation
195 |
196 |
197 |
198 | The option `{increment, I}` can be used to specify how much of the credit
199 | pool should be assigned to each job. The default increment is 1.
200 |
201 |
202 |
203 | `{named_counter, Name, Increment}` reuses an existing counter regulator.
204 | This can be used to link multiple queues to a shared credit pool. Note that
205 | this does not use the existing counter regulator as a template, but actually
206 | shares the credits with any other queues using the same named counter.
207 |
208 |
209 |
210 | __NOTE__ Currently, if there is no counter corresponding to the alias,
211 | the entry will simply be ignored during regulation. It is likely that this
212 | behaviour will change in the future.
213 |
214 |
215 |
216 |
217 |
218 |
219 | A simple rate-regulated queue with throughput rate `R`, and basic cpu- and
220 | memory-related feedback compensation.
221 |
222 |
223 |
224 |
225 |
226 |
227 | A simple counter-regulated queue, giving each job a weight of 1, and thus
228 | allowing at most `N` jobs to execute concurrently. Basic cpu- and memory-
229 | related feedback compensation.
230 |
231 |
232 |
233 |
234 |
235 | A producer queue is not open for incoming jobs, but will rather initiate
236 | jobs at the given rate.
237 |
238 | ## Function Index ##
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 | ## Function Details ##
247 |
248 |
249 |
250 | ### init/1 ###
251 |
252 | `init(X1) -> any()`
253 |
254 |
255 |
256 |
257 | ### start/2 ###
258 |
259 | `start(X1, X2) -> any()`
260 |
261 |
262 |
263 |
264 | ### stop/1 ###
265 |
266 | `stop(X1) -> any()`
267 |
268 |
269 |
--------------------------------------------------------------------------------
/doc/jobs_info.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_info #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 |
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### pp/1 ###
23 |
24 | `pp(L) -> any()`
25 |
26 |
27 |
--------------------------------------------------------------------------------
/doc/jobs_lib.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_lib #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 |
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### timestamp/0 ###
23 |
24 | `timestamp() -> any()`
25 |
26 |
27 |
28 |
29 | ### timestamp_to_datetime/1 ###
30 |
31 | `timestamp_to_datetime(TS) -> any()`
32 |
33 |
34 |
--------------------------------------------------------------------------------
/doc/jobs_prod_simple.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_prod_simple #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 |
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### init/2 ###
23 |
24 | `init(F, Info) -> any()`
25 |
26 |
27 |
28 |
29 | ### next/3 ###
30 |
31 | `next(Opaque, Stateful, Info) -> any()`
32 |
33 |
34 |
--------------------------------------------------------------------------------
/doc/jobs_queue.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_queue #
4 | * [Description](#description)
5 | * [Function Index](#index)
6 | * [Function Details](#functions)
7 |
8 |
9 | Default queue behaviour for JOBS (using ordered_set ets).
10 | __This module defines the `jobs_queue` behaviour.__
11 |
12 | Required callback functions: `new/2`, `delete/1`, `in/3`, `peek/1`, `out/2`, `all/1`, `info/2`.
13 |
14 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
15 |
16 |
17 | ## Description ##
18 |
19 |
20 | This module implements the default queue behaviour for JOBS, and also
21 | specifies the behaviour itself.
22 |
23 | ## Function Index ##
24 |
25 |
26 | all/1 | Return all the job entries in the queue, not removing them from the queue. |
behaviour_info/1 | |
delete/1 | Queue is being deleted; remove any external data structures. |
empty/1 | |
in/3 | Enqueue a job reference; return the updated queue. |
info/2 | Return information about the queue. |
is_empty/1 | |
new/2 | Instantiate a new queue. |
out/2 | Dequeue a batch of N jobs; return the modified queue. |
peek/1 | Looks at the first item in the queue, without removing it. |
representation/1 | A representation of a queue which can be inspected. |
timedout/1 | Return all entries that have been in the queue longer than MaxTime. |
timedout/2 | |
27 |
28 |
29 |
30 |
31 | ## Function Details ##
32 |
33 |
34 |
35 | ### all/1 ###
36 |
37 |
38 |
39 | all(Queue::#queue{}) -> [JobEntry]
40 |
41 |
42 |
43 |
44 |
45 | Return all the job entries in the queue, not removing them from the queue.
46 |
47 |
48 |
49 | ### behaviour_info/1 ###
50 |
51 | `behaviour_info(X1) -> any()`
52 |
53 |
54 |
55 |
56 | ### delete/1 ###
57 |
58 |
59 |
60 | delete(Queue::#queue{}) -> any()
61 |
62 |
63 |
64 |
65 |
66 |
67 | Queue is being deleted; remove any external data structures.
68 |
69 |
70 | If the queue behaviour has created an ETS table or similar, this is the place
71 | to get rid of it.
72 |
73 |
74 | ### empty/1 ###
75 |
76 | `empty(Queue) -> any()`
77 |
78 |
79 |
80 |
81 | ### in/3 ###
82 |
83 |
84 |
85 | in(TS::Timestamp, Job, Queue::#queue{}) -> #queue{}
86 |
87 |
88 |
89 |
90 |
91 |
92 | Enqueue a job reference; return the updated queue.
93 |
94 |
95 | This puts a job into the queue. The callback function is responsible for
96 | updating the #queue.oldest_job attribute, if needed. The #queue.oldest_job
97 | attribute shall either contain the Timestamp of the oldest job in the queue,
98 | or `undefined` if the queue is empty. It may be noted that, especially in the
99 | fairly trivial case of the `in/3` function, the oldest job would be
100 | `erlang:min(Timestamp, PreviousOldest)`, even if `PreviousOldest == undefined`.
101 |
102 |
103 | ### info/2 ###
104 |
105 |
106 |
107 | info(X1::Item, Queue::#queue{}) -> Info
108 |
109 |
110 | Item = max_time | oldest_job | length
111 |
112 | Return information about the queue.
113 |
114 |
115 |
116 | ### is_empty/1 ###
117 |
118 |
119 |
120 | is_empty(Queue::#queue{}) -> boolean()
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | ### new/2 ###
130 |
131 |
132 |
133 | new(Options, Q::#queue{}) -> #queue{}
134 |
135 |
136 |
137 |
138 |
139 |
140 | Instantiate a new queue.
141 |
142 |
143 | Options is the list of options provided when defining the queue.
144 | Q is an initial #queue{} record. It can be used directly by including
145 | `jobs/include/jobs.hrl`, or by using exprecs-style record accessors in the
146 | module `jobs_info`.
147 | See [parse_trans](http://github.com/esl/parse_trans) for more info
148 | on exprecs. In the `new/2` function, the #queue.st attribute will normally be
149 | used to keep track of the queue data structure.
150 |
151 |
152 | ### out/2 ###
153 |
154 |
155 |
156 | out(N::integer(), Queue::#queue{}) -> {[Entry], #queue{}}
157 |
158 |
159 |
160 |
161 |
162 |
163 | Dequeue a batch of N jobs; return the modified queue.
164 |
165 |
166 | Note that this function may need to update the #queue.oldest_job attribute,
167 | especially if the queue becomes empty.
168 |
169 |
170 | ### peek/1 ###
171 |
172 |
173 |
174 | peek(Queue::#queue{}) -> JobEntry | undefined
175 |
176 |
177 |
178 |
179 |
180 | Looks at the first item in the queue, without removing it.
181 |
182 |
183 |
184 | ### representation/1 ###
185 |
186 | `representation(Queue) -> any()`
187 |
188 | A representation of a queue which can be inspected
189 |
190 |
191 | ### timedout/1 ###
192 |
193 |
194 |
195 | timedout(Queue::#queue{}) -> [] | {[Entry], #queue{}}
196 |
197 |
198 |
199 |
200 |
201 |
202 | Return all entries that have been in the queue longer than MaxTime.
203 |
204 |
205 | NOTE: This is an inspection function; it doesn't remove the job entries.
206 |
207 |
208 | ### timedout/2 ###
209 |
210 | `timedout(TO, Queue) -> any()`
211 |
212 |
213 |
--------------------------------------------------------------------------------
/doc/jobs_queue_list.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_queue_list #
4 | * [Data Types](#types)
5 | * [Function Index](#index)
6 | * [Function Details](#functions)
7 |
8 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
9 |
10 |
11 |
12 | ## Data Types ##
13 |
14 |
15 |
16 |
17 | ### entry() ###
18 |
19 |
20 |
21 |
22 | entry() = {timestamp(), job()}
23 |
24 |
25 |
26 |
27 |
28 |
29 | ### info_item() ###
30 |
31 |
32 |
33 |
34 | info_item() = max_time | oldest_job | length
35 |
36 |
37 |
38 |
39 |
40 |
41 | ### job() ###
42 |
43 |
44 |
45 |
46 | job() = {pid(), reference()}
47 |
48 |
49 |
50 |
51 |
52 | ## Function Index ##
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | ## Function Details ##
61 |
62 |
63 |
64 | ### all/1 ###
65 |
66 |
67 |
68 | all(Queue::#queue{}) -> [entry()]
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ### delete/1 ###
78 |
79 | `delete(Queue) -> any()`
80 |
81 |
82 |
83 |
84 | ### empty/1 ###
85 |
86 | `empty(Queue) -> any()`
87 |
88 |
89 |
90 |
91 | ### in/3 ###
92 |
93 |
94 |
95 | in(TS::timestamp(), Job::job(), Queue::#queue{}) -> #queue{}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | ### info/2 ###
105 |
106 |
107 |
108 | info(X1::info_item(), Queue::#queue{}) -> any()
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | ### is_empty/1 ###
118 |
119 |
120 |
121 | is_empty(Queue::#queue{}) -> boolean()
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | ### new/2 ###
131 |
132 | `new(Options, Q) -> any()`
133 |
134 |
135 |
136 |
137 | ### out/2 ###
138 |
139 |
140 |
141 | out(N::integer(), Queue::#queue{}) -> {[entry()], #queue{}}
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | ### peek/1 ###
151 |
152 | `peek(Queue) -> any()`
153 |
154 |
155 |
156 |
157 | ### representation/1 ###
158 |
159 | `representation(Queue) -> any()`
160 |
161 |
162 |
163 |
164 | ### timedout/1 ###
165 |
166 |
167 |
168 | timedout(Queue::#queue{}) -> {[entry()], #queue{}}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | ### timedout/2 ###
178 |
179 | `timedout(TO, Queue) -> any()`
180 |
181 |
182 |
--------------------------------------------------------------------------------
/doc/jobs_sampler.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_sampler #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 | __This module defines the `jobs_sampler` behaviour.__
8 |
9 | Required callback functions: `init/2`, `sample/2`, `handle_msg/3`, `calc/2`.
10 |
11 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
12 |
13 |
14 | ## Function Index ##
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## Function Details ##
23 |
24 |
25 |
26 | ### behaviour_info/1 ###
27 |
28 | `behaviour_info(X1) -> any()`
29 |
30 |
31 |
32 |
33 | ### calc/3 ###
34 |
35 | `calc(Type, Template, History) -> any()`
36 |
37 |
38 |
39 |
40 | ### code_change/3 ###
41 |
42 | `code_change(FromVsn, State, Extra) -> any()`
43 |
44 |
45 |
46 |
47 | ### end_subscription/0 ###
48 |
49 | `end_subscription() -> any()`
50 |
51 |
52 |
53 |
54 | ### handle_call/3 ###
55 |
56 | `handle_call(X1, From, State) -> any()`
57 |
58 |
59 |
60 |
61 | ### handle_cast/2 ###
62 |
63 | `handle_cast(X1, S) -> any()`
64 |
65 |
66 |
67 |
68 | ### handle_info/2 ###
69 |
70 | `handle_info(Msg, State) -> any()`
71 |
72 |
73 |
74 |
75 | ### init/1 ###
76 |
77 | `init(Opts) -> any()`
78 |
79 |
80 |
81 |
82 | ### start_link/0 ###
83 |
84 | `start_link() -> any()`
85 |
86 |
87 |
88 |
89 | ### start_link/1 ###
90 |
91 | `start_link(Opts) -> any()`
92 |
93 |
94 |
95 |
96 | ### subscribe/0 ###
97 |
98 |
99 |
100 | subscribe() -> ok
101 |
102 |
103 |
104 |
105 |
106 |
107 | Subscribes to feedback indicator information
108 |
109 |
110 |
111 | This function allows a process to receive the same information as the
112 | jobs_server any time the information changes.
113 |
114 |
115 | The notifications are delivered on the format `{jobs_indicators, Info}`,
116 | where
117 |
118 | ```
119 |
120 | Info :: [{IndicatorName, LocalValue, Remote}]
121 | Remote :: [{NodeName, Value}]
122 | ```
123 |
124 |
125 | This information could be used e.g. to aggregate the information and generate
126 | new sampler information (which could be passed to a sampler plugin using
127 | [`tell_sampler/2`](#tell_sampler-2), or to a specific queue using [`jobs:ask_queue/2`](jobs.md#ask_queue-2).
128 |
129 |
130 |
131 | ### tell_sampler/2 ###
132 |
133 | `tell_sampler(P, Msg) -> any()`
134 |
135 |
136 |
137 |
138 | ### terminate/2 ###
139 |
140 | `terminate(X1, S) -> any()`
141 |
142 |
143 |
144 |
145 | ### trigger_sample/0 ###
146 |
147 | `trigger_sample() -> any()`
148 |
149 |
150 |
--------------------------------------------------------------------------------
/doc/jobs_sampler_cpu.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_sampler_cpu #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 | __Behaviours:__ [`jobs_sampler`](jobs_sampler.md).
8 |
9 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
10 |
11 |
12 | ## Function Index ##
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Function Details ##
21 |
22 |
23 |
24 | ### calc/2 ###
25 |
26 | `calc(History, St) -> any()`
27 |
28 |
29 |
30 |
31 | ### handle_msg/3 ###
32 |
33 | `handle_msg(Msg, Timestamp, ModS) -> any()`
34 |
35 |
36 |
37 |
38 | ### init/2 ###
39 |
40 | `init(Name, Opts) -> any()`
41 |
42 |
43 |
44 |
45 | ### sample/2 ###
46 |
47 | `sample(Timestamp, St) -> any()`
48 |
49 |
50 |
--------------------------------------------------------------------------------
/doc/jobs_sampler_history.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_sampler_history #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 |
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### add/2 ###
23 |
24 | `add(Entry, Jsh) -> any()`
25 |
26 |
27 |
28 |
29 | ### from_list/2 ###
30 |
31 | `from_list(MaxL, L0) -> any()`
32 |
33 |
34 |
35 |
36 | ### new/1 ###
37 |
38 | `new(Length) -> any()`
39 |
40 |
41 |
42 |
43 | ### take_last/2 ###
44 |
45 | `take_last(F, Jsh) -> any()`
46 |
47 |
48 |
49 |
50 | ### to_list/1 ###
51 |
52 | `to_list(Jsh) -> any()`
53 |
54 |
55 |
--------------------------------------------------------------------------------
/doc/jobs_sampler_mnesia.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_sampler_mnesia #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 | __Behaviours:__ [`jobs_sampler`](jobs_sampler.md).
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### calc/2 ###
23 |
24 | `calc(History, St) -> any()`
25 |
26 |
27 |
28 |
29 | ### handle_msg/3 ###
30 |
31 | `handle_msg(X1, T, S) -> any()`
32 |
33 |
34 |
35 |
36 | ### init/2 ###
37 |
38 | `init(Name, Opts) -> any()`
39 |
40 |
41 |
42 |
43 | ### sample/2 ###
44 |
45 | `sample(T, S) -> any()`
46 |
47 |
48 |
--------------------------------------------------------------------------------
/doc/jobs_server.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_server #
4 | * [Data Types](#types)
5 | * [Function Index](#index)
6 | * [Function Details](#functions)
7 |
8 | __Behaviours:__ [`gen_server`](gen_server.md).
9 |
10 | __Authors:__ : Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)).
11 |
12 |
13 |
14 | ## Data Types ##
15 |
16 |
17 |
18 |
19 | ### info_category() ###
20 |
21 |
22 |
23 |
24 | info_category() = queues | group_rates | counters
25 |
26 |
27 |
28 |
29 |
30 |
31 | ### queue_name() ###
32 |
33 |
34 |
35 |
36 | queue_name() = any()
37 |
38 |
39 |
40 |
41 |
42 | ## Function Index ##
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ## Function Details ##
51 |
52 |
53 |
54 | ### add_counter/2 ###
55 |
56 | `add_counter(Name, Options) -> any()`
57 |
58 |
59 |
60 |
61 | ### add_group_rate/2 ###
62 |
63 | `add_group_rate(Name, Options) -> any()`
64 |
65 |
66 |
67 |
68 | ### add_queue/2 ###
69 |
70 |
71 |
72 | add_queue(Name::queue_name(), Options::[option()]) -> ok
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ### ask/0 ###
82 |
83 |
84 |
85 | ask() -> {ok, any()} | {error, rejected | timeout}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | ### ask/1 ###
95 |
96 |
97 |
98 | ask(Type::job_class()) -> {ok, reg_obj()} | {error, rejected | timeout}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | ### ask_queue/2 ###
108 |
109 |
110 |
111 | ask_queue(QName, Request) -> Reply
112 |
113 |
114 |
115 |
116 |
117 |
118 | Invoke the Q:handle_call/3 function (if it exists).
119 |
120 |
121 | Send a request to a specific queue in the JOBS server.
122 | Each queue has its own local state, allowing it to collect special statistics.
123 | This function allows a client to send a request that is handled by a specific
124 | queue instance, either to pull information from the queue, or to influence its
125 | state.
126 |
127 |
128 | ### code_change/3 ###
129 |
130 | `code_change(FromVsn, St, Extra) -> any()`
131 |
132 |
133 |
134 |
135 | ### delete_counter/1 ###
136 |
137 | `delete_counter(Name) -> any()`
138 |
139 |
140 |
141 |
142 | ### delete_group_rate/1 ###
143 |
144 | `delete_group_rate(Name) -> any()`
145 |
146 |
147 |
148 |
149 | ### delete_queue/1 ###
150 |
151 |
152 |
153 | delete_queue(Name::queue_name()) -> ok
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | ### dequeue/2 ###
163 |
164 |
165 |
166 | dequeue(Type::job_class(), N::integer() | infinity) -> [{timestamp(), any()}]
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | ### done/1 ###
176 |
177 |
178 |
179 | done(Opaque::reg_obj()) -> ok
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | ### enqueue/2 ###
189 |
190 |
191 |
192 | enqueue(Type::job_class(), Item::any()) -> ok
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 | ### handle_call/3 ###
202 |
203 | `handle_call(Req, From, S) -> any()`
204 |
205 |
206 |
207 |
208 | ### handle_cast/2 ###
209 |
210 | `handle_cast(Msg, St) -> any()`
211 |
212 |
213 |
214 |
215 | ### handle_info/2 ###
216 |
217 | `handle_info(Msg, St) -> any()`
218 |
219 |
220 |
221 |
222 | ### info/1 ###
223 |
224 |
225 |
226 | info(Item::info_category()) -> [any()]
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | ### init/1 ###
236 |
237 |
238 |
239 | init(Opts::[option()]) -> {ok, #st{}}
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | ### modify_counter/2 ###
249 |
250 | `modify_counter(Name, Opts) -> any()`
251 |
252 |
253 |
254 |
255 | ### modify_group_rate/2 ###
256 |
257 | `modify_group_rate(Name, Opts) -> any()`
258 |
259 |
260 |
261 |
262 | ### modify_regulator/4 ###
263 |
264 | `modify_regulator(Type, QName, RegName, Opts) -> any()`
265 |
266 |
267 |
268 |
269 | ### queue_info/1 ###
270 |
271 | `queue_info(Name) -> any()`
272 |
273 |
274 |
275 |
276 | ### queue_info/2 ###
277 |
278 | `queue_info(Name, Item) -> any()`
279 |
280 |
281 |
282 |
283 | ### run/1 ###
284 |
285 |
286 |
287 | run(Fun::fun(() -> X)) -> X
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 | ### run/2 ###
297 |
298 |
299 |
300 | run(Type::job_class(), Fun::fun(() -> X)) -> X
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 | ### set_modifiers/1 ###
310 |
311 | `set_modifiers(Modifiers) -> any()`
312 |
313 |
314 |
315 |
316 | ### start_link/0 ###
317 |
318 |
319 |
320 | start_link() -> {ok, pid()}
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 | ### start_link/1 ###
330 |
331 |
332 |
333 | start_link(Opts0::[option()]) -> {ok, pid()}
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | ### terminate/2 ###
343 |
344 | `terminate(X1, X2) -> any()`
345 |
346 |
347 |
348 |
349 | ### timestamp/0 ###
350 |
351 | `timestamp() -> any()`
352 |
353 |
354 |
355 |
356 | ### timestamp_to_datetime/1 ###
357 |
358 | `timestamp_to_datetime(TS) -> any()`
359 |
360 |
361 |
--------------------------------------------------------------------------------
/doc/jobs_stateful_simple.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module jobs_stateful_simple #
4 | * [Function Index](#index)
5 | * [Function Details](#functions)
6 |
7 |
8 |
9 |
10 | ## Function Index ##
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Function Details ##
19 |
20 |
21 |
22 | ### handle_call/4 ###
23 |
24 | `handle_call(Req, From, Stateful, Info) -> any()`
25 |
26 |
27 |
28 |
29 | ### init/2 ###
30 |
31 | `init(F, Info) -> any()`
32 |
33 |
34 |
35 |
36 | ### next/3 ###
37 |
38 | `next(Opaque, Stateful, Info) -> any()`
39 |
40 |
41 |
--------------------------------------------------------------------------------
/doc/overview.edoc:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | @copyright 2010 Erlang Solutions Ltd.
18 | @version 0.1
19 | @title jobs - a Job scheduler for load regulation
20 |
21 | @doc
22 |
23 | JOBS
24 | ====
25 |
26 | Jobs is a job scheduler for load regulation of Erlang applications.
27 | It provides a queueing framework where each queue can be configured
28 | for throughput rate, credit pool and feedback compensation.
29 | Queues can be added and modified at runtime, and customizable
30 | "samplers" propagate load status across all nodes in the system.
31 |
32 | Specifically, jobs provides three features:
33 |
34 | * Job scheduling: A job is scheduled according to certain constraints.
35 | For instance, you may want to define that no more than 9 jobs of a
36 | certain type can execute simultaneously and the maximal rate at
37 | which you can start such jobs are 300 per second.
38 | * Job queueing: When load is higher than the scheduling limits
39 | additional jobs are *queued* by the system to be run later when load
40 | clears. Certain rules govern queues: are they dequeued in FIFO or
41 | LIFO order? How many jobs can the queue take before it is full? Is
42 | there a deadline after which jobs should be rejected. When we hit
43 | the queue limits we reject the job. This provides a feedback
44 | mechanism on the client of the queue so you can take action.
45 | * Sampling and dampening: Periodic samples of the Erlang VM can
46 | provide information about the health of the system in general. If we
47 | have high CPU load or high memory usage, we apply dampening to the
48 | scheduling rules: we may lower the concurrency count or the rate at
49 | which we execute jobs. When the health problem clears, we remove the
50 | dampener and run at full speed again.
51 |
52 | Examples
53 | --------
54 |
55 | To be done
56 |
57 | Prerequisites
58 | -------------
59 | This application requires 'exprecs'.
60 | The 'exprecs' module is part of http://github.com/esl/parse_trans
61 |
62 | Contribute
63 | ----------
64 | For issues, comments or feedback please [create an issue!] [1]
65 |
66 | [1]: http://github.com/esl/jobs/issues "jobs issues"
67 |
68 | @end
--------------------------------------------------------------------------------
/doc/stylesheet.css:
--------------------------------------------------------------------------------
1 | /* standard EDoc style sheet */
2 | body {
3 | font-family: Verdana, Arial, Helvetica, sans-serif;
4 | margin-left: .25in;
5 | margin-right: .2in;
6 | margin-top: 0.2in;
7 | margin-bottom: 0.2in;
8 | color: #000000;
9 | background-color: #ffffff;
10 | }
11 | h1,h2 {
12 | margin-left: -0.2in;
13 | }
14 | div.navbar {
15 | background-color: #add8e6;
16 | padding: 0.2em;
17 | }
18 | h2.indextitle {
19 | padding: 0.4em;
20 | background-color: #add8e6;
21 | }
22 | h3.function,h3.typedecl {
23 | background-color: #add8e6;
24 | padding-left: 1em;
25 | }
26 | div.spec {
27 | margin-left: 2em;
28 | background-color: #eeeeee;
29 | }
30 | a.module,a.package {
31 | text-decoration:none
32 | }
33 | a.module:hover,a.package:hover {
34 | background-color: #eeeeee;
35 | }
36 | ul.definitions {
37 | list-style-type: none;
38 | }
39 | ul.index {
40 | list-style-type: none;
41 | background-color: #eeeeee;
42 | }
43 |
44 | /*
45 | * Minor style tweaks
46 | */
47 | ul {
48 | list-style-type: square;
49 | }
50 | table {
51 | border-collapse: collapse;
52 | }
53 | td {
54 | padding: 3
55 | }
56 |
--------------------------------------------------------------------------------
/examples/jobs_cpu.gnu:
--------------------------------------------------------------------------------
1 | set terminal png transparent nocrop enhanced font arial 8 size 800,600
2 |
3 | set autoscale y
4 | set autoscale y2
5 |
6 | set key autotitle columnhead
7 | plot "jobs_cpu.dat" using 2 with lines axes x1y1, "jobs_cpu.dat" using 3 with lines axes x2y2, "jobs_cpu.dat" using 4 with impulses axes x1y1, "jobs_cpu.dat" using 5 with impulses axes x1y1, "jobs_cpu.dat" using 6 with lines axes x1y1
8 |
9 |
--------------------------------------------------------------------------------
/examples/performance_logger.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2011 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | -module(performance_logger).
18 | -behaviour(gen_server).
19 |
20 | %% Interface
21 | -export([start_link/0,
22 | increment_counter/2,
23 | decrement_counter/2,
24 | set_counter/2,
25 | start_recording/1,
26 | end_recording/0,
27 | save_recorded_data_to_file/1]).
28 |
29 | %% gen_server-specific
30 | -export([init/1,
31 | handle_cast/2]).
32 |
33 | %% internal
34 | -export([tick/0]).
35 |
36 | %% testing
37 | -export([test_recording/0]).
38 |
39 | -export([spec_to_gnuplot_script/1]).
40 |
41 | -define(SAMPLING_TIME, 125). %% Duration between samples, in msec.
42 | -define(PFL_ETS_NAME, pfl_stats).
43 |
44 | -define(TIME_COMPUTATION_SCALE, 1).
45 |
46 | %% :)
47 | -define(WITH_OPEN_FILE(Anaphora,FileName,Modes,Code), case file:open(FileName,Modes) of
48 | {ok, Anaphora} -> Code, file:close(Anaphora);
49 | SthElse -> SthElse
50 | end).
51 |
52 | %% Data spec description
53 | -type counter_type() :: fun(() -> any()) | atom(). %% For custom counters one can supply a 0-arity fun instead of an usual name.
54 | -type counter_name() :: string(). %% Name used on chart as a label.
55 | -type sampling_type() :: diff | identity | accumulate.
56 | -type data_spec() :: [{counter_type(), counter_name(), sampling_type()}].
57 |
58 |
59 | %% GENSERVERIFY BEGIN
60 | %% Interface
61 | start_link() ->
62 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
63 |
64 | %%% Counter operations
65 | increment_counter(CounterName, Increment) ->
66 | gen_server:cast(?MODULE, {incf, CounterName, Increment}).
67 |
68 | decrement_counter(CounterName, Decrement) ->
69 | gen_server:cast(?MODULE, {decf, CounterName, Decrement}).
70 |
71 | set_counter(CounterName, NewValue) ->
72 | gen_server:cast(?MODULE, {setf, CounterName, NewValue}).
73 |
74 | %%% Data recording
75 | %% Note the DataSpec.
76 | -spec start_recording(DataSpec :: data_spec()) -> any().
77 | start_recording(DataSpec) ->
78 | gen_server:cast(?MODULE, {start_recording, DataSpec}).
79 |
80 | end_recording() ->
81 | gen_server:cast(?MODULE, end_recording).
82 |
83 | save_recorded_data_to_file(FileName) ->
84 | gen_server:cast(?MODULE, {save_data, FileName}).
85 |
86 | %% A call to tick causes the Logger to take a snapshot of current counter values and save that as a sample.
87 | %% NOTE if we get flooded with calls to
88 | %% increment/decrement/set_counter and gen_server starts falling
89 | %% behind, delays between samples may not exactly match the specified rate.
90 | tick() ->
91 | gen_server:cast(?MODULE, ping).
92 |
93 | %% Implementation
94 | -record(state, {data_spec, %% data spec, see description at the top of the file
95 | start_time, %% the time of the start of start_recording,it is used to calculate the offset from this start time, the offset is used to indicate the descending order of the ets table "pfl_stats" key
96 | timer}). %% reference to timer that makes us take samples every now and then
97 |
98 | init(_State) ->
99 | ets:new(?PFL_ETS_NAME, [ordered_set, named_table, public]),
100 | {ok, #state{}}.
101 |
102 | handle_cast({incf, CounterName, Increment}, _State) ->
103 | setf(CounterName, getf(CounterName) + Increment),
104 | {noreply, _State};
105 |
106 | handle_cast({decf, CounterName, Decrement}, _State) ->
107 | setf(CounterName, getf(CounterName) - Decrement),
108 | {noreply, _State};
109 |
110 | handle_cast({setf, CounterName, NewValue}, _State) ->
111 | setf(CounterName, NewValue),
112 | {noreply, _State};
113 |
114 | %%% Data collection
115 | handle_cast(ping, State = #state{data_spec = DataSpec}) ->
116 | NewSample = calculate_sample(DataSpec),
117 | store_sample({compute_time_offset(State), NewSample}),
118 | {noreply, State};
119 |
120 | %%% Data recording
121 | %% Note the DataSpec.
122 | handle_cast({start_recording, DataSpec}, State) ->
123 | ets:delete_all_objects(?PFL_ETS_NAME),
124 | {ok, TRef} = timer:apply_interval(?SAMPLING_TIME, ?MODULE, tick, []),
125 | {noreply, State#state{start_time = erlang:now(),
126 | %% we add two default counters, that measure CPU load and memory usage.
127 | data_spec = [{fun cpu_load/0, "CPULoad", identity},
128 | {fun memory_use/0, "MemoryUse", identity} | DataSpec],
129 | timer = TRef}};
130 |
131 | handle_cast(end_recording, State) ->
132 | timer:cancel(State#state.timer),
133 | {noreply, State};
134 |
135 |
136 | handle_cast({save_data, FileName}, State = #state{data_spec = DataSpec}) ->
137 | FirstElement = first_element(),
138 | ?WITH_OPEN_FILE(File, FileName, [write],
139 | begin
140 | save_header(DataSpec, File),
141 | save_data(FirstElement,
142 | next_element(FirstElement),
143 | lists:duplicate(length(DataSpec), 0), %% we need an auxiliary list for each counter, initialized to all zeros
144 | DataSpec,
145 | File)
146 | end),
147 | {noreply, State}.
148 |
149 | sample_to_string({Timestamp, Entry}) ->
150 | io_lib:format("~p~s~n", [Timestamp, lists:foldl(fun(W, Acc) -> Acc ++ "\t" ++ io_lib:format("~p",[W]) end, [], Entry)]).
151 |
152 | %% Measure an approximate current CPU load value.
153 | cpu_load() ->
154 | cpu_sup:util().
155 | memory_use() ->
156 | erlang:memory(total).
157 |
158 | store_sample(Sample) ->
159 | ets:insert(?PFL_ETS_NAME, Sample).
160 |
161 | %%%% Tools for victory. No refunds.
162 | %%%% (also, not really tested)
163 | setf(Counter, Value) ->
164 | put(Counter, Value).
165 |
166 | getf(Counter) when is_function(Counter) ->
167 | Counter();
168 |
169 | %% NOTE that getf will convert 'undefined' atom to 0, so that
170 | %% we may assume we're working with numerical counters.
171 | getf(Counter) ->
172 | case get(Counter) of
173 | undefined ->
174 | 0;
175 | Value ->
176 | Value
177 | end.
178 |
179 |
180 | calculate_sample(Counters) ->
181 | lists:map(fun({Counter, _Name, _SamplingType}) ->
182 | getf(Counter) end,
183 | Counters).
184 |
185 | compute_time_offset(#state{start_time = StartTime}) ->
186 | timer:now_diff(erlang:now(), StartTime) * ?TIME_COMPUTATION_SCALE.
187 |
188 | %%
189 | save_header(DataSpec, File) ->
190 | file:write(File, "Timestamp" ++ lists:foldl(fun({_, Name, _}, Acc) -> Acc ++ "\t" ++ Name end, [], DataSpec) ++ "\n").
191 |
192 | %% Save measured data to file.
193 | %% save_data(Stream of data,
194 | %% Stream of data, shifted by one element left,
195 | %% Additional buffer for integration,
196 | %% DataSpec,
197 | %% File to save to).
198 | %%
199 | %% All data in the input stream represent the measured values of counters. However, the API allows us to specify that
200 | %% some counters should have their data integrated, and others should record differences between recent values.
201 | %% Feeding in the data stream both as normal and shifted by one element allows us to compute differences, while
202 | %% additional buffer allows us to integrate specific counters.
203 | save_data(_, end_of_data, _, _, _) ->
204 | ok;
205 | save_data(Fn_1, Fn = {Timestamp, _}, Result, Data, File) ->
206 | Out = compute_sample_value(Fn_1, Fn, Result, Data),
207 | file:write(File, sample_to_string({Timestamp, Out})),
208 | save_data(Fn, next_element(Fn), Out, Data, File).
209 |
210 | compute_sample_value({_, Fn_1}, {_, Fn}, Result, Data) ->
211 | zipwith4(fun(Cntr_prev, Cntr, Cntr_local, {_, _, CntrType}) ->
212 | case CntrType of
213 | identity -> Cntr;
214 | diff -> Cntr - Cntr_prev;
215 | accumulate -> Cntr + Cntr_local
216 | end
217 | end,
218 | Fn_1,
219 | Fn,
220 | Result,
221 | Data).
222 |
223 | %% Helper functions for working with stream that comes from a specific ETS table.
224 | first_element() ->
225 | [WhatINeed] = ets:lookup(?PFL_ETS_NAME, ets:first(?PFL_ETS_NAME)),
226 | WhatINeed.
227 |
228 | next_element({Key, _Value}) ->
229 | case ets:next(?PFL_ETS_NAME, Key) of
230 | '$end_of_table' -> end_of_data;
231 | NextKey -> [WhatINeed] = ets:lookup(?PFL_ETS_NAME, NextKey),
232 | WhatINeed
233 | end.
234 |
235 | %% zipwith4 - lists:zipwith for 4 lists.
236 | zipwith4(Combine, List1, List2, List3, List4) ->
237 | zipwith4(Combine, List1, List2, List3, List4, []).
238 |
239 | zipwith4(_, [], [], [], [], Accu) ->
240 | lists:reverse(Accu);
241 | zipwith4(Combine, [H1 | T1], [H2 | T2], [H3 | T3], [H4 | T4], Accu) ->
242 | zipwith4(Combine, T1, T2, T3, T4, [Combine(H1, H2, H3, H4) | Accu]).
243 |
244 | %% TODO needs real implementation
245 | spec_to_gnuplot_script(DataSpec) ->
246 | io_lib:format("set terminal png transparent nocrop enhanced font arial 8 size 800,600
247 |
248 | set autoscale y
249 | set autoscale y2
250 |
251 | set key autotitle columnhead
252 |
253 | plot \"foobar\" ", []).
254 |
255 |
256 | %% Test if the whole recording business works.
257 | %% NOTE, to verify the test, one needs to inspect the proper .txt file saved by it.
258 | test_recording() ->
259 | increment_counter(foobar, 42),
260 | increment_counter(firebirds, 15),
261 | increment_counter(jane, 24),
262 | start_recording([{foobar, "foobziubar", identity},
263 | {jane, "JaneDwim", diff},
264 | {firebirds, "24zachody", accumulate}]),
265 | timer:sleep(500),
266 | decrement_counter(foobar, 42),
267 | decrement_counter(jane, 14),
268 | decrement_counter(firebirds, 14),
269 | timer:sleep(1500),
270 | decrement_counter(foobar, 42),
271 | decrement_counter(jane, 14),
272 | decrement_counter(firebirds, 14),
273 | timer:sleep(1500),
274 | decrement_counter(foobar, 42),
275 | decrement_counter(firebirds, 14),
276 | timer:sleep(1500),
277 | increment_counter(foobar, 45),
278 | increment_counter(jane, 55),
279 | increment_counter(firebirds, 55),
280 | timer:sleep(1500),
281 | increment_counter(jane, 55),
282 | timer:sleep(1500),
283 | end_recording(),
284 | save_recorded_data_to_file("itworks.txt").
285 |
--------------------------------------------------------------------------------
/examples/performer.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2011 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | -module(performer).
18 |
19 | -export([start/0, stop/0]).
20 | -export([a_test_scenario/0]).
21 | -define(DELAY_BETWEEN_TASKS, 15).
22 | start() ->
23 | register(?MODULE, spawn(fun run_cpu_stresstests/0)).
24 |
25 | stop() ->
26 | ?MODULE ! stop.
27 |
28 | run_cpu_stresstests() ->
29 | jobs:add_queue(ramirez,
30 | [{regulators, [{rate, [{limit, 20},
31 | {modifiers,
32 | [{cpu, 15, {max, 0}}]}]}]}]),
33 | spawn_cpu_intensive_jobs().
34 | spawn_cpu_intensive_jobs() ->
35 | receive
36 | stop ->
37 | ok
38 | after ?DELAY_BETWEEN_TASKS ->
39 | spawn(fun() -> jobs:run(ramirez, fun cpu_intensive_job/0) end),
40 | performance_logger:increment_counter(jobs_enqueued, 1),
41 | spawn_cpu_intensive_jobs()
42 | end.
43 |
44 | cpu_intensive_job() ->
45 | %% TODO insert real CPU-intensive task here.
46 | %% NOTE that somehow, this seems like enough to crank up the CPU usage.
47 | timer:sleep(500),
48 | performance_logger:increment_counter(jobs_done, 1).
49 |
50 | queue_frequency() ->
51 | jobs:queue_info(ramirez, rate_limit).
52 |
53 | %% A simple test scenario that should show you the basic feedback reaction to CPU usage.
54 | %% NOTE that you need to start Jobs with following environmental variable setting:
55 | %% samplers <= [{foobar, jobs_sampler_cpu, []}]
56 | %% where 'foobar' can be - as far as I can tell - any atom.
57 | %% NOTE that you need to set 'samplers', not 'sampler' - setting the latter will result in Jobs not working.
58 | a_test_scenario() ->
59 | performance_logger:set_counter(jobs_enqueued, 0),
60 | performance_logger:set_counter(jobs_done, 0),
61 | start(),
62 | timer:sleep(500),
63 | performance_logger:start_recording([{jobs_enqueued, "\"Jobs enqueued\"", diff},
64 | {jobs_done, "\"Jobs done\"", diff},
65 | {fun queue_frequency/0, "\"Queue frequency\"", identity}]),
66 | timer:sleep(12000),
67 | stop(),
68 | timer:sleep(16000),
69 | performance_logger:end_recording(),
70 | performance_logger:save_recorded_data_to_file("jobs_cpu.dat"),
71 | ok.
72 |
--------------------------------------------------------------------------------
/include/jobs.hrl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs.hrl
19 | %% @author : Ulf Wiger
20 | %% @end
21 | %% Description :
22 | %%
23 | %% Created : 15 Jan 2010 by Ulf Wiger
24 | %%-------------------------------------------------------------------
25 |
26 | -export_type([counter/0, reg_obj/0]).
27 |
28 | -type job_class() :: any().
29 | -opaque counter() :: {any(), any()}.
30 | -opaque reg_obj() :: [counter()].
31 |
32 | -type option() :: {queues, [q_spec()]}
33 | | {config, file:name()}
34 | | {group_rates, [{q_name(), [option()]}]}
35 | | {counters, [{q_name(), [option()]}]}
36 | | {interval, integer()}.
37 | -type timestamp() :: integer(). % microseconds with a special epoch
38 |
39 | -type q_name() :: any().
40 | -type q_std_type() :: standard_rate | standard_counter.
41 | -type q_opts() :: [{atom(), any()}].
42 | -type q_spec() :: {q_name(), q_std_type(), q_opts()}
43 | | {q_name(), q_opts()}.
44 |
45 | -type q_modifiers() :: [q_modifier()].
46 | -type q_modifier() :: {cpu, integer()} % predefined
47 | | {memory, integer()} % predefined
48 | | {any(), integer()}. % user-defined
49 |
50 |
51 | -record(rate, {limit = 0,
52 | preset_limit = 0,
53 | interval,
54 | modifiers = [],
55 | active_modifiers = []}).
56 |
57 | -record(counter, {name, increment = undefined}).
58 | -record(group_rate, {name}).
59 |
60 | -record(rr,
61 | %% Rate-based regulation
62 | {name,
63 | rate = #rate{}}).
64 | % limit = 0 :: float(),
65 | % interval = 0 :: undefined | float(),
66 | % modifiers = [] :: [{atom(),integer()}],
67 | % active_modifiers = [] :: [{atom(),integer()}],
68 | % preset_limit = 0}).
69 |
70 | -record(cr,
71 | %% Counter-based regulation
72 | {name,
73 | increment = 1,
74 | value = 0,
75 | rate = #rate{},
76 | owner,
77 | queues = [],
78 | % limit = 5,
79 | % interval = 50,
80 | % modifiers = [] :: [{atom(),integer()}],
81 | % active_modifiers = [] :: [{atom(),integer()}],
82 | % preset_limit = 5,
83 | shared = false}).
84 |
85 | -record(grp, {name,
86 | rate = #rate{},
87 | latest_dispatch=0 :: integer()}).
88 | % modifiers = [] :: [{atom(),integer()}],
89 | % active_modifiers = [] :: [{atom(),integer()}],
90 | % limit = 0 :: float(),
91 | % preset_limit = 0 :: float(),
92 | % interval :: float()}).
93 |
94 | -type regulator() :: #rr{} | #cr{} | regulator_ref().
95 | -type regulator_ref() :: #group_rate{} | #counter{}.
96 |
97 | -type m_f_args() :: {atom(), atom(), list()}.
98 |
99 | %% -record(producer, {f={erlang,error,[undefined_producer]}
100 | %% :: m_f_args() | function(),
101 | %% mode = spawn :: spawn | {stateful, }).
102 | -record(producer, {mod = jobs_prod_simple,
103 | state}).
104 |
105 | %% -record(producer, {f={erlang,error,[undefined_producer]}
106 | %% :: m_f_args() | function()}).
107 | -record(passive , {type = fifo :: fifo}).
108 | -record(action , {a = approve :: approve | reject}).
109 |
110 | -record(queue, {name :: any(),
111 | mod :: atom(),
112 | type = fifo :: fifo | lifo | #producer{} | #passive{}
113 | | #action{},
114 | group :: atom(),
115 | regulators = [] :: [regulator() | regulator_ref()],
116 | max_time :: undefined | integer(),
117 | max_size :: undefined | integer(),
118 | latest_dispatch = 0 :: integer(),
119 | approved = 0,
120 | queued = 0,
121 | check_interval :: integer() | mfa(),
122 | oldest_job :: undefined | integer(),
123 | timer,
124 | check_counter = 0 :: integer(),
125 | waiters = [] :: [{pid(), reference()}],
126 | stateful,
127 | st
128 | }).
129 |
130 | -record(sampler, {name,
131 | mod,
132 | mod_state,
133 | type, % binary | meter
134 | step, % {seconds, [{Secs,Step}]}|{levels,[{Level,Step}]}
135 | hist_length = 10,
136 | history = queue:new()}).
137 |
138 | -record(stateless, {f}).
139 | -record(stateful, {f, st}).
140 |
141 | %% Gproc counter objects for counter-based regulation
142 | %% Each worker process gets a counter object. The aggregated counter,
143 | %% owned by the jobs_server, maintains a running tally of the concurrently
144 | %% existing counter objects of the given name.
145 | %%
146 | -define(COUNTER(Name), {c,l,{?MODULE,Name}}).
147 | -define( AGGR(Name), {a,l,{?MODULE,Name}}).
148 |
149 | -define(COUNTER_SAMPLE_INTERVAL, infinity).
150 |
151 | %% The jobs_server may, under certain circumstances, generate error reports
152 | %% This value, in microseconds, defines the highest frequency with which
153 | %% it can issue error reports. Any reports that would cause this limit to
154 | %% be exceeded are simply discarded.
155 | %
156 | -define(MAX_ERROR_RPT_INTERVAL_US, 1000000).
157 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | {erl_opts, [debug_info]}.
3 | {xref_checks, [undefined_function_calls]}.
4 |
5 | {cover_enabled, true}.
6 | {eunit_opts, [verbose]}.
7 |
8 | {clean_files, ["*~","*/*~","*/*.xfm","test/*.beam"]}.
9 |
10 | {deps, [
11 | {meck, ".*",
12 | {git, "git://github.com/eproxus/meck.git", "0.8.2"}},
13 | {parse_trans, ".*",
14 | {git, "git://github.com/esl/parse_trans.git", "2.8"}},
15 | {edown, ".*",
16 | {git, "git://github.com/esl/edown.git", "0.4"}}
17 | ]}.
18 |
19 | {edoc_opts, [{doclet, edown_doclet},
20 | {top_level_readme,
21 | {"./README.md",
22 | "http://github.com/esl/jobs"}}]}.
23 |
--------------------------------------------------------------------------------
/src/jobs.app.src:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | %%==============================================================================
3 | %% Copyright 2010 Erlang Solutions Ltd.
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 | %%==============================================================================
17 |
18 | {application, jobs,
19 | [
20 | {vsn, git},
21 | {description, "Job scheduler"},
22 | {applications, [kernel, stdlib]},
23 | {registered, []},
24 | {mod, {jobs_app, []}},
25 | {env, []}
26 | ]}.
27 |
--------------------------------------------------------------------------------
/src/jobs.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs.erl
19 | %% @author : Ulf Wiger
20 | %% @doc
21 | %% This is the public API of the JOBS framework.
22 | %%
23 | %% @end
24 | %% Created : 15 Jan 2010 by Ulf Wiger
25 | %%-------------------------------------------------------------------
26 | -module(jobs).
27 |
28 | -export([ask/1,
29 | done/1,
30 | job_info/1,
31 | run/2,
32 | enqueue/2,
33 | dequeue/2]).
34 |
35 | -export([ask_queue/2]).
36 |
37 |
38 | %% Configuration API
39 | -export([add_queue/2,
40 | delete_queue/1,
41 | info/1,
42 | queue_info/1,
43 | queue_info/2,
44 | modify_regulator/4,
45 | add_counter/2,
46 | modify_counter/2,
47 | delete_counter/1,
48 | add_group_rate/2,
49 | modify_group_rate/2,
50 | delete_group_rate/1]).
51 |
52 |
53 | %% @spec ask(Type) -> {ok, Opaque} | {error, Reason}
54 | %% @doc Asks permission to run a job of Type. Returns when permission granted.
55 | %%
56 | %% The simplest way to have jobs regulated is to spawn a request per job.
57 | %% The process should immediately call this function, and when granted
58 | %% permission, execute the job, and then terminate.
59 | %% If for some reason the process needs to remain, to execute more jobs,
60 | %% it should explicitly call `jobs:done(Opaque)'.
61 | %% This is not strictly needed when regulation is rate-based, but as the
62 | %% regulation strategy may change over time, it is the prudent thing to do.
63 | %% @end
64 | %%
65 | ask(Type) ->
66 | jobs_server:ask(Type).
67 |
68 | %% @spec done(Opaque) -> ok
69 | %% @doc Signals completion of an executed task.
70 | %%
71 | %% This is used when the current process wants to submit more jobs to load
72 | %% regulation. It is mandatory when performing counter-based regulation
73 | %% (unless the process terminates after completing the task). It has no
74 | %% effect if the job type is purely rate-regulated.
75 | %% @end
76 | %%
77 | done(Opaque) ->
78 | jobs_server:done(Opaque).
79 |
80 | %% @spec run(Type, Function::function()) -> Result
81 | %% @doc Executes Function() when permission has been granted by job regulator.
82 | %%
83 | %% This is equivalent to performing the following sequence:
84 | %%
85 | %% case jobs:ask(Type) of
86 | %% {ok, Opaque} ->
87 | %% try Function()
88 | %% after
89 | %% jobs:done(Opaque)
90 | %% end;
91 | %% {error, Reason} ->
92 | %% erlang:error(Reason)
93 | %% end.
94 | %%
95 | %% @end
96 | %%
97 | run(Queue, F) when is_function(F, 0); is_function(F, 1) ->
98 | jobs_server:run(Queue, F).
99 |
100 | enqueue(Queue, Item) ->
101 | jobs_server:enqueue(Queue, Item).
102 |
103 | dequeue(Queue, N) when N =:= infinity; is_integer(N), N > 0 ->
104 | jobs_server:dequeue(Queue, N).
105 |
106 | %% @spec job_info(Opaque) -> undefined | Info
107 | %% @doc Retrieves job-specific information from the `Opaque' data object.
108 | %%
109 | %% The queue could choose to return specific information that is passed to a
110 | %% granted job request. This could be used e.g. for load-balancing strategies.
111 | %% @end
112 | %%
113 | job_info({_, Opaque}) ->
114 | proplists:get_value(info, Opaque).
115 |
116 | %% @spec add_queue(Name::any(), Options::[{Key,Value}]) -> ok
117 | %% @doc Installs a new queue in the load regulator on the current node.
118 | %% @end
119 | %%
120 | add_queue(Name, Options) ->
121 | jobs_server:add_queue(Name, Options).
122 |
123 | %% @spec delete_queue(Name) -> boolean()
124 | %% @doc Deletes the named queue from the load regulator on the current node.
125 | %% Returns `true' if there was in fact such a queue; `false' otherwise.
126 | %% @end
127 | %%
128 | delete_queue(Name) ->
129 | jobs_server:delete_queue(Name).
130 |
131 | %% @spec ask_queue(QueueName, Request) -> Reply
132 | %% @doc Sends a synchronous request to a specific queue.
133 | %%
134 | %% This function is mainly intended to be used for back-end processes that act
135 | %% as custom extensions to the load regulator itself. It should not be used by
136 | %% regular clients. Sophisticated queue behaviours could export gen_server-like
137 | %% logic allowing them to respond to synchronous calls, either for special
138 | %% inspection, or for influencing the queue state.
139 | %% @end
140 | %%
141 | ask_queue(QueueName, Request) ->
142 | jobs_server:ask_queue(QueueName, Request).
143 |
144 | %% @spec add_counter(Name, Options) -> ok
145 | %% @doc Adds a named counter to the load regulator on the current node.
146 | %% Fails if there already is a counter the name `Name'.
147 | %% @end
148 | %%
149 | add_counter(Name, Options) ->
150 | jobs_server:add_counter(Name, Options).
151 |
152 | %% @spec delete_counter(Name) -> boolean()
153 | %% @doc Deletes a named counter from the load regulator on the current node.
154 | %% Returns `true' if there was in fact such a counter; `false' otherwise.
155 | %% @end
156 | %%
157 | delete_counter(Name) ->
158 | jobs_server:delete_counter(Name).
159 |
160 | %% @spec add_group_rate(Name, Options) -> ok
161 | %% @doc Adds a group rate regulator to the load regulator on the current node.
162 | %% Fails if there is already a group rate regulator of the same name.
163 | %% @end
164 | %%
165 | add_group_rate(Name, Options) ->
166 | jobs_server:add_group_rate(Name, Options).
167 |
168 | delete_group_rate(Name) ->
169 | jobs_server:delete_group_rate(Name).
170 |
171 | info(Item) ->
172 | jobs_server:info(Item).
173 |
174 | queue_info(Name) ->
175 | jobs_server:queue_info(Name).
176 |
177 | queue_info(Name, Item) ->
178 | jobs_server:queue_info(Name, Item).
179 |
180 | modify_regulator(Type, QName, RegName, Opts) when Type==counter;Type==rate ->
181 | jobs_server:modify_regulator(Type, QName, RegName, Opts).
182 |
183 | modify_counter(CName, Opts) ->
184 | jobs_server:modify_counter(CName, Opts).
185 |
186 | modify_group_rate(GRName, Opts) ->
187 | jobs_server:modify_group_rate(GRName, Opts).
188 |
--------------------------------------------------------------------------------
/src/jobs_app.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %% @doc Application module for JOBS.
18 | %% Normally, JOBS is configured at startup, using a static configuration.
19 | %% There is a reconfiguration API {@link jobs}, which is mainly for evolution
20 | %% of the system.
21 | %%
22 | %% == Configuring JOBS ==
23 | %% A static configuration can be provided via application environment
24 | %% variables for the `jobs' application. The following is a list of
25 | %% recognised configuration parameters.
26 | %%
27 | %% === {config, Filename} ===
28 | %% Evaluate a file using {@link //kernel/file:script/1}, treating the data
29 | %% returned from the script as a list of configuration options.
30 | %%
31 | %% === {queues, QueueOptions} ===
32 | %% Configure a list of queues according to the provided QueueOptions.
33 | %% If no queues are specified, a queue named `default' will be created
34 | %% with default characteristics.
35 | %%
36 | %% Below are the different queue configuration options:
37 | %%
38 | %% ==== {Name, Options} ====
39 | %% This is the generic queue configuration pattern.
40 | %% `Name :: any()' is used to identify the queue.
41 | %%
42 | %% Options:
43 | %%
44 | %% `{mod, Module::atom()}' provides the name of the queueing module.
45 | %% The default module is `jobs_queue'.
46 | %%
47 | %% `{type, fifo | lifo | approve | reject | {producer, F}}'
48 | %% specifies the semantics of the queue. Note that the specified queue module
49 | %% may be limited to only one type (e.g. the `jobs_queue_list' module only
50 | %% supports `lifo' semantics).
51 | %%
52 | %% If the type is `{producer, F}', it doesn't matter which queue module is
53 | %% used, as it is not possible to submit job requests to a producer queue.
54 | %% The producer queue will initiate jobs using `spawn_monitor(F)' at the
55 | %% rate given by the regulators for the queue.
56 | %%
57 | %% If the type is `approve' or `reject', respectively, all other options will
58 | %% be irrelevant. Any request to the queue will either be immediately approved
59 | %% or immediately rejected.
60 | %%
61 | %% `{max_time, integer() | undefined}' specifies the longest time that a job
62 | %% request may spend in the queue. If `undefined', no limit is imposed.
63 | %%
64 | %% `{max_size, integer() | undefined}' specifies the maximum length (number
65 | %% of job requests) of the queue. If the queue has reached the maximum length,
66 | %% subsequent job requests will be rejected unless it is possible to remove
67 | %% enough requests that have exceeded the maximum allowed time in the queue.
68 | %%
69 | %% `{regulators, [{regulator_type(), Opts]}' specifies the regulation
70 | %% characteristics of the queue.
71 | %%
72 | %% The following types of regulator are supported:
73 | %%
74 | %% `regulator_type() :: rate | counter | group_rate'
75 | %%
76 | %% It is possible to combine different types of regulator on the same queue,
77 | %% e.g. a queue may have both rate- and counter regulation. It is not possible
78 | %% to have two different rate regulators for the same queue.
79 | %%
80 | %% Common regulator options:
81 | %%
82 | %% `{name, term()}' names the regulator; by default, a name will be generated.
83 | %%
84 | %% `{limit, integer()}' defines the limit for the regulator. If it is a rate
85 | %% regulator, the value represents the maximum number of jobs/second; if it
86 | %% is a counter regulator, it represents the total number of "credits"
87 | %% available.
88 | %%
89 | %% `{modifiers, [modifier()]}'
90 | %%
91 | %%
92 | %% modifier() :: {IndicatorName :: any(), unit()}
93 | %% | {Indicator, local_unit(), remote_unit()}
94 | %% | {Indicator, Fun}
95 | %%
96 | %% local_unit() :: unit() :: integer()
97 | %% remote_unit() :: {avg, unit()} | {max, unit()}
98 | %%
99 | %%
100 | %% Feedback indicators are sent from the sampler framework. Each indicator
101 | %% has the format `{IndicatorName, LocalLoadFactor, Remote}'.
102 | %%
103 | %% `Remote :: [{Node, LoadFactor}]'
104 | %%
105 | %% `IndicatorName' defines the type of indicator. It could be e.g. `cpu',
106 | %% `memory', `mnesia', or any other name defined by one of the sampler plugins.
107 | %%
108 | %% The effect of a modifier is calculated as the sum of the effects from local
109 | %% and remote load. As the remote load is represented as a list of
110 | %% `{Node,Factor}' it is possible to multiply either the average or the max
111 | %% load on the remote nodes with the given factor: `{avg,Unit} | {max, Unit}'.
112 | %%
113 | %% For custom interpretation of the feedback indicator, it is possible to
114 | %% specify a function `F(LocalFactor, Remote) -> Effect', where Effect is a
115 | %% positive integer.
116 | %%
117 | %% The resulting effect value is used to reduce the predefined regulator limit
118 | %% with the given number of percentage points, e.g. if a rate regulator has
119 | %% a predefined limit of 100 jobs/sec, and `Effect = 20', the current rate
120 | %% limit will become 80 jobs/sec.
121 | %%
122 | %% `{rate, Opts}' - rate regulation
123 | %%
124 | %% Currently, no special options exist for rate regulators.
125 | %%
126 | %% `{counter, Opts}' - counter regulation
127 | %%
128 | %% The option `{increment, I}' can be used to specify how much of the credit
129 | %% pool should be assigned to each job. The default increment is 1.
130 | %%
131 | %% `{named_counter, Name, Increment}' reuses an existing counter regulator.
132 | %% This can be used to link multiple queues to a shared credit pool. Note that
133 | %% this does not use the existing counter regulator as a template, but actually
134 | %% shares the credits with any other queues using the same named counter.
135 | %%
136 | %% __NOTE__ Currently, if there is no counter corresponding to the alias,
137 | %% the entry will simply be ignored during regulation. It is likely that this
138 | %% behaviour will change in the future.
139 | %%
140 | %% ==== {Name, standard_rate, R} ====
141 | %% A simple rate-regulated queue with throughput rate `R', and basic cpu- and
142 | %% memory-related feedback compensation.
143 | %%
144 | %% ==== {Name, standard_counter, N} ====
145 | %% A simple counter-regulated queue, giving each job a weight of 1, and thus
146 | %% allowing at most `N' jobs to execute concurrently. Basic cpu- and memory-
147 | %% related feedback compensation.
148 | %%
149 | %% ==== {Name, producer, F, Options} ====
150 | %% A producer queue is not open for incoming jobs, but will rather initiate
151 | %% jobs at the given rate.
152 | %% @end
153 | %%
154 | -module(jobs_app).
155 |
156 | -export([start/2, stop/1,
157 | init/1]).
158 |
159 |
160 | start(_, _) ->
161 | supervisor:start_link({local,?MODULE},?MODULE,[]).
162 |
163 | stop(_) ->
164 | ok.
165 |
166 |
167 | init([]) ->
168 | {ok, {{rest_for_one,3,10},
169 | [{jobs_server, {jobs_server,start_link,[]},
170 | permanent, 3000, worker, [jobs_server]}|
171 | sampler_spec()]}}.
172 |
173 |
174 | sampler_spec() ->
175 | Mod = case application:get_env(sampler) of
176 | {ok,M} when M =/= undefined -> M;
177 | _ -> jobs_sampler
178 | end,
179 | [{jobs_sampler, {Mod,start_link,[]}, permanent, 3000, worker, [Mod]}].
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/jobs_info.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 | -module(jobs_info).
17 |
18 | -export([pp/1]).
19 |
20 | -include("jobs.hrl").
21 | -include_lib("parse_trans/include/exprecs.hrl").
22 |
23 | -export_records([rr, cr, grp, rate, queue, sampler]).
24 |
25 |
26 | pp(L) when is_list(L) ->
27 | [pp(X) || X <- L];
28 | pp(X) ->
29 | case '#is_record-'(X) of
30 | true ->
31 | RecName = element(1,X),
32 | {RecName, lists:zip(
33 | '#info-'(RecName,fields),
34 | pp(tl(tuple_to_list(X))))};
35 | false ->
36 | if is_tuple(X) ->
37 | list_to_tuple(pp(tuple_to_list(X)));
38 | true ->
39 | X
40 | end
41 | end.
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/jobs_lib.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | -module(jobs_lib).
18 |
19 | -export([timestamp/0,
20 | timestamp_to_datetime/1]).
21 |
22 |
23 | timestamp() ->
24 | %% Invented epoc is {1258,0,0}, or 2009-11-12, 4:26:40
25 | {MS,S,US} = erlang:now(),
26 | (MS-1258)*1000000000 + S*1000 + US div 1000.
27 |
28 | timestamp_to_datetime(TS) ->
29 | %% Our internal timestamps are relative to Now = {1258,0,0}
30 | %% It doesn't really matter much how we construct a now()-like tuple,
31 | %% as long as the weighted sum of the three numbers is correct.
32 | S = TS div 1000,
33 | MS = TS rem 1000,
34 | %% return {Datetime, Milliseconds}
35 | {calendar:now_to_datetime({1258,S,0}), MS}.
36 |
--------------------------------------------------------------------------------
/src/jobs_prod_simple.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_prod_simple).
2 |
3 | -export([init/2,
4 | next/3]).
5 |
6 | -include("jobs.hrl").
7 |
8 | init(F, _Info) when is_function(F, 0) ->
9 | #stateless{f = F};
10 | init(F, Info) when is_function(F, 2) ->
11 | #stateful{f = F, st = F(init, Info)};
12 | init({_, F, A} = MFA, _Info) when is_atom(F), is_list(A) ->
13 | #stateless{f = MFA}.
14 |
15 | next(_Opaque, #stateful{f = F, st = St} = P, Info) ->
16 | case F(St, Info) of
17 | {F1, St1} when is_function(F1, 0) ->
18 | {F1, P#stateful{st = St1}};
19 | Other ->
20 | erlang:error({bad_producer_next, Other})
21 | end;
22 | next(_Opaque, #stateless{f = F} = P, _Info) ->
23 | case F of
24 | {M,Fn,A} ->
25 | {fun() -> apply(M, Fn, A) end, P};
26 | F when is_function(F, 0) ->
27 | {F, P}
28 | end.
29 |
--------------------------------------------------------------------------------
/src/jobs_queue.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs_queue.erl
19 | %% @author : Ulf Wiger
20 | %% @end
21 | %% Description :
22 | %%
23 | %% Created : 15 Jan 2010 by Ulf Wiger
24 | %%-------------------------------------------------------------------
25 | %% @doc Default queue behaviour for JOBS (using ordered_set ets).
26 | %%
27 | %% This module implements the default queue behaviour for JOBS, and also
28 | %% specifies the behaviour itself.
29 | %% @end
30 |
31 | -module(jobs_queue).
32 | -author('ulf.wiger@erlang-solutions.com').
33 | -copyright('Erlang Solutions Ltd.').
34 |
35 | -export([new/2,
36 | delete/1]).
37 | -export([in/3,
38 | out/2,
39 | peek/1,
40 | info/2,
41 | all/1,
42 | empty/1,
43 | is_empty/1,
44 | representation/1,
45 | timedout/1, timedout/2]).
46 |
47 | -export([behaviour_info/1]).
48 |
49 | -include("jobs.hrl").
50 | -import(jobs_server, [timestamp/0]).
51 |
52 | -record(st, {table}).
53 |
54 | %-type timestamp() :: integer().
55 | -type job() :: {pid(), reference()}.
56 | -type entry() :: {timestamp(), job()}.
57 |
58 | behaviour_info(callbacks) ->
59 | [{new , 2},
60 | {delete , 1},
61 | {in , 3},
62 | {peek , 1},
63 | {out , 2},
64 | {all , 1},
65 | {info , 2}];
66 | behaviour_info(_) ->
67 | undefined.
68 |
69 |
70 | %% @spec new(Options, #queue{}) -> #queue{}
71 | %% @doc Instantiate a new queue.
72 | %%
73 | %% Options is the list of options provided when defining the queue.
74 | %% Q is an initial #queue{} record. It can be used directly by including
75 | %% `jobs/include/jobs.hrl', or by using exprecs-style record accessors in the
76 | %% module `jobs_info'.
77 | %% See parse_trans for more info
78 | %% on exprecs. In the `new/2' function, the #queue.st attribute will normally be
79 | %% used to keep track of the queue data structure.
80 | %% @end
81 | %%
82 | new(Options, Q) ->
83 | case proplists:get_value(type, Options, fifo) of
84 | fifo ->
85 | Tab = ets:new(?MODULE, [ordered_set]),
86 | Q#queue{st = #st{table = Tab}}
87 | end.
88 |
89 | %% @doc A representation of a queue which can be inspected
90 | %% @end
91 | representation(
92 | #queue { oldest_job = OJ,
93 | st = #st { table = Tab}}) ->
94 | Contents = ets:match_object(Tab, '$1'),
95 | [{oldest_job, OJ},
96 | {contents, [X || {X} <- Contents]}].
97 |
98 | %% @spec delete(#queue{}) -> any()
99 | %% @doc Queue is being deleted; remove any external data structures.
100 | %%
101 | %% If the queue behaviour has created an ETS table or similar, this is the place
102 | %% to get rid of it.
103 | %% @end
104 | %%
105 | delete(#queue{st = #st{table = T}}) ->
106 | ets:delete(T).
107 |
108 |
109 |
110 | -spec in(timestamp(), job(), #queue{}) -> #queue{}.
111 | %% @spec in(Timestamp, Job, #queue{}) -> #queue{}
112 | %% @doc Enqueue a job reference; return the updated queue.
113 | %%
114 | %% This puts a job into the queue. The callback function is responsible for
115 | %% updating the #queue.oldest_job attribute, if needed. The #queue.oldest_job
116 | %% attribute shall either contain the Timestamp of the oldest job in the queue,
117 | %% or `undefined' if the queue is empty. It may be noted that, especially in the
118 | %% fairly trivial case of the `in/3' function, the oldest job would be
119 | %% `erlang:min(Timestamp, PreviousOldest)', even if `PreviousOldest == undefined'.
120 | %% @end
121 | %%
122 | in(TS, Job, #queue{st = #st{table = Tab}, oldest_job = OJ} = Q) ->
123 | OJ1 = erlang:min(TS, OJ), % Works even if OJ==undefined
124 | ets:insert(Tab, {{TS, Job}}),
125 | Q#queue{oldest_job = OJ1}.
126 |
127 |
128 | -spec peek(#queue{}) -> entry().
129 | %% @spec peek(#queue{}) -> JobEntry | undefined
130 | %% @doc Looks at the first item in the queue, without removing it.
131 | %%
132 | peek(#queue{st = #st{table = T}}) ->
133 | case ets:first(T) of
134 | '$end_of_table' ->
135 | undefined;
136 | Key ->
137 | Key
138 | end.
139 |
140 | -spec out(N :: integer(), #queue{}) -> {[entry()], #queue{}}.
141 | %% @spec out(N :: integer(), #queue{}) -> {[Entry], #queue{}}
142 | %% @doc Dequeue a batch of N jobs; return the modified queue.
143 | %%
144 | %% Note that this function may need to update the #queue.oldest_job attribute,
145 | %% especially if the queue becomes empty.
146 | %% @end
147 | %%
148 | out(N,#queue{st = #st{table = T}}=Q) when N >= 0 ->
149 | {out1(N, T), set_oldest_job(Q)}.
150 |
151 |
152 | -spec all(#queue{}) -> [entry()].
153 | %% @spec all(#queue{}) -> [JobEntry]
154 | %% @doc Return all the job entries in the queue, not removing them from the queue.
155 | %%
156 | all(#queue{st = #st{table = T}}) ->
157 | ets:select(T, [{{'$1'},[],['$1']}]).
158 |
159 |
160 | -type info_item() :: max_time | oldest_job | length.
161 |
162 | -spec info(info_item(), #queue{}) -> any().
163 | %% @spec info(Item, #queue{}) -> Info
164 | %% Item = max_time | oldest_job | length
165 | %% @doc Return information about the queue.
166 | %%
167 | info(max_time , #queue{max_time = T} ) -> T;
168 | info(oldest_job, #queue{oldest_job = OJ}) -> OJ;
169 | info(length , #queue{st = #st{table = Tab}}) ->
170 | ets:info(Tab, size).
171 |
172 | -spec timedout(#queue{}) -> [] | {[entry()], #queue{}}.
173 | %% @spec timedout(#queue{}) -> [] | {[Entry], #queue{}}
174 | %% @doc Return all entries that have been in the queue longer than MaxTime.
175 | %%
176 | %% NOTE: This is an inspection function; it doesn't remove the job entries.
177 | %% @end
178 | %%
179 | timedout(#queue{max_time = undefined} = Q) -> {[], Q};
180 | timedout(#queue{max_time = TO} = Q) ->
181 | timedout(TO, Q).
182 |
183 | timedout(_ , #queue{oldest_job = undefined}) -> [];
184 | timedout(TO, #queue{st = #st{table = Tab}} = Q) ->
185 | Now = timestamp(),
186 | Objs = find_expired(Tab, Now, TO),
187 | OJ = case ets:first(Tab) of
188 | '$end_of_table' -> undefined;
189 | {TS, _} -> TS
190 | end,
191 | {Objs, Q#queue{oldest_job = OJ}}.
192 |
193 |
194 |
195 | -spec is_empty(#queue{}) -> boolean().
196 | %%
197 | %% Check whether the queue is empty.
198 | %%
199 | is_empty(#queue{type = {producer, _}}) -> false;
200 | is_empty(#queue{oldest_job = undefined}) -> true;
201 | is_empty(#queue{}) ->
202 | false.
203 |
204 |
205 | out1(0, _Tab) -> [];
206 | out1(1, Tab) ->
207 | case ets:first(Tab) of
208 | '$end_of_table' ->
209 | [];
210 | {_TS,_Client} = Key ->
211 | ets:delete(Tab, Key),
212 | [Key]
213 | end;
214 | out1(N, Tab) when N > 0 ->
215 | %% We impose an arbitrary limit of 100 jobs fetched in one chunk.
216 | %% The main reason for capping the limit is that ets:select/3 will
217 | %% crash if N is a bignum; we probably don't want to chunk that many
218 | %% objects anyway, so we set the limit much lower.
219 | Limit = erlang:min(N, 100),
220 | case ets:select(Tab, [{{'$1'},[],['$1']}], Limit) of
221 | '$end_of_table' ->
222 | [];
223 | {Keys, _} ->
224 | [ets:delete(Tab, K) || K <- Keys],
225 | Keys
226 | end.
227 |
228 | set_oldest_job(#queue{st = #st{table = Tab}} = Q) ->
229 | OJ = case ets:first(Tab) of
230 | '$end_of_table' ->
231 | undefined;
232 | {TS,_} ->
233 | TS
234 | end,
235 | Q#queue{oldest_job = OJ}.
236 |
237 |
238 | find_expired(Tab, Now, TO) ->
239 | find_expired(ets:first(Tab), Tab, Now, TO, []).
240 |
241 | %% we return the reversed list, but I don't think that matters here.
242 | find_expired('$end_of_table', _, _, _, Acc) ->
243 | Acc;
244 | find_expired({TS, _} = Key, Tab, Now, TO, Acc) ->
245 | case is_expired(TS, Now, TO) of
246 | true ->
247 | ets:delete(Tab, Key),
248 | find_expired(ets:first(Tab), Tab, Now, TO, [Key|Acc]);
249 | false ->
250 | Acc
251 | end.
252 |
253 | empty(#queue{st = #st{table = T}} = Q) ->
254 | ets:delete_all_objects(T),
255 | Q#queue{oldest_job = undefined}.
256 |
257 |
258 | is_expired(TS, Now, TO) ->
259 | MS = Now - TS,
260 | MS > TO.
261 |
262 |
263 |
264 |
265 |
266 |
--------------------------------------------------------------------------------
/src/jobs_queue_list.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs_queue.erl
19 | %% @author : Ulf Wiger
20 | %% @end
21 | %% Description :
22 | %%
23 | %% Created : 15 Jan 2010 by Ulf Wiger
24 | %%-------------------------------------------------------------------
25 |
26 | -module(jobs_queue_list).
27 | -author('ulf.wiger@erlang-solutions.com').
28 | -copyright('Erlang Solutions Ltd.').
29 |
30 | -export([new/2,
31 | delete/1]).
32 | -export([in/3,
33 | out/2,
34 | info/2,
35 | peek/1,
36 | all/1,
37 | empty/1,
38 | is_empty/1,
39 | representation/1,
40 | timedout/1, timedout/2]).
41 |
42 | -include("jobs.hrl").
43 |
44 | %-type timestamp() :: integer().
45 | -type job() :: {pid(), reference()}.
46 | -type entry() :: {timestamp(), job()}.
47 |
48 |
49 | new(Options, Q) ->
50 | case proplists:get_value(type, Options, lifo) of
51 | lifo -> Q#queue{st = []}
52 | end.
53 |
54 | delete(#queue{}) -> true.
55 |
56 | -spec in(timestamp(), job(), #queue{}) -> #queue{}.
57 | %%
58 | %% Enqueue a job reference; return the updated queue
59 | %%
60 | in(TS, Job, #queue{st = []} = Q) ->
61 | Q#queue{st = [{TS, Job}], oldest_job = TS};
62 | in(TS, Job, #queue{st = L} = Q) ->
63 | Q#queue{st = [{TS, Job} | L]}.
64 |
65 | -spec out(N :: integer(), #queue{}) -> {[entry()], #queue{}}.
66 | %%
67 | %% Dequeue a batch of N jobs; return the modified queue.
68 | %%
69 | out(N, #queue{st = L, oldest_job = OJ} = Q) when N >= 0 ->
70 | {Out, Rest} = split(N, L),
71 | OJ1 = case Rest of
72 | [] -> undefined;
73 | _ -> OJ
74 | end,
75 | {Out, Q#queue{st = Rest, oldest_job = OJ1}}.
76 |
77 | representation(#queue { st = L, oldest_job = OJ}) ->
78 | [{oldest_job, OJ},
79 | {contents, L}].
80 |
81 | split(N, L) ->
82 | split(N, L, []).
83 |
84 | split(_, [], Acc) ->
85 | {lists:reverse(Acc), []};
86 | split(N, [H|T], Acc) when N > 0 ->
87 | split(N-1, T, [H|Acc]);
88 | split(0, T, Acc) ->
89 | {lists:reverse(Acc), T}.
90 |
91 |
92 | peek(#queue{st = []}) -> undefined;
93 | peek(#queue { st = [H | _]}) -> H.
94 |
95 |
96 | -spec all(#queue{}) -> [entry()].
97 | %%
98 | %% Return all the job entries in the queue
99 | %%
100 | all(#queue{st = L}) ->
101 | L.
102 |
103 |
104 | -type info_item() :: max_time | oldest_job | length.
105 |
106 | -spec info(info_item(), #queue{}) -> any().
107 | %%
108 | %% Return information about the queue.
109 | %%
110 | info(max_time , #queue{max_time = T} ) -> T;
111 | info(oldest_job, #queue{oldest_job = OJ}) -> OJ;
112 | info(length , #queue{st = L}) ->
113 | length(L).
114 |
115 | -spec timedout(#queue{}) -> {[entry()], #queue{}}.
116 | %%
117 | %% Return all entries that have been in the queue longer than MaxTime.
118 | %%
119 | timedout(#queue{max_time = undefined}) -> [];
120 | timedout(#queue{max_time = TO} = Q) ->
121 | timedout(TO, Q).
122 |
123 | timedout(_ , #queue{oldest_job = undefined}) -> [];
124 | timedout(TO, #queue{st = L} = Q) ->
125 | Now = jobs_server:timestamp(),
126 | {Left, Timedout} = lists:splitwith(fun({TS,_}) ->
127 | not(is_expired(TS,Now,TO))
128 | end, L),
129 | OJ = get_oldest_job(Left),
130 | {Timedout, Q#queue{oldest_job = OJ, st = Left}}.
131 |
132 | get_oldest_job([]) -> undefined;
133 | get_oldest_job(L) ->
134 | element(1, hd(lists:reverse(L))).
135 |
136 |
137 | -spec is_empty(#queue{}) -> boolean().
138 | %%
139 | %% Check whether the queue is empty.
140 | %%
141 | is_empty(#queue{st = []}) -> true;
142 | is_empty(_) ->
143 | false.
144 |
145 | empty(#queue{} = Q) ->
146 | Q#queue{oldest_job = undefined, st = []}.
147 |
148 | is_expired(TS, Now, TO) ->
149 | MS = Now - TS,
150 | MS > TO.
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/src/jobs_sampler.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs_sampler.erl
19 | %% @author : Ulf Wiger
20 | %% @end
21 | %% Description :
22 | %%
23 | %% Created : 15 Jan 2010 by Ulf Wiger
24 | %%-------------------------------------------------------------------
25 | -module(jobs_sampler).
26 |
27 | -export([start_link/0, start_link/1,
28 | trigger_sample/0,
29 | tell_sampler/2,
30 | subscribe/0,
31 | end_subscription/0,
32 | calc/3]).
33 |
34 | -export([init/1,
35 | behaviour_info/1,
36 | handle_call/3,
37 | handle_cast/2,
38 | handle_info/2,
39 | terminate/2,
40 | code_change/3]).
41 |
42 | -include("jobs.hrl").
43 |
44 | %% -record(indicators, {mnesia_dumper = 0,
45 | %% mnesia_tm = 0,
46 | %% mnesia_remote = []}).
47 |
48 |
49 | -define(SAMPLE_INTERVAL, 10000).
50 |
51 |
52 | -record(state, {modified = false,
53 | update_delay = 0,
54 | sample_interval = ?SAMPLE_INTERVAL,
55 | %% indicators = [],
56 | %% remote_indicators = [],
57 | samplers = [] :: [#sampler{}],
58 | subscribers = [],
59 | modifiers = orddict:new(),
60 | remote_modifiers = []}).
61 |
62 |
63 | behaviour_info(callbacks) ->
64 | [{init, 2},
65 | {sample, 2},
66 | {handle_msg, 3},
67 | {calc, 2}].
68 |
69 |
70 | trigger_sample() ->
71 | gen_server:cast(?MODULE, sample).
72 |
73 | start_link() ->
74 | Opts = application:get_all_env(jobs),
75 | start_link(Opts).
76 |
77 | start_link(Opts) ->
78 | gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
79 |
80 |
81 | tell_sampler(P, Msg) ->
82 | gen_server:call(?MODULE, {tell_sampler, P, timestamp(), Msg}).
83 |
84 | %% @spec subscribe() -> ok
85 | %% @doc Subscribes to feedback indicator information
86 | %%
87 | %% This function allows a process to receive the same information as the
88 | %% jobs_server any time the information changes.
89 | %%
90 | %% The notifications are delivered on the format `{jobs_indicators, Info}',
91 | %% where
92 | %%
93 | %% Info :: [{IndicatorName, LocalValue, Remote}]
94 | %% Remote :: [{NodeName, Value}]
95 | %%
96 | %%
97 | %% This information could be used e.g. to aggregate the information and generate
98 | %% new sampler information (which could be passed to a sampler plugin using
99 | %% {@link tell_sampler/2}, or to a specific queue using {@link jobs:ask_queue/2}.
100 | %%
101 | subscribe() ->
102 | gen_server:call(?MODULE, subscribe).
103 |
104 | %%
105 | end_subscription() ->
106 | gen_server:call(?MODULE, end_subscription).
107 |
108 | %% ==========================================================
109 | %% Gen_server callbacks
110 |
111 | init(Opts) ->
112 | Samplers = init_samplers(Opts),
113 | S0 = #state{samplers = Samplers},
114 | UpdateDelay = proplists:get_value(
115 | sample_update_delay, Opts, S0#state.update_delay),
116 | SampleInterval = proplists:get_value(
117 | sample_interval, Opts, S0#state.sample_interval),
118 | timer:apply_interval(SampleInterval, ?MODULE, trigger_sample, []),
119 | gen_server:abcast(nodes(), ?MODULE, {get_status, node()}),
120 | {ok, #state{samplers = Samplers,
121 | update_delay = UpdateDelay,
122 | sample_interval = SampleInterval}}.
123 |
124 |
125 | handle_call({tell_sampler, Name, TS, Msg}, _From, #state{samplers = Samplers0} = St) ->
126 | case lists:keyfind(Name, #sampler.name, Samplers0) of
127 | false ->
128 | {reply, {error, not_found}, St};
129 | #sampler{} = Sampler ->
130 | Sampler1 = one_handle_info(Msg, TS, Sampler),
131 | Samplers1 = lists:keyreplace(Name, #sampler.name, Samplers0, Sampler1),
132 | {reply, ok, St#state{samplers = Samplers1}}
133 | end;
134 | handle_call(subscribe, {Pid,_}, #state{subscribers = Subs} = St) ->
135 | MRef = erlang:monitor(process, Pid),
136 | case lists:keymember(Pid,1,Subs) of
137 | true ->
138 | {reply, ok, St};
139 | false ->
140 | Subs1 = [{Pid, MRef} | Subs],
141 | {reply, ok, St#state{subscribers = Subs1}}
142 | end;
143 | handle_call(end_subscription, {Pid,_}, #state{subscribers = Subs} = St) ->
144 | case lists:keyfind(Pid, 1, Subs) of
145 | false ->
146 | {reply, ok, St};
147 | {_, MRef} = Found ->
148 | erlang:demonitor(MRef),
149 | {reply, ok, St#state{subscribers = Subs -- [Found]}}
150 | end.
151 |
152 |
153 | handle_info({?MODULE, update}, #state{modified = IsModified} = S) ->
154 | case IsModified of
155 | true ->
156 | {noreply, report_global(
157 | report_local(S#state{modified = false}))};
158 | false ->
159 | {noreply, S}
160 | end;
161 | handle_info({'DOWN', MRef, _, _, _}, #state{subscribers = Subs} = St) ->
162 | {noreply, St#state{subscribers = lists:keydelete(MRef,2,Subs)}};
163 | handle_info(Msg, #state{samplers = Samplers0} = S) ->
164 | Samplers = map_handle_info(Msg, Samplers0),
165 | {noreply, calc_modifiers(S#state{samplers = Samplers})}.
166 |
167 |
168 | handle_cast({get_status, Node}, S) ->
169 | tell_node(Node, S),
170 | {noreply, S};
171 | handle_cast(sample, #state{samplers = Samplers0} = S) ->
172 | Samplers = collect_samples(Samplers0),
173 | {noreply, calc_modifiers(S#state{samplers = Samplers})};
174 | handle_cast({remote, Node, Modifiers}, #state{remote_modifiers = Ds} = S) ->
175 | NewDs =
176 | lists:keysort(1, ([{{K,Node},V} || {K,V} <- Modifiers]
177 | ++ [D || {{_,N},_} = D <- Ds, N =/= Node])),
178 | {noreply, report_local(S#state{remote_modifiers = NewDs})}.
179 |
180 |
181 | terminate(_, _S) ->
182 | ok.
183 |
184 |
185 | code_change(_FromVsn, State, _Extra) ->
186 | {ok, State}.
187 |
188 | %% end Gen_server callbacks
189 | %% ==========================================================
190 |
191 |
192 | init_samplers(Opts) ->
193 | Samplers = proplists:get_value(samplers, Opts, []),
194 | lists:map(
195 | fun({Name, Mod, Args}) ->
196 | {ok, ModSt} = Mod:init(Name, Args),
197 | #sampler{name = Name,
198 | mod = Mod,
199 | mod_state = ModSt}
200 | end, Samplers).
201 |
202 |
203 |
204 | group_modifiers(Local, Remote) ->
205 | RemoteRegrouped = lists:foldl(
206 | fun({{K,N},V}, D) ->
207 | orddict:append(K,{N,V},D)
208 | end, orddict:new(), Remote),
209 | [{K, V, remote_modifiers(K,RemoteRegrouped)}
210 | || {K,V} <- Local]
211 | ++
212 | [{K, 0, Vs} || {K,Vs} <- RemoteRegrouped,
213 | not lists:keymember(K, 1, Local)].
214 |
215 | remote_modifiers(K, Remote) ->
216 | case orddict:find(K, Remote) of
217 | {ok, Vs} ->
218 | Vs;
219 | error ->
220 | []
221 | end.
222 |
223 |
224 | collect_samples(Samplers) ->
225 | [one_sample(S) || S <- Samplers].
226 |
227 |
228 | one_sample(#sampler{mod = M,
229 | mod_state = ModS} = Sampler) ->
230 | Timestamp = timestamp(),
231 | try M:sample(Timestamp, ModS) of
232 | {Res, NewModS} ->
233 | add_to_history(Res, Timestamp,
234 | Sampler#sampler{mod_state = NewModS});
235 | ignore ->
236 | Sampler
237 | catch
238 | error:Err ->
239 | sampler_error(Err, Sampler)
240 | end.
241 |
242 |
243 | map_handle_info(Msg, Samplers) ->
244 | Timestamp = timestamp(),
245 | [one_handle_info(Msg, Timestamp, S) || S <- Samplers].
246 |
247 | one_handle_info(Msg, TS, #sampler{mod = M, mod_state = ModS} = Sampler) ->
248 | try M:handle_msg(Msg, TS, ModS) of
249 | {ignore, ModS1} ->
250 | Sampler#sampler{mod_state = ModS1};
251 | {log, Sample, ModS1} ->
252 | add_to_history(Sample, TS, Sampler#sampler{mod_state = ModS1})
253 | catch
254 | error:Err ->
255 | sampler_error(Err, Sampler)
256 | end.
257 |
258 |
259 | add_to_history(Result, Timestamp, #sampler{hist_length = Len,
260 | history = History} = S) ->
261 | Item = {Timestamp, Result},
262 | NewHistory =
263 | case queue:len(History) of
264 | HL when HL >= Len ->
265 | queue:in(Item, queue:drop(History));
266 | _ ->
267 | queue:in(Item, History)
268 | end,
269 | S#sampler{history = NewHistory}.
270 |
271 |
272 | sampler_error(Err, Sampler) ->
273 | error_logger:error_report([{?MODULE, sampler_error},
274 | {error, Err},
275 | {sampler, Sampler}]),
276 | % For now, don't modify the sampler (disable it...?)
277 | Sampler.
278 |
279 |
280 | report_local(#state{modifiers = Local, remote_modifiers = Remote,
281 | subscribers = Subs} = S) ->
282 | Grouped = group_modifiers(Local, Remote),
283 | jobs_server:set_modifiers(Grouped),
284 | [Pid ! {jobs_indicators, Grouped} || {Pid,_} <- Subs],
285 | S.
286 |
287 | report_global(#state{modifiers = Local} = S) ->
288 | gen_server:abcast(nodes(), ?MODULE, {remote, node(), Local}),
289 | S.
290 |
291 |
292 |
293 | calc_modifiers(#state{samplers = Samplers} = S) ->
294 | S1 = calc_modifiers(Samplers, S),
295 | case S1#state.modified of
296 | true ->
297 | erlang:send_after(S#state.update_delay, self(), {?MODULE,update});
298 | false ->
299 | skip
300 | end,
301 | S1.
302 |
303 |
304 | calc_modifiers(Samplers, #state{modifiers = Modifiers0} = S) ->
305 | {Samplers1, {Modifiers1, IsModified}} =
306 | lists:mapfoldl(
307 | fun(#sampler{mod = M,
308 | mod_state = ModS,
309 | history = History} = Sx, {Acc,Flg}) ->
310 | try M:calc(History, ModS) of
311 | {NewModifiers, NewModSt} ->
312 | {Sx#sampler{mod_state = NewModSt},
313 | {merge_modifiers(orddict:from_list(NewModifiers), Acc), true}};
314 | false ->
315 | {Sx, {Acc, Flg}}
316 | catch
317 | error:Err ->
318 | sampler_error(Err, Sx),
319 | {Sx, {Acc,Flg}}
320 | end
321 | end, {Modifiers0, false}, Samplers),
322 | S#state{samplers = Samplers1, modifiers = Modifiers1, modified = IsModified}.
323 |
324 |
325 | merge_modifiers(New, Modifiers) ->
326 | orddict:merge(
327 | fun(_, V1, V2) -> erlang:max(V1, V2) end, New, Modifiers).
328 |
329 |
330 | tell_node(Node, S) ->
331 | gen_server:cast({?MODULE, Node}, {remote, node(), S#state.modifiers}).
332 |
333 |
334 | %% example: type = time , step = {seconds, [{0,1},{30,2},{45,3},{50,4}]}
335 | %% type = value, step = [{80,1},{85,2},{90,3},{95,4},{100,5}]
336 |
337 | calc(Type, Template, History) ->
338 | case queue:is_empty(History) of
339 | true -> 0;
340 | false -> calc1(Type, Template, History)
341 | end.
342 |
343 | calc1(time, Template, History) ->
344 | Now = timestamp(),
345 | {Unit, Steps} = case Template of
346 | T when is_list(T) ->
347 | {msec, T};
348 | {U, T} ->
349 | U1 = if
350 | U==sec; U==seconds -> sec;
351 | U==ms; msec -> msec
352 | end,
353 | {U1, T}
354 | end,
355 | case true_since(History) of
356 | 0 -> 0;
357 | Since ->
358 | %% timestamps are in milliseconds
359 | Time = case Unit of
360 | sec -> (Now - Since) div 1000;
361 | msec -> Now - Since
362 | end,
363 | pick_step(Time, Steps)
364 | end;
365 | calc1(value, Template, History) ->
366 | {value, {_, Level}} = queue:peek_r(History),
367 | pick_step(Level, Template).
368 |
369 | true_since(Q) ->
370 | true_since(queue:out_r(Q), 0).
371 |
372 | true_since({{value,{_,false}},_}, Since) ->
373 | Since;
374 | true_since({empty, _}, Since) ->
375 | Since;
376 | true_since({{value,{T,true}},Q1}, _) ->
377 | true_since(queue:out_r(Q1), T).
378 |
379 |
380 | pick_step(Level, Ls) ->
381 | take_last(fun({L,_}) ->
382 | Level >= L
383 | end, Ls, 0).
384 |
385 | take_last(F, [{_,V} = H|T], Last) ->
386 | case F(H) of
387 | true -> take_last(F, T, V);
388 | false -> Last
389 | end;
390 | take_last(_, [], Last) ->
391 | Last.
392 |
393 |
394 | %% millisecond timestamp, never wraps
395 | timestamp() ->
396 | jobs_server:timestamp() div 1000.
397 |
--------------------------------------------------------------------------------
/src/jobs_sampler_cpu.erl:
--------------------------------------------------------------------------------
1 | %% -*- erlang-indent-level: 4; indent-tabs-mode: nil -*-
2 | %%==============================================================================
3 | %% Copyright 2010 Erlang Solutions Ltd.
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 | %%==============================================================================
17 |
18 | %%-------------------------------------------------------------------
19 | %% File : jobs_sampler_cpu.erl
20 | %% @author : Ulf Wiger
21 | %% @end
22 | %% Description :
23 | %%
24 | %% Created : 15 Jan 2010 by Ulf Wiger
25 | %%-------------------------------------------------------------------
26 | -module(jobs_sampler_cpu).
27 | -behaviour(jobs_sampler).
28 |
29 | -export([init/2,
30 | sample/2,
31 | handle_msg/3,
32 | calc/2]).
33 |
34 | -record(st, {levels = []}).
35 |
36 | default_levels() -> [{80,1},{90,2},{100,3}].
37 |
38 |
39 | init(_Name, Opts) ->
40 | cpu_sup:util([per_cpu]), % first return value is rubbish, per the docs
41 | Levels = proplists:get_value(levels, Opts, default_levels()),
42 | {ok, #st{levels = Levels}}.
43 |
44 | handle_msg(_Msg, _Timestamp, ModS) ->
45 | {ignore, ModS}.
46 |
47 | sample(_Timestamp, #st{} = S) ->
48 | Result =
49 | case cpu_sup:util([per_cpu]) of
50 | Info when is_list(Info) ->
51 | Utils = [U || {_,U,_,_} <- Info],
52 | case Utils of
53 | [U] ->
54 | %% only one cpu
55 | U;
56 | [_,_|_] ->
57 | %% This is a form of ad-hoc averaging, which tries to
58 | %% account for the possibility that the application
59 | %% loads the cores unevenly.
60 | calc_avg_util(Utils)
61 | end;
62 | _ ->
63 | undefined
64 | end,
65 | {Result, S}.
66 |
67 | calc_avg_util(Utils) ->
68 | case minmax(Utils) of
69 | {A,B} when B-A > 50 ->
70 | %% very uneven load
71 | High = [U || U <- Utils,
72 | B-U > 20],
73 | lists:sum(High)/length(High);
74 | {Low,High} ->
75 | (High+Low)/2
76 | end.
77 |
78 |
79 | minmax([H|T]) ->
80 | lists:foldl(
81 | fun(X, {Min,Max}) ->
82 | {erlang:min(X,Min), erlang:max(X,Max)}
83 | end, {H,H}, T).
84 |
85 |
86 | calc(History, #st{levels = Levels} = St) ->
87 | L = jobs_sampler:calc(value, Levels, History),
88 | {[{cpu,L}], St}.
89 |
--------------------------------------------------------------------------------
/src/jobs_sampler_history.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | -module(jobs_sampler_history).
18 | -export([new/1,
19 | add/2,
20 | to_list/1,
21 | from_list/2,
22 | take_last/2]).
23 |
24 | -record(jsh, {max_length,
25 | length = 0,
26 | history = queue:new()}).
27 |
28 | new(Length) ->
29 | #jsh{max_length = Length}.
30 |
31 |
32 | add(Entry, #jsh{length = L,
33 | max_length = L} = R) ->
34 | In = {timestamp(), Entry},
35 | do_add(In, drop(R));
36 | add(Entry, #jsh{} = R) ->
37 | do_add({timestamp(), Entry}, R).
38 |
39 |
40 | do_add(Item, #jsh{length = L, history = H} = R) ->
41 | R#jsh{length = L+1, history = queue:in(Item, H)}.
42 |
43 | drop(#jsh{length = 0} = R) ->
44 | R;
45 | drop(#jsh{length = L, history = H} = R) ->
46 | R#jsh{length = L-1, history = queue:drop(H)}.
47 |
48 |
49 | from_list(MaxL, L0) ->
50 | {Length, L} = case length(L0) of
51 | Len when Len > MaxL ->
52 | {MaxL, lists:sublist(L0, MaxL)};
53 | Len ->
54 | {Len, L0}
55 | end,
56 | #jsh{max_length = MaxL,
57 | length = Length,
58 | history = queue:from_list(L)}.
59 |
60 | to_list(#jsh{history = Q}) ->
61 | queue:to_list(Q).
62 |
63 |
64 | take_last(F, #jsh{history = Q}) ->
65 | take_last(F, queue:to_list(Q), []).
66 |
67 | take_last(F, [H|T], Last) ->
68 | case F(H) of
69 | true -> take_last(F, T, H);
70 | false -> Last
71 | end;
72 | take_last(_, [], Last) ->
73 | Last.
74 |
75 |
76 |
77 | %% Millisecond timestamp, never wraps
78 | timestamp() ->
79 | jobs_server:timestamp() div 1000.
80 |
--------------------------------------------------------------------------------
/src/jobs_sampler_mnesia.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | -module(jobs_sampler_mnesia).
18 |
19 | -behaviour(jobs_sampler).
20 |
21 | -export([init/2,
22 | sample/2,
23 | handle_msg/3,
24 | calc/2]).
25 |
26 | -record(st, {levels = [],
27 | subscriber}).
28 |
29 |
30 |
31 | init(Name, Opts) ->
32 | Pid = spawn_link(fun() ->
33 | mnesia:subscribe(system),
34 | subscriber_loop(Name)
35 | end),
36 | Levels = proplists:get_value(levels, Opts, default_levels()),
37 | {ok, #st{levels = Levels, subscriber = Pid}}.
38 |
39 | default_levels() ->
40 | {seconds, [{0,1}, {30,2}, {45,3}, {60,4}]}.
41 |
42 | handle_msg({mnesia_system_event, {mnesia,{dump_log,_}}}, _T, S) ->
43 | {log, true, S};
44 | handle_msg({mnesia_system_event, {mnesia_tm, message_queue_len, _}}, _T, S) ->
45 | {log, true, S};
46 | handle_msg(_, _T, S) ->
47 | {ignore, S}.
48 |
49 | sample(_T, S) ->
50 | {is_overload(), S}.
51 |
52 | calc(History, #st{levels = Levels} = S) ->
53 | {[{mnesia,jobs_sampler:calc(time, Levels, History)}], S}.
54 |
55 |
56 | subscriber_loop(Name) ->
57 | receive
58 | Msg ->
59 | case jobs_sampler:tell_sampler(Name, Msg) of
60 | ok ->
61 | subscriber_loop(Name);
62 | {error, _} ->
63 | %% sampler likely removed
64 | exit(normal)
65 | end
66 | end.
67 |
68 |
69 | is_overload() ->
70 | %% e.g: [{mnesia_tm,true},{mnesia_dump_log,false}]
71 | lists:keymember(true, 2, mnesia_overload_read()).
72 |
73 | mnesia_overload_read() ->
74 | %% This function is not present in mnesia versions older than R14A
75 | case erlang:function_exported(mnesia_lib,overload_read,0) of
76 | false ->
77 | [];
78 | true ->
79 | mnesia_lib:overload_read()
80 | end.
81 |
--------------------------------------------------------------------------------
/src/jobs_server.erl:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2010 Erlang Solutions Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | %%-------------------------------------------------------------------
18 | %% File : jobs_server.erl
19 | %% @author : Ulf Wiger
20 | %% @end
21 | %% Description :
22 | %%
23 | %% Created : 15 Jan 2010 by Ulf Wiger
24 | %%-------------------------------------------------------------------
25 | -module(jobs_server).
26 | -behaviour(gen_server).
27 |
28 | -export([ask/0,
29 | ask/1,
30 | run/1, run/2,
31 | done/1,
32 | enqueue/2,
33 | dequeue/2]).
34 |
35 | -export([set_modifiers/1]).
36 |
37 | -export([ask_queue/2]).
38 |
39 | %% Config API
40 | -export([add_queue/2,
41 | delete_queue/1,
42 | add_counter/2,
43 | modify_counter/2,
44 | delete_counter/1,
45 | add_group_rate/2,
46 | modify_group_rate/2,
47 | delete_group_rate/1,
48 | info/1,
49 | queue_info/1,
50 | queue_info/2,
51 | modify_regulator/4]).
52 |
53 |
54 | -export([timestamp/0,
55 | timestamp_to_datetime/1]).
56 |
57 | -export([start_link/0,
58 | start_link/1]).
59 | -export([init/1,
60 | handle_call/3,
61 | handle_cast/2,
62 | handle_info/2,
63 | terminate/2,
64 | code_change/3]).
65 |
66 |
67 | -import(proplists, [get_value/3]).
68 |
69 | -include("jobs.hrl").
70 | -record(st, {queues = [] :: [#queue{}],
71 | group_rates = [] :: [#grp{}],
72 | counters = [] :: [#cr{}],
73 | monitors,
74 | q_select :: atom(),
75 | q_select_st :: any(),
76 | default_queue,
77 | info_f}).
78 |
79 |
80 | -define(SERVER, ?MODULE).
81 |
82 | -type queue_name() :: any().
83 | -type info_category() :: queues | group_rates | counters.
84 |
85 | -spec ask() -> {ok, any()} | {error, rejected | timeout}.
86 | %%
87 | ask() ->
88 | ask(default).
89 |
90 |
91 | -spec ask(job_class()) -> {ok, reg_obj()} | {error, rejected | timeout}.
92 | %%
93 | ask(Type) ->
94 | call(?SERVER, {ask, Type}, infinity).
95 |
96 | -spec run(fun(() -> X)) -> X.
97 | %%
98 | run(Fun) when is_function(Fun, 0) ->
99 | run(undefined, Fun).
100 |
101 | -spec run(job_class(), fun(() -> X)) -> X.
102 | %%
103 | run(Type, Fun) when is_function(Fun, 0) ->
104 | case ask(Type) of
105 | {ok, Opaque} ->
106 | try Fun()
107 | after
108 | done(Opaque)
109 | end;
110 | {error,Reason} ->
111 | erlang:error(Reason)
112 | end;
113 | run(Type, Fun) when is_function(Fun, 1) ->
114 | case ask(Type) of
115 | {ok, Opaque} ->
116 | try Fun(Opaque)
117 | after
118 | done(Opaque)
119 | end;
120 | {error,Reason} ->
121 | erlang:error(Reason)
122 | end.
123 |
124 | -spec done(reg_obj()) -> ok.
125 | %%
126 | done({undefined, _}) ->
127 | %% no monitoring on this job (e.g. from a {action, approve} queue)
128 | ok;
129 | done(Opaque) ->
130 | gen_server:cast(?MODULE, {done, self(), del_info(Opaque)}).
131 |
132 | del_info({Ref, L}) ->
133 | {Ref, lists:keydelete(info, 1, L)}.
134 |
135 |
136 | -spec enqueue(job_class(), any()) -> ok.
137 | %%
138 | enqueue(Type, Item) ->
139 | call(?SERVER, {enqueue, Type, Item}, infinity).
140 |
141 | -spec dequeue(job_class(), integer() | infinity) -> [{timestamp(), any()}].
142 | %%
143 | dequeue(Type, N) when N==infinity; is_integer(N), N > 0 ->
144 | call(?SERVER, {dequeue, Type, N}, infinity).
145 |
146 |
147 | -spec add_queue(queue_name(), [option()]) -> ok.
148 | %%
149 | add_queue(Name, Options) ->
150 | call(?SERVER, {add_queue, Name, Options}).
151 |
152 | -spec delete_queue(queue_name()) -> ok.
153 | delete_queue(Name) ->
154 | call(?SERVER, {delete_queue, Name}).
155 |
156 |
157 | %% @spec ask_queue(QName, Request) -> Reply
158 | %% @doc Invoke the Q:handle_call/3 function (if it exists).
159 | %%
160 | %% Send a request to a specific queue in the JOBS server.
161 | %% Each queue has its own local state, allowing it to collect special statistics.
162 | %% This function allows a client to send a request that is handled by a specific
163 | %% queue instance, either to pull information from the queue, or to influence its
164 | %% state.
165 | %% @end
166 | %%
167 | -spec ask_queue(queue_name(), any()) -> any().
168 | ask_queue(QName, Request) ->
169 | call(?SERVER, {ask_queue, QName, Request}).
170 |
171 | -spec info(info_category()) -> [any()].
172 | info(Item) ->
173 | call(?SERVER, {info, Item}).
174 |
175 | queue_info(Name) ->
176 | call(?SERVER, {queue_info, Name}).
177 |
178 | queue_info(Name, Item) ->
179 | call(?SERVER, {queue_info, Name, Item}).
180 |
181 | add_group_rate(Name, Options) ->
182 | call(?SERVER, {add_group_rate, Name, Options}).
183 |
184 | modify_group_rate(Name, Opts) ->
185 | call(?SERVER, {modify_group_rate, Name, Opts}).
186 |
187 | delete_group_rate(Name) ->
188 | call(?SERVER, {delete_group_rate, Name}).
189 |
190 | modify_regulator(Type, QName, RegName, Opts) ->
191 | call(?SERVER, {modify_regulator, Type, QName, RegName, Opts}).
192 |
193 | add_counter(Name, Options) ->
194 | call(?SERVER, {add_counter, Name, Options}).
195 |
196 | modify_counter(Name, Opts) ->
197 | call(?SERVER, {modify_counter, Name, Opts}).
198 |
199 | delete_counter(Name) ->
200 | call(?SERVER, {delete_counter, Name}).
201 |
202 |
203 |
204 | %% Sampler API
205 | %%
206 | set_modifiers(Modifiers) ->
207 | call(?SERVER, {set_modifiers, Modifiers}).
208 |
209 |
210 | %% Client-side call function
211 | %%
212 | call(Server, Req) ->
213 | call(Server, Req, infinity).
214 |
215 | call(Server, Req, Timeout) ->
216 | case gen_server:call(Server, Req, Timeout) of
217 | badarg ->
218 | erlang:error(badarg);
219 | {badarg, Reason} ->
220 | erlang:error(Reason);
221 | Res ->
222 | Res
223 | end.
224 |
225 |
226 | %% Reply functions called by the server
227 |
228 | %% approve(From) ->
229 | %% approve(From, []).
230 |
231 | approve(From, Counters) ->
232 | gen_server:reply(From, {ok, Counters}).
233 |
234 |
235 | reject(From) ->
236 | gen_server:reply(From, {error, rejected}).
237 |
238 | timeout({_, {Pid,Ref} = From}) when is_pid(Pid), is_reference(Ref) ->
239 | gen_server:reply(From, {error, timeout}).
240 |
241 |
242 | %% start function
243 |
244 | -spec start_link() -> {ok, pid()}.
245 | %%
246 | start_link() ->
247 | start_link(options()).
248 |
249 | -spec start_link([option()]) -> {ok, pid()}.
250 | %%
251 | start_link(Opts0) when is_list(Opts0) ->
252 | Opts = expand_opts(sort_groups(group_opts(Opts0))),
253 | gen_server:start_link({local,?MODULE}, ?MODULE, Opts, []).
254 |
255 | %% Server-side callbacks and helpers
256 |
257 | -spec options() -> [{_Ctxt :: env | opts, _App:: user | atom(), [option()]}].
258 | options() ->
259 | JobsOpts = {env, jobs, application:get_all_env(jobs)},
260 | Other = try [{env, A, Os} || {A, Os} <- setup:find_env_vars(jobs)]
261 | catch
262 | error:undef -> []
263 | end,
264 | [JobsOpts | Other].
265 |
266 | group_opts([{_,_,_} = H|T]) ->
267 | [H|group_opts(T)];
268 | group_opts([{_,_}|_] = Opts) ->
269 | {U, Rest} = lists:splitwith(fun(X) -> size(X) == 2 end, Opts),
270 | [{opts, user, U}|group_opts(Rest)];
271 | group_opts([]) ->
272 | [].
273 |
274 | sort_groups(Gs) ->
275 | %% How to give priority to certain 'global' options?
276 | %% 1. Options specified explicitly in Options
277 | %% 2. Options specified as 'jobs' environment variables
278 | %% (there are none by default)
279 | %% 3. Options in other apps, as returned by 'setup:find_env_vars/1'
280 | %%
281 | %% Note that, as of now, we 'lift' all user options to the fore, even if
282 | %% they appear mixed in the argument given to jobs_server:start_link/1.
283 | %%
284 | User = [U || {opts, user, _} = U <- Gs],
285 | [Jobs] = [J || {env, jobs, _} = J <- Gs],
286 | User ++ [Jobs | Gs -- [Jobs | User]].
287 |
288 | -spec expand_opts([option()]) -> [option()].
289 | %%
290 | expand_opts([{Ctxt, A, Opts}|T]) ->
291 | Exp = try expand_opts_(Opts)
292 | catch
293 | throw:{{error,R}, Arg} ->
294 | error(R, [{context, Ctxt}, {app, A} | Arg])
295 | end,
296 | [{Ctxt, A, Exp}|expand_opts(T)];
297 | expand_opts([]) ->
298 | [].
299 |
300 | expand_opts_([{config, F}|Opts]) ->
301 | case file:script(F) of
302 | {ok, NewOpts} ->
303 | NewOpts ++ expand_opts_(Opts);
304 | {error, _} = E ->
305 | throw({E, [{config,F}]})
306 | end;
307 | expand_opts_([H|T]) ->
308 | [H|expand_opts_(T)];
309 | expand_opts_([]) ->
310 | [].
311 |
312 |
313 | standard_rate(R) when is_integer(R), R >= 0 ->
314 | [{regulators,
315 | [{rate,
316 | [{limit, R},
317 | {modifiers, standard_modifiers()}]
318 | }]
319 | }].
320 |
321 | standard_counter(N) when is_integer(N), N >= 0 ->
322 | [{regulators,
323 | [{counter,
324 | [{limit, N},
325 | {modifiers, standard_modifiers()}]
326 | }]
327 | }].
328 |
329 | -spec standard_modifiers() -> [q_modifier(),...].
330 | %%
331 | standard_modifiers() ->
332 | [{cpu, 10},
333 | {memory, 10}].
334 |
335 | -spec init([option()]) -> {ok, #st{}}.
336 | %%
337 | init(Opts) ->
338 | process_flag(priority,high),
339 | S0 = #st{},
340 | [Qs, Gs, Cs] =
341 | [get_all_values(K,Opts,Def)
342 | || {K,Def} <- [{queues , [{default,[]}]},
343 | {group_rates, []},
344 | {counters , []}]],
345 | Groups = init_groups(Gs),
346 | Counters = init_counters(Cs),
347 | S1 = S0#st{group_rates = Groups,
348 | counters = Counters},
349 | Queues = init_queues(Qs, S1),
350 | Default0 = case Queues of
351 | [] ->
352 | undefined;
353 | [#queue{name = N}|_] ->
354 | N
355 | end,
356 | Default = get_first_value(default_queue, Opts, Default0),
357 | {ok, set_info(
358 | kick_producers(
359 | lift_counters(S1#st{queues = Queues,
360 | default_queue = Default,
361 | monitors = ets:new(monitors,[set])})))}.
362 |
363 |
364 |
365 | init_queues(Qs, S) ->
366 | lists:map(fun(Q) ->
367 | init_queue(Q,S)
368 | end, Qs).
369 |
370 | init_queue({Name, standard_rate, R}, S) when is_integer(R), R > 0 ->
371 | init_queue({Name, standard_rate(R)}, S);
372 | init_queue({Name, standard_counter, N}, S) when is_integer(N), N > 0 ->
373 | init_queue({Name, standard_counter(N)}, S);
374 | init_queue({Name, passive, Opts}, S) ->
375 | init_queue({Name, [{type, {passive, fifo}} | Opts]}, S);
376 | init_queue({Name, Action}, _S) when Action==approve; Action==reject ->
377 | #queue{name = Name, type = {action, Action}};
378 | init_queue({Name, producer, F, Opts}, S) ->
379 | init_queue({Name, [{type, {producer, F}} | Opts]}, S);
380 | init_queue({Name, Opts0}, S) when is_list(Opts0) ->
381 | %% Allow the regulators to be named at the top-level.
382 | %% This makes it possible to write {q, [{counter, [{limit,1}]}]},
383 | %% instead of {q, [{regulators, [{counter, [{limit,1}]}]}]}.
384 | {Regs0, Opts} = lists:foldr(
385 | fun(X, {R,O}) when is_tuple(X) ->
386 | case lists:member(
387 | element(1,X), [counter, rate,
388 | named_counter,
389 | group_rate]) of
390 | true -> {[X|R], O};
391 | false -> {R, [X|O]}
392 | end;
393 | (X, {R, O}) -> {R, [X|O]}
394 | end, {[], []}, normalize_options(Opts0)),
395 | [ChkI, Regs] =
396 | [get_value(K,Opts,D) ||
397 | {K, D} <- [{check_interval,undefined},
398 | {regulators, Regs0}]],
399 | Q0 = q_new([{name,Name}|Opts]),
400 | Q1 = init_regulators(Regs, Q0#queue{check_interval = ChkI}),
401 | calculate_check_interval(Q1, S).
402 |
403 | normalize_options(Opts) ->
404 | merge_regulators(
405 | lists:flatmap(
406 | fun({standard_rate, R}) when is_integer(R), R >= 0 ->
407 | standard_rate(R);
408 | ({standard_counter, C}) when is_integer(C), C >= 0 ->
409 | standard_counter(C);
410 | ({producer, F}) ->
411 | [{type, {producer, F}}];
412 | (passive) ->
413 | [{type, {passive, fifo}}];
414 | (A) when A==approve; A==reject ->
415 | [{type, {action, approve}}];
416 | ({action, A}) when A==approve; A==reject ->
417 | [{type, {action, approve}}];
418 | ({K, V}) ->
419 | [{K, V}]
420 | end, Opts)).
421 |
422 | merge_regulators(Opts) ->
423 | merge_regulators(lists:keytake(regulators, 1, Opts), Opts, []).
424 |
425 | merge_regulators(false, Opts, []) ->
426 | Opts;
427 | merge_regulators(false, Opts, Regs) ->
428 | [{regulators, Regs}|Opts];
429 | merge_regulators({value, {regulators, Rs}, Opts}, _, Acc) ->
430 | merge_regulators(lists:keytake(regulators, 1, Opts), Opts,
431 | Acc ++ Rs).
432 |
433 | calculate_check_interval(#queue{regulators = Rs,
434 | check_interval = undefined} = Q, S) ->
435 | Regs = expand_regulators(Rs, S),
436 | I = lists:foldl(
437 | fun(R, Acc) ->
438 | Rate = get_rate(R),
439 | erlang:min(Rate#rate.interval, Acc)
440 | end, infinity, Regs),
441 | Q#queue{check_interval = I};
442 | calculate_check_interval(Q, _) ->
443 | Q.
444 |
445 | init_groups(Gs) ->
446 | lists:map(
447 | fun({Name, Opts}) ->
448 | init_group(Opts, #grp{name = Name})
449 | end, Gs).
450 |
451 | init_group(Opts, G) ->
452 | R = #rate{},
453 | Limit = get_value(limit, Opts, R#rate.limit),
454 | Modifiers = get_value(modifiers, Opts, R#rate.modifiers),
455 | Interval = interval(Limit),
456 | G#grp{rate = #rate{limit = Limit,
457 | preset_limit = Limit,
458 | modifiers = Modifiers,
459 | interval = Interval}}.
460 |
461 | init_counters(Cs) ->
462 | lists:map(
463 | fun({Name, Opts}) ->
464 | init_counter([{name, Name}|Opts], #cr{name = Name,
465 | shared = true})
466 | end, Cs).
467 |
468 | init_regulators(Rs, #queue{name = Qname} = Q) ->
469 | {Rs1,_} = lists:mapfoldl(
470 | fun({rate, Opts}, {Rx,Cx}) ->
471 | Name0 = {rate,Qname,Rx},
472 | R0 = #rate{},
473 | RR0 = #rr{name = Name0, rate = R0},
474 | {init_rate_regulator(Opts, RR0), {Rx+1, Cx}};
475 | ({counter, Opts}, {Rx,Cx}) when is_list(Opts) ->
476 | Name0 = {counter,Qname,Cx},
477 | Name = get_value(name, Opts, Name0),
478 | {init_counter(Opts, #cr{name = Name,
479 | owner = Qname}), {Rx,Cx+1}};
480 | ({named_counter, Name, Incr}, {Rx,Cx}) when is_integer(Incr) ->
481 | {#counter{name = Name, increment = Incr}, {Rx,Cx+1}};
482 | ({group_rate, R} = Link, Acc) when is_atom(R) ->
483 | {Link, Acc}
484 | %% ({counter, R} = Link, Acc) when is_atom(R) ->
485 | %% {Link, Acc}
486 | end, {1,1}, Rs),
487 | case [RR || #rr{} = RR <- Rs1] of
488 | [_,_|_] = Multiples ->
489 | erlang:error(only_one_rate_regulator_allowed, Multiples);
490 | _ ->
491 | ok
492 | end,
493 | Q#queue{regulators = Rs1}.
494 |
495 | init_rate_regulator(Opts, #rr{rate = R} = RR) ->
496 | [Limit,Modifiers,Name] =
497 | [get_value(K,Opts,D) ||
498 | {K,D} <- [{limit , R#rate.limit},
499 | {modifiers, R#rate.modifiers},
500 | {name , RR#rr.name}]],
501 | Interval = interval(Limit),
502 | RR#rr{name = Name,
503 | rate = #rate{limit = Limit,
504 | interval = Interval,
505 | preset_limit = Limit,
506 | modifiers = Modifiers}}.
507 |
508 |
509 | %% init_counter(Opts) ->
510 | %% init_counter(Opts, #cr{}).
511 |
512 | init_counter(Opts, #cr{} = CR0) ->
513 | R = #rate{},
514 | Limit = get_value(limit, Opts, R#rate.limit),
515 | Interval = get_value(interval, Opts, ?COUNTER_SAMPLE_INTERVAL),
516 | Increment = get_value(increment, Opts, 1),
517 | Modifiers = get_value(modifiers, Opts, R#rate.modifiers),
518 | CR0#cr{rate = #rate{limit = Limit,
519 | interval = Interval,
520 | preset_limit = Limit,
521 | modifiers = Modifiers},
522 | increment = Increment}.
523 |
524 | lift_counters(#st{queues = Qs} = S) ->
525 | lists:foldl(fun do_lift_counters/2, S, Qs).
526 |
527 | do_lift_counters(#queue{name = Name, regulators = Rs} = Q,
528 | #st{queues = Queues, counters = Counters} = S0) ->
529 | Aliases = [A || #counter{name = A} <- Rs],
530 | S = lift_aliases(Aliases, Name, S0),
531 | case [R || #cr{} = R <- Rs] of
532 | [] ->
533 | S;
534 | [_|_] = CRs ->
535 | {Rs1, Cs1} = lists:foldl(fun(CR,Acc) ->
536 | mk_counter_alias(CR,Acc,Name)
537 | end, {Rs, Counters}, CRs),
538 | Q1 = Q#queue{regulators = Rs1},
539 | Queues1 = lists:keyreplace(Name, #queue.name, Queues, Q1),
540 | S#st{queues = Queues1, counters = Cs1}
541 | end.
542 |
543 | lift_aliases([], _, S) ->
544 | S;
545 | lift_aliases(Aliases, QName, #st{counters = Cs} = S) ->
546 | Cs1 = lists:foldl(
547 | fun(Alias, Csx) ->
548 | case lists:keyfind(Alias, #cr.name, Csx) of
549 | false ->
550 | %% aliased counter doesn't exist...
551 | %% We should probably issue a warning.
552 | Csx;
553 | #cr{queues = Queues} = Existing ->
554 | Qs = [QName|Queues -- [QName]],
555 | lists:keyreplace(Alias, #cr.name, Csx,
556 | Existing#cr{queues = Qs})
557 | end
558 | end, Cs, Aliases),
559 | S#st{counters = Cs1}.
560 |
561 | mk_counter_alias(#cr{name = Name,
562 | increment = I} = CR, {Rs, Cs}, QName) ->
563 | Alias = #counter{name = Name, increment = I},
564 | Rs1 = lists_replace(CR, Rs, Alias),
565 | case lists:keyfind(Name, #cr.name, Cs) of
566 | false ->
567 | {Rs1, [CR#cr{queues = [QName]}|Cs]};
568 | #cr{queues = Queues} = Existing ->
569 | Qs = [QName|Queues -- [QName]],
570 | {Rs1, lists:keyreplace(Name, #cr.name, Cs,
571 | Existing#cr{queues = Qs})}
572 | end.
573 |
574 | pickup_aliases(Queues, #cr{name = N} = CR) ->
575 | QNames = lists:foldl(
576 | fun(#queue{name = QName, regulators = Rs}, Acc) ->
577 | case [1 || #counter{name = Nx} <- Rs, Nx =:= N] of
578 | [] ->
579 | Acc;
580 | [_|_] ->
581 | [QName | Acc]
582 | end
583 | end, [], Queues),
584 | CR#cr{queues = QNames}.
585 |
586 | lists_replace(X, [X | T], With) -> [With | T];
587 | lists_replace(X, [A | T], With) -> [A | lists_replace(X, T, With)].
588 |
589 |
590 | -spec interval(integer()) -> undefined | float().
591 | %%
592 | %% Return the sampling interval (in ms) based on max frequency.
593 | %% If max frequency is 0, we choose what corresponds to an unlimited interval
594 | %%
595 | interval(0 ) -> undefined; % greater than any int()
596 | interval(Limit) ->
597 | 1000 / Limit.
598 |
599 | set_info(S) ->
600 | %% We need to clear the info_f attribute, to
601 | %% avoid recursively inheriting all previous states.
602 | S1 = S#st{info_f = undefined},
603 | S1#st{info_f = fun(Q) ->
604 | get_info(Q, S1)
605 | end}.
606 |
607 |
608 | %% Gen_server callbacks
609 | handle_call(Req, From, S) ->
610 | try i_handle_call(Req, From, S) of
611 | {noreply, #st{} = S1} ->
612 | {noreply, set_info(S1)};
613 | {reply, R, #st{} = S1} ->
614 | {reply, R, set_info(S1)}
615 | catch
616 | error:Reason ->
617 | io:fwrite("caught Reason = ~p~n", [{Reason, erlang:get_stacktrace()}]),
618 | error_report([{error, Reason},
619 | {request, Req},
620 | {stacktrace, erlang:get_stacktrace()}]),
621 | {reply, badarg, S}
622 | end.
623 |
624 | i_handle_call({ask, Type}, From, #st{queues = Qs} = S) ->
625 | TS = timestamp(),
626 | {Qname, S1} = select_queue(Type, TS, S),
627 | case get_queue(Qname, Qs) of
628 | #queue{type = #action{a = approve}, stateful = undefined,
629 | approved = Approved} = Q ->
630 | S2 = S1#st{queues = lists:keyreplace(
631 | Qname, #queue.name, Qs,
632 | Q#queue{approved = Approved + 1})},
633 | approve(From, {undefined, []}), {noreply, S2};
634 | #queue{type = #action{a = approve}, stateful = Stf,
635 | approved = Approved} = Q ->
636 | {Val, Stf1} = update_stateful(Stf, [], S1#st.info_f),
637 | S2 = S1#st{queues = lists:keyreplace(
638 | Qname, #queue.name, Qs,
639 | Q#queue{stateful = Stf1,
640 | approved = Approved + 1})},
641 | approve(From, {undefined, [{info, Val}]}), {noreply, S2};
642 | #queue{type = #action{a = reject}} ->
643 | reject(From), {noreply, S1};
644 | #queue{type = #producer{}} ->
645 | {reply, badarg, S1};
646 | #queue{} = Q ->
647 | {Q2, S2} = queue_job(TS, From, Q, S1),
648 | {noreply, update_queue(Q2, S2)};
649 | false ->
650 | {reply, badarg, S1}
651 | end;
652 | i_handle_call({enqueue, Type, Item}, _From, #st{queues = Qs} = S) ->
653 | TS = timestamp(),
654 | {Qname, S1} = select_queue(Type, TS, S),
655 | case get_queue(Qname, Qs) of
656 | #queue{type = {passive, _}, waiters = Ws} = Q ->
657 | case Ws of
658 | [] ->
659 | {Result, S2} = do_enqueue(TS, Item, Q, S1),
660 | {reply, Result, S2};
661 | [From|Ws1] ->
662 | gen_server:reply(From, [{TS, Item}]),
663 | {reply, ok, update_queue(Q#queue{waiters = Ws1}, S)}
664 | end;
665 | _ ->
666 | {reply, badarg, S1}
667 | end;
668 | i_handle_call({dequeue, Type, N}, From, #st{queues = Qs} = S) ->
669 | {Qname, _S1} = select_queue(Type, undefined, S),
670 | case get_queue(Qname, Qs) of
671 | #queue{type = #passive{}, waiters = Ws} = Q ->
672 | {Items, Q1} = q_out(N, Q),
673 | case Items of
674 | [] ->
675 | {noreply, update_queue(Q1#queue{waiters = Ws ++ [From]}, S)};
676 | Jobs ->
677 | {reply, Jobs, update_queue(Q1, S)}
678 | end;
679 | _Other ->
680 | io:fwrite("get_queue(~p, ~p) -> ~p~n", [Qname, Qs, _Other]),
681 | {reply, badarg, S}
682 | end;
683 | i_handle_call({set_modifiers, Modifiers}, _, #st{queues = Qs,
684 | group_rates = GRs,
685 | counters = Cs} = S) ->
686 | GRs1 = [apply_modifiers(Modifiers, G) || G <- GRs],
687 | Cs1 = [apply_modifiers(Modifiers, C) || C <- Cs],
688 | Qs1 = [apply_modifiers(Modifiers, Q) || Q <- Qs],
689 | {reply, ok, S#st{queues = Qs1,
690 | group_rates = GRs1,
691 | counters = Cs1}};
692 | i_handle_call({add_queue, Name, Options}, _, #st{queues = Qs} = S) ->
693 | false = get_queue(Name, Qs),
694 | NewQueues = init_queues([{Name, Options}], S),
695 | revisit_queue(Name),
696 | {reply, ok, lift_counters(S#st{queues = Qs ++ NewQueues})};
697 | i_handle_call({delete_queue, Name}, _, #st{queues = Qs} = S) ->
698 | case get_queue(Name, Qs) of
699 | false ->
700 | {reply, false, S};
701 | #queue{} = Q ->
702 | %% for now, let's be very brutal
703 | case Q of
704 | #queue{st = undefined} -> ok;
705 | _ ->
706 | [reject(Client) || {_, Client} <- q_all(Q)]
707 | end,
708 | q_delete(Q),
709 | {reply, true, S#st{queues = lists:keydelete(Name,#queue.name,Qs)}}
710 | end;
711 | i_handle_call({info, Item}, _, S) ->
712 | {reply, get_info(Item, S), S};
713 | i_handle_call({queue_info, Name}, _, #st{queues = Qs} = S) ->
714 | {reply, get_queue_info(Name, Qs), S};
715 | i_handle_call({queue_info, Name, Item}, _, #st{queues = Qs} = S) ->
716 | {reply, get_queue_info(Name, Item, Qs), S};
717 | i_handle_call({modify_regulator, Type, Qname, RegName, Opts}, _, S) ->
718 | case get_queue(Qname, S#st.queues) of
719 | #queue{} = Q ->
720 | case do_modify_regulator(Type, RegName, Opts, Q) of
721 | badarg ->
722 | {reply, badarg, S};
723 | Q1 ->
724 | S1 = update_queue(Q1, S),
725 | {reply, ok, S1}
726 | end;
727 | _ ->
728 | {reply, badarg, S}
729 | end;
730 | i_handle_call({modify_counter, CName, Opts}, _, #st{counters = Cs} = S) ->
731 | case get_counter(CName, Cs) of
732 | false ->
733 | {reply, badarg, S};
734 | #cr{} = CR ->
735 | try CR1 = init_counter(Opts, CR),
736 | {reply, ok, S#st{counters = update_counter(CName,Cs,CR1)}}
737 | catch
738 | error:_ ->
739 | {reply, badarg, S}
740 | end
741 | end;
742 | i_handle_call({delete_counter, Name}, _, #st{counters = Cs} = S) ->
743 | case get_counter(Name, Cs) of
744 | false ->
745 | {reply, false, S};
746 | #cr{} ->
747 | %% The question is whether we should also delete references
748 | %% to the queue? It seems like we should...
749 | Cs1 = lists:keydelete(Name, #cr.name, Cs),
750 | {reply, true, S#st{counters = Cs1}}
751 | end;
752 | i_handle_call({add_counter, Name, Opts}=Req, _, #st{counters = Cs} = S) ->
753 | case get_counter(Name, Cs) of
754 | false ->
755 | try CR = init_counter([{name,Name}|Opts], #cr{name = Name,
756 | shared = true}),
757 | CR1 = pickup_aliases(S#st.queues, CR),
758 | {reply, ok, S#st{counters = Cs ++ [CR1]}}
759 | catch
760 | error:Reason ->
761 | error_logger:error_report([{error, Reason},
762 | {request, Req}]),
763 | {reply, badarg, S}
764 | end;
765 | _ ->
766 | {reply, badarg, S}
767 | end;
768 | i_handle_call({add_group_rate, Name, Opts}=Req,_, #st{group_rates = Gs} = S) ->
769 | case get_group(Name, Gs) of
770 | false ->
771 | try GR = init_group(Opts, #grp{name = Name}),
772 | {reply, ok, S#st{group_rates = Gs ++ [GR]}}
773 | catch
774 | error:Reason ->
775 | error_logger:error_report([{error, Reason},
776 | {request, Req}]),
777 | {reply, badarg, S}
778 | end;
779 | _ ->
780 | {reply, badarg, S}
781 | end;
782 | i_handle_call({modify_group_rate, Name, Opts},_, #st{group_rates = Gs} = S) ->
783 | case get_group(Name, Gs) of
784 | #grp{} = G ->
785 | try G1 = init_group(Opts, G),
786 | {reply, ok, S#st{group_rates = update_group(Name,Gs,G1)}}
787 | catch
788 | error:_ ->
789 | {reply, badarg, S}
790 | end;
791 | _ ->
792 | {reply, badarg, S}
793 | end;
794 | i_handle_call({ask_queue, QName, Req}, From, #st{queues = Qs} = S) ->
795 | case get_queue(QName, Qs) of
796 | false ->
797 | {reply, badarg, S};
798 | #queue{stateful = Stf} = Q ->
799 | SetQState =
800 | fun(Stf1) ->
801 | S#st{queues =
802 | lists:keyreplace(QName, #queue.name, Qs,
803 | Q#queue{stateful = Stf1})}
804 | end,
805 | %% We don't catch errors; this is done at the level above.
806 | %% One possible error is that the queue module doesn't have a
807 | %% handle_call/3 callback.
808 | case ask_stateful(Req, From, Stf, S#st.info_f) of
809 | badarg -> {reply, badarg, S};
810 | {reply, Reply, Stf1} ->
811 | {reply, Reply, SetQState(Stf1)};
812 | {noreply, Stf1} ->
813 | {noreply, SetQState(Stf1)}
814 | %% case M:handle_call(Req, From, QSt) of
815 | %% {reply, Rep, QSt1} ->
816 | %% {reply, Rep, SetQState(QSt1)};
817 | %% {noreply, QSt1} ->
818 | %% {noreply, SetQState(QSt1)}
819 | end
820 | end;
821 | i_handle_call(_Req, _, S) ->
822 | {reply, badarg, S}.
823 |
824 | handle_cast({done, Pid, {Ref, Opaque}}, #st{monitors = Mons} = S) ->
825 | Cs = proplists:get_value(counters, Opaque, []),
826 | case ets:lookup(Mons, {Pid,Ref}) of
827 | [] ->
828 | %% huh?
829 | io:fwrite("Didn't find monitor for Pid ~p~n", [Pid]);
830 | [_] ->
831 | erlang:demonitor(Ref, [flush]),
832 | ets:delete(Mons, {Pid,Ref})
833 | end,
834 | {Revisit, S1} = restore_counters(Cs, S),
835 | {noreply, revisit_queues(Revisit, S1)};
836 | handle_cast(_Msg, S) ->
837 | {noreply, S}.
838 |
839 | handle_info({'DOWN', Ref, _, Pid, _}, #st{monitors = Mons} = S) ->
840 | case ets:lookup(Mons, {Pid,Ref}) of
841 | [{_, QName}] ->
842 | ets:delete(Mons, {Pid,Ref}),
843 | Cs = get_counters(QName, S), %% TODO! implement function
844 | {Revisit, S1} = restore_counters(Cs, S),
845 | {noreply, revisit_queues(Revisit, S1)};
846 | [] ->
847 | {noreply, S}
848 | end;
849 | handle_info({check_queue, Name}, #st{queues = Qs} = S) ->
850 | case get_queue(Name, Qs) of
851 | #queue{} = Q ->
852 | TS = timestamp(),
853 | {Q1, S1} = perform_queue_check(Q#queue{timer = undefined}, TS, S),
854 | {noreply, update_queue(restart_timer(Q1), S1)};
855 | _ ->
856 | {noreply, S}
857 | end;
858 | handle_info(_Msg, S) ->
859 | io:fwrite("~p: handle_info(~p,~p)~n", [?MODULE, _Msg,S]),
860 | {noreply, S}.
861 |
862 | terminate(_,_) ->
863 | ok.
864 |
865 | code_change(_FromVsn, St, _Extra) ->
866 | {ok, set_info(St)}.
867 |
868 |
869 | %% Internal functions
870 |
871 | get_info(queues, #st{queues = Qs}) ->
872 | jobs_info:pp(Qs);
873 | get_info(group_rates, #st{group_rates = Gs}) ->
874 | jobs_info:pp(Gs);
875 | get_info(counters, #st{counters = Cs}) ->
876 | jobs_info:pp(Cs).
877 |
878 | get_queue_info(Name, Qs) ->
879 | case get_queue(Name, Qs) of
880 | false ->
881 | undefined;
882 | Other ->
883 | jobs_info:pp(Other)
884 | end.
885 |
886 | get_queue_info(Name, Item, Qs) ->
887 | case get_queue(Name, Qs) of
888 | false ->
889 | undefined;
890 | Q ->
891 | do_get_queue_info(Item, Q)
892 | end.
893 |
894 | do_get_queue_info(rate_limit, #queue{regulators = Rs}) ->
895 | case lists:keyfind(rr, 1, Rs) of
896 | #rr{rate = #rate{limit = Limit}} ->
897 | Limit;
898 | false ->
899 | undefined
900 | end.
901 |
902 | get_queue(Name, Qs) ->
903 | lists:keyfind(Name, #queue.name, Qs).
904 |
905 | get_group(undefined, _) ->
906 | false;
907 | get_group(Name, Groups) ->
908 | lists:keyfind(Name, #grp.name, Groups).
909 |
910 | get_counter(undefined, _) ->
911 | false;
912 | get_counter(Name, Cs) ->
913 | lists:keyfind(Name, #cr.name, Cs).
914 |
915 | update_group(Name, Gs, GR) ->
916 | lists:keyreplace(Name, #grp.name, Gs, GR).
917 |
918 | update_counter(Name, Cs, CR) ->
919 | lists:keyreplace(Name, #cr.name, Cs, CR).
920 |
921 | update_rate_regulator(RR, Rs) ->
922 | %% At most one rate regulator allowed per queue - match on record tag
923 | lists:keyreplace(rr, 1, Rs, RR).
924 |
925 |
926 | do_modify_regulator(Type, Name, Opts, #queue{} = Q) ->
927 | case lists:keymember(name, 1, Opts) of
928 | true ->
929 | badarg;
930 | false ->
931 | ok
932 | end,
933 | case Type of
934 | rate -> do_modify_rate_regulator(Name, Opts, Q);
935 | counter -> do_modify_counter_regulator(Name, Opts, Q);
936 | _ ->
937 | badarg
938 | end.
939 |
940 | do_modify_rate_regulator(Name, Opts, #queue{regulators = Regs} = Q) ->
941 | case lists:keyfind(Name, #rr.name, Regs) of
942 | #rr{} = RR ->
943 | try RR1 = init_rate_regulator(Opts, RR),
944 | Q#queue{regulators = lists:keyreplace(
945 | Name,#rr.name,Regs,RR1)}
946 | catch
947 | error:_ ->
948 | badarg
949 | end;
950 | _ ->
951 | badarg
952 | end.
953 |
954 | do_modify_counter_regulator(Name, Opts, #queue{regulators = Regs} = Q) ->
955 | case lists:keyfind(Name, #rr.name, Regs) of
956 | #cr{} = CR ->
957 | try CR1 = init_counter(Opts, CR),
958 | Q#queue{regulators = lists:keyreplace(
959 | Name,#cr.name,Regs,CR1)}
960 | catch
961 | error:_ ->
962 | badarg
963 | end;
964 | _ ->
965 | badarg
966 | end.
967 |
968 | job_queued(#queue{check_counter = Ctr} = Q, PrevSz, TS, S) ->
969 | case Ctr + 1 of
970 | C when C > 10 ->
971 | perform_queue_check(Q, TS, S);
972 | C ->
973 | Q1 = Q#queue{check_counter = C},
974 | if PrevSz == 0 ->
975 | perform_queue_check(Q1, TS, S);
976 | true ->
977 | {Q#queue{check_counter = C}, S}
978 | end
979 | end.
980 |
981 | check_timedout(#queue{max_time = undefined} = Q, _) -> Q;
982 | check_timedout(#queue{oldest_job = undefined} = Q, _) -> Q;
983 | check_timedout(#queue{max_time = MaxT,
984 | oldest_job = Oldest} = Q, TS) ->
985 | if (TS - Oldest) > MaxT * 1000 ->
986 | case q_timedout(Q) of
987 | [] -> Q;
988 | {OldJobs, Q1} ->
989 | [timeout(J) || J <- OldJobs],
990 | Q1
991 | end;
992 | true ->
993 | Q
994 | end.
995 |
996 |
997 | perform_queue_check(Q, TS, S) ->
998 | Q1 = check_timedout(maybe_cancel_timer(Q), TS),
999 | case check_queue(Q1, TS, S) of
1000 | {0, _, _} ->
1001 | {Q1, update_queue(Q1, S)};
1002 | {N, Counters, Regs} when N > 0 ->
1003 | {Nd, _Jobs, Q2} = dispatch_N(N, Counters, TS, Q1, S),
1004 | {_Q3, _S3} = update_counters_and_regs(Nd, Counters, Regs, Q2, S);
1005 | %% {Q3, S3} = update_regulators(
1006 | %% Regs, Q2#queue{latest_dispatch = TS,
1007 | %% check_counter = 0}, S),
1008 | %% update_counters(Jobs, Counters, update_queue(Q3, S3));
1009 | Bad ->
1010 | erlang:error({bad_N, Bad})
1011 | end.
1012 |
1013 | maybe_cancel_timer(#queue{timer = undefined} = Q) ->
1014 | Q;
1015 | maybe_cancel_timer(#queue{timer = TRef} = Q) ->
1016 | erlang:cancel_timer(TRef),
1017 | Q#queue{timer = undefined}.
1018 |
1019 |
1020 | check_queue(#queue{type = #producer{}} = Q, TS, S) ->
1021 | do_check_queue(Q, TS, S);
1022 | check_queue(#queue{} = Q, TS, S) ->
1023 | case q_is_empty(Q) of
1024 | true ->
1025 | %% no action necessary
1026 | {0, [], []};
1027 | false ->
1028 | %% non-empty queue
1029 | do_check_queue(Q, TS, S)
1030 | end.
1031 |
1032 | do_check_queue(#queue{regulators = Regs0} = Q, TS, S) ->
1033 | Regs = expand_regulators(Regs0, S),
1034 | {N, Counters} = check_regulators(Regs, TS, Q),
1035 | {N, Counters, Regs}.
1036 |
1037 |
1038 | -type exp_regulator() :: {#cr{}, integer() | undefined} | #rr{} | #grp{}.
1039 |
1040 | -spec expand_regulators([regulator()], #st{}) -> [exp_regulator()].
1041 | %%
1042 | expand_regulators([#group_rate{name = R}|Regs], #st{group_rates = GRs} = S) ->
1043 | case get_group(R, GRs) of
1044 | false -> expand_regulators(Regs, S);
1045 | #grp{} = GR -> [GR|expand_regulators(Regs, S)]
1046 | end;
1047 | expand_regulators([#counter{name = C,
1048 | increment = I}|Regs], #st{counters = Cs} = S) ->
1049 | case get_counter(C, Cs) of
1050 | false -> expand_regulators(Regs, S);
1051 | #cr{} = CR -> [{CR,I}|expand_regulators(Regs, S)]
1052 | end;
1053 | expand_regulators([#rr{} = R|Regs], S) ->
1054 | [R|expand_regulators(Regs, S)];
1055 | expand_regulators([#cr{} = R|Regs], S) ->
1056 | [R|expand_regulators(Regs, S)];
1057 | expand_regulators([], _) ->
1058 | [].
1059 |
1060 | get_counters(QName, #st{queues = Qs} = S) ->
1061 | case get_queue(QName, Qs) of
1062 | false ->
1063 | [];
1064 | #queue{regulators = Rs} ->
1065 | CRs = expand_regulators([C || #counter{} = C <- Rs], S),
1066 | [{C, cr_increment(Ig,Il)} ||
1067 | {#cr{name = C, increment = Ig}, Il} <- CRs]
1068 | end.
1069 |
1070 | %% include_regulator(false, _, Regs, S) ->
1071 | %% expand_regulators(Regs, S);
1072 | %% include_regulator(R, Local, Regs, S) ->
1073 | %% [{R, Local}|expand_regulators(Regs, S)].
1074 |
1075 | update_counters_and_regs(NJobs, Counters, Regs, Q, S) ->
1076 | update_regulators(Regs, Q, update_counters(NJobs, Counters, S)).
1077 |
1078 | update_regulators(Regs, Q0, S0) ->
1079 | lists:foldl(
1080 | fun(#grp{name = R} = GR, {Q, #st{group_rates = GRs} = S}) ->
1081 | GR1 = GR#grp{latest_dispatch = Q#queue.latest_dispatch},
1082 | S1 = S#st{group_rates = update_group(R, GRs, GR1)},
1083 | {Q, S1};
1084 | (#rr{} = RR, {#queue{regulators = Rs} = Q, S}) ->
1085 | %% There can be at most one rate regulator
1086 | Q1 = Q#queue{regulators = update_rate_regulator(RR, Rs)},
1087 | {Q1, S};
1088 | (_, Acc) ->
1089 | Acc
1090 | %% (#cr{name = R,
1091 | %% shared = true} = CR, {Q, #st{counters = Cs} = S}) ->
1092 | %% S1 = S#st{counters = update_counter(R, Cs, CR)},
1093 | %% {Q, S1};
1094 | %% (#cr{name = R} = CR, {#queue{regulators = Rs} = Q, S}) ->
1095 | %% Q1 = Q#queue{regulators = update_counter(R, Rs, CR)},
1096 | %% {Q1, S}
1097 | end, {Q0, S0}, Regs).
1098 |
1099 |
1100 | -spec check_regulators([exp_regulator()], timestamp(), #queue{}) ->
1101 | {integer(), [any()]}.
1102 | %%
1103 | check_regulators(Regs, TS, #queue{latest_dispatch = TL}) ->
1104 | case check_regulators(Regs, TS, TL, undefined, []) of
1105 | {undefined, _} ->
1106 | {0, []};
1107 | Other ->
1108 | Other
1109 | end.
1110 |
1111 | check_regulators([R|Regs], TS, TL, N, Cs) ->
1112 | case R of
1113 | #rr{rate = #rate{interval = I}} ->
1114 | N1 = check_rr(I, TS, TL),
1115 | check_regulators(Regs, TS, TL, erlang:min(N, N1), Cs);
1116 | #grp{rate = #rate{interval = I}, latest_dispatch = TLg} ->
1117 | N1 = check_rr(I, TS, TLg),
1118 | check_regulators(Regs, TS, TL, erlang:min(N, N1), Cs);
1119 | {#cr{} = CR, Il} ->
1120 | #cr{name = Name,
1121 | value = Val,
1122 | increment = Ig,
1123 | rate = #rate{limit = Max}} = CR,
1124 | I = cr_increment(Ig, Il),
1125 | C1 = check_cr(Val, I, Max),
1126 | Cs1 = if C1 == 0 ->
1127 | Cs;
1128 | true ->
1129 | [{Name, I}|Cs]
1130 | end,
1131 | check_regulators(Regs, TS, TL, erlang:min(C1, N), Cs1)
1132 | end;
1133 | check_regulators([], _, _, N, Cs) ->
1134 | {N, Cs}.
1135 |
1136 | cr_increment(Ig, undefined) -> Ig;
1137 | cr_increment(_ , Il) when is_integer(Il) -> Il.
1138 |
1139 | check_rr(undefined, _, _) ->
1140 | 0;
1141 | check_rr(I, _, 0) ->
1142 | %% initial dispatch
1143 | if I > 0 ->
1144 | 1;
1145 | true ->
1146 | 0
1147 | end;
1148 | check_rr(I, TS, TL) when TS > TL, I > 0 ->
1149 | trunc((TS - TL)/(I*1000)).
1150 |
1151 | check_cr(Val, I, Max) ->
1152 | case Max - Val of
1153 | N when N > 0 ->
1154 | N div I;
1155 | _ ->
1156 | 0
1157 | end.
1158 |
1159 | -spec dispatch_N(integer() | infinity, [any()], integer(), #queue{}, #st{}) ->
1160 | {list(), #queue{}}.
1161 | %%
1162 | dispatch_N(N, Counters, TS, #queue{name = QName,
1163 | approved = Approved0,
1164 | type = #producer{mod = Mod,
1165 | state = MS} = Prod} = Q,
1166 | #st{monitors = Mons, info_f = I}) ->
1167 | {Jobs, MS1} = spawn_producers(N, Mod, MS, Counters, I),
1168 | monitor_jobs(Jobs, QName, Mons),
1169 | Prod1 = Prod#producer{state = MS1},
1170 | %% Jobs = [spawn_mon_producer(F) || _ <- lists:seq(1,N)],
1171 | %% lists:foreach(
1172 | %% fun({Pid,Ref}) ->
1173 | %% ets:insert(Mons, {{Pid,Ref}, QName})
1174 | %% end, Jobs),
1175 | %% {Jobs, Q};
1176 | {N, Jobs, Q#queue{type = Prod1, approved = Approved0 + N,
1177 | latest_dispatch = TS}};
1178 | dispatch_N(N, Counters, TS, Q, #st{} = S) ->
1179 | {Jobs, Q1} = q_out(N, Q),
1180 | dispatch_jobs(Jobs, Counters, TS, Q1, S).
1181 |
1182 | dispatch_jobs(Jobs, [], TS, #queue{name = QName, stateful = Stf0} = Q,
1183 | #st{info_f = I, monitors = Mons}) ->
1184 | {Nd, NStf} = lists:foldl(
1185 | fun({_,{Pid,Ref}} = Job, {N, Stf}) ->
1186 | ets:insert(Mons, {{Pid,Ref}, QName}),
1187 | {_, Client, Opaque, Stf1} =
1188 | job_opaque(Job, [], Stf, I),
1189 | approve(Client, {Ref, Opaque}),
1190 | {N+1, Stf1}
1191 | end, {0, Stf0}, Jobs),
1192 | Approved0 = Q#queue.approved,
1193 | {Nd, Jobs, Q#queue{latest_dispatch = TS,
1194 | stateful = NStf,
1195 | approved = Approved0 + Nd}};
1196 | dispatch_jobs(Jobs, Counters, TS, #queue{stateful = Stf0} = Q,
1197 | #st{monitors = Mons, info_f = I}) ->
1198 | Opaque0 = [{counters, Counters}],
1199 | {Nd, NStf} = lists:foldl(
1200 | fun(Job, {N, Stf}) ->
1201 | {Pid, Client, Opaque, Stf1} =
1202 | job_opaque(Job, Opaque0, Stf, I),
1203 | Ref = erlang:monitor(process, Pid),
1204 | approve(Client, {Ref, Opaque}),
1205 | ets:insert(Mons, {{Pid, Ref}, Q#queue.name}),
1206 | {N+1, Stf1}
1207 | end, {0, Stf0}, Jobs),
1208 | Approved0 = Q#queue.approved,
1209 | {Nd, Jobs, Q#queue{latest_dispatch = TS,
1210 | stateful = NStf,
1211 | approved = Approved0 + Nd}}.
1212 |
1213 | update_stateful(undefined, _, _) ->
1214 | undefined;
1215 | update_stateful({Mod, ModS}, Opaque, I) ->
1216 | {V, S1} = Mod:next(Opaque, ModS, I),
1217 | {V, {Mod, S1}}.
1218 |
1219 | ask_stateful(_Req, _, undefined, _) ->
1220 | badarg;
1221 | ask_stateful(Req, From, {Mod, ModS}, I) ->
1222 | case Mod:handle_call(Req, From, ModS, I) of
1223 | {reply, Reply, NewModS} ->
1224 | {reply, Reply, {Mod, NewModS}};
1225 | {noreply, NewModS} ->
1226 | {noreply, {Mod, NewModS}}
1227 | end.
1228 |
1229 |
1230 | spawn_producers(N, Mod, ModS, Counters, I) ->
1231 | spawn_producers(N, Mod, ModS, [{counters, Counters}], I, []).
1232 |
1233 | spawn_producers(N, Mod, ModS, Opaque, I, Acc) when N > 0 ->
1234 | {F, ModS1} = Mod:next(Opaque, ModS, I),
1235 | spawn_producers(N-1, Mod, ModS1, Opaque, I,
1236 | [spawn_monitor(F) | Acc]);
1237 | spawn_producers(_, _, ModS, _, _, Acc) ->
1238 | {Acc, ModS}.
1239 |
1240 | monitor_jobs(Jobs, QName, Mons) ->
1241 | lists:foreach(
1242 | fun({Pid,Ref}) ->
1243 | ets:insert(Mons, {{Pid,Ref}, QName})
1244 | end, Jobs).
1245 |
1246 | %% spawn_mon_producer({M, F, A}) ->
1247 | %% spawn_monitor(M, F, A);
1248 | %% spawn_mon_producer(F) when is_function(F, 0) ->
1249 | %% spawn_monitor(F).
1250 |
1251 |
1252 | job_opaque({_, {Pid,_} = Client}, Opaque, Stf, I) ->
1253 | Stf1 = update_stateful(Stf, Opaque, I),
1254 | {Pid, Client, add_stateful(Stf1, Opaque), stateful_st(Stf1)}.
1255 | %% job_opaque({_, {Pid,_} = Client, Info}, Opaque, Stf, I) ->
1256 | %% Opaque1 = [{info, Info}|Opaque],
1257 | %% Stf1 = update_stateful(Stf, Opaque1, I),
1258 | %% {Pid, Client, add_stateful(Stf1, Opaque1), stateful_st(Stf1)}.
1259 |
1260 | add_stateful(undefined, Opaque) ->
1261 | Opaque;
1262 | add_stateful({V, _}, Opaque) ->
1263 | [{info, V}|Opaque].
1264 |
1265 | stateful_st(undefined) -> undefined;
1266 | stateful_st({_, St}) -> St.
1267 |
1268 |
1269 | update_counters(_, [], S) -> S;
1270 | update_counters(N, Cs, #st{counters = Counters} = S) ->
1271 | Counters1 =
1272 | lists:foldl(
1273 | fun({C,I}, Acc) ->
1274 | #cr{value = Old} = CR =
1275 | lists:keyfind(C, #cr.name, Acc),
1276 | CR1 = CR#cr{value = Old + I*N},
1277 | lists:keyreplace(C, #cr.name, Acc, CR1)
1278 | end, Counters, Cs),
1279 | S#st{counters = Counters1}.
1280 |
1281 |
1282 | restore_counters(Cs, #st{} = S) ->
1283 | lists:foldl(fun restore_counter/2, {[], S}, Cs).
1284 |
1285 | restore_counter({C, I}, {Revisit, #st{counters = Counters} = S}) ->
1286 | #cr{value = Val, queues = Qs} = CR =
1287 | lists:keyfind(C, #cr.name, Counters),
1288 | CR1 = CR#cr{value = Val - I},
1289 | Counters1 = lists:keyreplace(C, #cr.name, Counters, CR1),
1290 | S1 = S#st{counters = Counters1},
1291 | {union(Qs, Revisit), S1}.
1292 |
1293 | union(L1, L2) ->
1294 | (L1 -- L2) ++ L2.
1295 |
1296 | revisit_queues(Qs, S) ->
1297 | Expanded = [{Q, get_latest_dispatch(Q, S)} || Q <- Qs],
1298 | [revisit_queue(Q) || {Q,_} <- lists:keysort(2, Expanded)],
1299 | S.
1300 |
1301 | get_latest_dispatch(Q, #st{queues = Qs}) ->
1302 | #queue{latest_dispatch = Tl} =
1303 | lists:keyfind(Q, #queue.name, Qs),
1304 | Tl.
1305 |
1306 | revisit_queue(Qname) ->
1307 | self() ! {check_queue, Qname}.
1308 |
1309 | update_queue(#queue{name = N, type = T} = Q, #st{queues = Qs} = S) ->
1310 | Q1 = case T of
1311 | {passive, _} -> Q;
1312 | _ ->
1313 | start_timer(Q)
1314 | end,
1315 | S#st{queues = lists:keyreplace(N, #queue.name, Qs, Q1)}.
1316 |
1317 | kick_producers(#st{queues = Qs} = S) ->
1318 | lists:foreach(
1319 | fun(#queue{name = Qname, type = #producer{}}) ->
1320 | revisit_queue(Qname);
1321 | (_Q) ->
1322 | ok
1323 | end, Qs),
1324 | S.
1325 |
1326 | restart_timer(Q) ->
1327 | start_timer(maybe_cancel_timer(Q)).
1328 |
1329 | start_timer(#queue{timer = TRef} = Q) when TRef =/= undefined ->
1330 | Q;
1331 | start_timer(#queue{name = Name} = Q) ->
1332 | case next_time(timestamp(), Q) of
1333 | T when is_integer(T) ->
1334 | Msg = {check_queue, Name},
1335 | TRef = do_send_after(T, Msg),
1336 | Q#queue{timer = TRef};
1337 | undefined ->
1338 | Q
1339 | end.
1340 |
1341 | do_send_after(T, Msg) ->
1342 | erlang:send_after(T, self(), Msg).
1343 |
1344 | apply_modifiers(Modifiers, #queue{regulators = Rs} = Q) ->
1345 | Rs1 = [apply_modifiers(Modifiers, R) || R <- Rs],
1346 | Q#queue{regulators = Rs1};
1347 | apply_modifiers(_, #counter{} = C) ->
1348 | %% this is just an alias to a counter; modifiers are applied to counters separately
1349 | C;
1350 | apply_modifiers(Modifiers, Regulator) ->
1351 | with_modifiers(Modifiers, Regulator, fun apply_damper/4).
1352 |
1353 | %% (Don't quite remember right now when this is supposed to be used...)
1354 | %%
1355 | %% remove_modifiers(Modifiers, Regulator) ->
1356 | %% with_modifiers(Modifiers, Regulator, fun remove_damper/4).
1357 |
1358 | %% remove_damper(Type, _, _, R) ->
1359 | %% apply_corr(Type, 0, R).
1360 |
1361 |
1362 | with_modifiers(Modifiers, Regulator, F) ->
1363 | Rate = get_rate(Regulator),
1364 | R0 = lists:foldl(
1365 | fun({K, Local, Remote}, R) ->
1366 | F(K, Local, Remote, R)
1367 | end, Rate, Modifiers),
1368 | R1 = apply_active_modifiers(R0),
1369 | set_rate(R1, Regulator).
1370 |
1371 |
1372 | apply_damper(Type, Local, Remote, R) ->
1373 | case lists:keyfind(Type, 1, R#rate.modifiers) of
1374 | false ->
1375 | %% no effect on this regulator
1376 | R;
1377 | Found ->
1378 | apply_damper(Type, Found, Local, Remote, R)
1379 | end.
1380 |
1381 | apply_damper(Type, _Found, 0, [], R) ->
1382 | apply_corr(Type, 0, R);
1383 | apply_damper(Type, Found, Local, Remote, R) ->
1384 | case Found of
1385 | {_, Unit} when is_integer(Unit) ->
1386 | %% The active_modifiers list is kept up-to-date in order
1387 | %% to support remove_damper().
1388 | Corr = Local * Unit,
1389 | apply_corr(Type, Corr, R);
1390 | {_, LocalUnit, RemoteUnit} ->
1391 | LocalCorr = Local * LocalUnit,
1392 | RemoteCorr = case RemoteUnit of
1393 | {avg, RU} ->
1394 | RU * avg_remotes(Remote);
1395 | {max, RU} ->
1396 | RU * max_remotes(Remote)
1397 | end,
1398 | apply_corr(Type, LocalCorr + RemoteCorr, R);
1399 | {_, F} when tuple_size(F) == 2; is_function(F,2) ->
1400 | case F(Local, Remote) of
1401 | Corr when is_integer(Corr) ->
1402 | apply_corr(Type, Corr, R)
1403 | end
1404 | end.
1405 |
1406 | avg_remotes([]) ->
1407 | 0;
1408 | avg_remotes(L) ->
1409 | Sum = lists:foldl(fun({_,V}, Acc) -> V + Acc end, 0, L),
1410 | Sum div length(L).
1411 |
1412 | max_remotes(L) ->
1413 | lists:foldl(fun({_,V}, Acc) ->
1414 | erlang:max(V, Acc)
1415 | end, 0, L).
1416 |
1417 | apply_corr(Type, 0, R) ->
1418 | R#rate{active_modifiers = lists:keydelete(
1419 | Type, 1, R#rate.active_modifiers)};
1420 | apply_corr(Type, Corr, R) ->
1421 | ADs = lists:keystore(Type, 1, R#rate.active_modifiers,
1422 | {Type, Corr}),
1423 | R#rate{active_modifiers = ADs}.
1424 |
1425 |
1426 |
1427 | get_rate(#rr {rate = R}) -> R;
1428 | get_rate(#cr {rate = R}) -> R;
1429 | get_rate({#cr {rate = R},_}) -> R;
1430 | get_rate(#grp{rate = R}) -> R.
1431 |
1432 | set_rate(R, #rr {} = Reg) -> Reg#rr {rate = R};
1433 | set_rate(R, #cr {} = Reg) -> Reg#cr {rate = R};
1434 | set_rate(R, #grp{} = Reg) -> Reg#grp{rate = R}.
1435 |
1436 |
1437 | apply_active_modifiers(#rate{preset_limit = Preset,
1438 | active_modifiers = ADs} = R) ->
1439 | Limit = lists:foldl(
1440 | fun({_,Corr}, L) ->
1441 | L - round(Corr*Preset/100)
1442 | end, Preset, ADs),
1443 | R#rate{limit = Limit,
1444 | interval = interval(Limit)}.
1445 |
1446 | queue_job(TS, From, #queue{max_size = MaxSz} = Q, S) ->
1447 | CurSz = q_info(length, Q),
1448 | if CurSz >= MaxSz ->
1449 | case q_timedout(Q) of
1450 | [] ->
1451 | reject(From),
1452 | S;
1453 | {OldJobs, Q1} ->
1454 | [timeout(J) || J <- OldJobs],
1455 | %% update_queue(q_in(TS, From, Q1), S)
1456 | job_queued(q_in(TS, From, Q1), CurSz, TS, S)
1457 | end;
1458 | true ->
1459 | %% update_queue(q_in(TS, From, Q), S)
1460 | job_queued(q_in(TS, From, Q), CurSz, TS, S)
1461 | end.
1462 |
1463 | do_enqueue(TS, Item, #queue{max_size = MaxSz} = Q, S) ->
1464 | CurSz = q_info(length, Q),
1465 | if CurSz >= MaxSz ->
1466 | {{error, full}, S};
1467 | true ->
1468 | {ok, update_queue(q_in(TS, Item, Q), S)}
1469 | end.
1470 |
1471 | select_queue(Type, _, #st{q_select = undefined, default_queue = Def} = S) ->
1472 | if Type == undefined ->
1473 | {Def, S};
1474 | true ->
1475 | {Type, S}
1476 | end;
1477 | select_queue(Type, _, #st{q_select = M, q_select_st = MS, info_f = I} = S) ->
1478 | case M:queue_name(Type, MS, I) of
1479 | {ok, Name, MS1} ->
1480 | {Name, S#st{q_select_st = MS1}};
1481 | {error, Reason} ->
1482 | erlang:error({badarg, Reason})
1483 | end.
1484 |
1485 |
1486 | %% ===========================================================
1487 | %% Queue accessor functions.
1488 | %% It is possible to define a different queue type through the option
1489 | %% queue_mod :: atom() (Default = jobs_queue)
1490 | %% The callback module must implement the functions below.
1491 | %%
1492 | q_new(Opts) ->
1493 | [Name, Mod, Type, Stateful, MaxTime, MaxSize] =
1494 | [get_value(K, Opts, Def) || {K, Def} <- [{name, undefined},
1495 | {mod , jobs_queue},
1496 | {type, fifo},
1497 | {stateful, undefined},
1498 | {max_time, undefined},
1499 | {max_size, undefined}]],
1500 | Q0 = #queue{name = Name,
1501 | mod = Mod,
1502 | type = Type,
1503 | max_time = MaxTime,
1504 | max_size = MaxSize},
1505 | Q1 = Q0#queue{stateful = init_stateful(Stateful, Q0)},
1506 | case Type of
1507 | {producer, F} ->
1508 | init_producer(F, Opts, Q1);
1509 | %% Q0#queue{type = #producer{f = F}};
1510 | {action , A} -> Q1#queue{type = #action {a = A}};
1511 | _ ->
1512 | #queue{} = q_new(Opts, Q1, Mod)
1513 | end.
1514 |
1515 | q_new(Options, Q, Mod) ->
1516 | Mod:new(queue_options(Options, Q), Q).
1517 |
1518 | queue_options(Opts, #queue{type = #passive{type = T}}) ->
1519 | [{type, T}|Opts];
1520 | queue_options(Opts, _) ->
1521 | Opts.
1522 |
1523 | init_stateful(undefined, _) ->
1524 | undefined;
1525 | init_stateful(F, Q) ->
1526 | {Mod, Args} = init_f(F, jobs_stateful_simple),
1527 | ModS = Mod:init(Args, jobs_info:pp(Q)),
1528 | {Mod, ModS}.
1529 |
1530 | init_f(Type, DefMod) ->
1531 | case Type of
1532 | F when is_function(F, 0); is_function(F, 2) ->
1533 | {DefMod, F};
1534 | {M,F,A} ->
1535 | {DefMod, {M,F,A}};
1536 | {M, A} when is_atom(M) ->
1537 | {M, A}
1538 | end.
1539 |
1540 | init_producer(Type, _Opts, Q) ->
1541 | {Mod, Args} = init_f(Type, jobs_prod_simple),
1542 | ModS = Mod:init(Args, jobs_info:pp(Q)),
1543 | Q#queue{type = #producer{mod = Mod,
1544 | state = ModS}}.
1545 |
1546 | q_all (#queue{mod = Mod} = Q) -> Mod:all (Q).
1547 | q_timedout(#queue{mod = Mod} = Q) -> Mod:timedout(Q).
1548 | q_delete (#queue{mod = Mod} = Q) -> Mod:delete (Q).
1549 | %%
1550 | %q_is_empty(#queue{type = #producer{}}) -> false;
1551 | q_is_empty(#queue{mod = Mod} = Q) -> Mod:is_empty(Q).
1552 | %%
1553 | q_out (infinity, #queue{mod = Mod} = Q) -> Mod:all (Q);
1554 | q_out (N , #queue{mod = Mod} = Q) -> Mod:out (N, Q).
1555 | q_info (I , #queue{mod = Mod} = Q) -> Mod:info (I, Q).
1556 | %%
1557 | q_in(TS, From, #queue{mod = Mod, oldest_job = OJ} = Q) ->
1558 | OJ1 = erlang:min(TS, OJ), % Works even if OJ==undefined
1559 | Mod:in(TS, From, Q#queue{oldest_job = OJ1}).
1560 |
1561 | %% End queue accessor functions.
1562 | %% ===========================================================
1563 |
1564 | -spec next_time(TS :: integer(), #queue{}) -> integer() | undefined.
1565 | %%
1566 | %% Calculate the delay (in ms) until queue is checked again.
1567 | %%
1568 | next_time(TS, #queue{type = #producer{}} = Q) ->
1569 | next_time_(TS, Q);
1570 | next_time(_, #queue{type = #passive{}}) -> undefined;
1571 | next_time(_TS, #queue{oldest_job = undefined}) ->
1572 | %% queue is empty
1573 | undefined;
1574 | next_time(TS, Q) ->
1575 | next_time_(TS, Q).
1576 | %% next_time(TS, #queue{check_interval = infinity,
1577 | %% max_time = MaxT,
1578 | %% oldest_job = Oldest}) ->
1579 | %% case MaxT of
1580 | %% undefined -> undefined;
1581 | %% _ ->
1582 | %% %% timestamps are us, timeouts need to be in ms
1583 | %% erlang:max(0, MaxT - ((TS - Oldest) div 1000))
1584 | %% end;
1585 | next_time_(TS, #queue{latest_dispatch = TS1,
1586 | check_interval = I0,
1587 | max_time = MaxT,
1588 | oldest_job = Oldest}) ->
1589 | I = case I0 of
1590 | _ when is_number(I0) -> I0;
1591 | infinity -> undefined;
1592 | {M, F, As} ->
1593 | M:F(TS, TS1, As)
1594 | end,
1595 | TO = case MaxT of
1596 | undefined -> undefined;
1597 | _ when is_integer(MaxT) ->
1598 | MaxT - ((TS - Oldest) div 1000)
1599 | end,
1600 | NextI = if is_number(I) ->
1601 | Since = (TS - TS1) div 1000,
1602 | erlang:max(0, trunc(I - Since));
1603 | true ->
1604 | undefined
1605 | end,
1606 | case {TO, NextI} of
1607 | {undefined,undefined} -> undefined;
1608 | {undefined,_} -> NextI;
1609 | {_,undefined} -> TO;
1610 | {_,_} -> erlang:min(TO, NextI)
1611 | end.
1612 |
1613 |
1614 | %% Microsecond timestamp; never wraps
1615 | timestamp() ->
1616 | %% Invented epoc is {1258,0,0}, or 2009-11-12, 4:26:40
1617 | {MS,S,US} = erlang:now(),
1618 | (MS-1258)*1000000000000 + S*1000000 + US.
1619 |
1620 | timestamp_to_datetime(TS) ->
1621 | %% Our internal timestamps are relative to Now = {1258,0,0}
1622 | %% It doesn't really matter much how we construct a now()-like tuple,
1623 | %% as long as the weighted sum of the three numbers is correct.
1624 | S = TS div 1000000,
1625 | MS = round(TS rem 1000000 / 1000),
1626 | %% return {Datetime, Milliseconds}
1627 | {calendar:now_to_datetime({1258,S,0}), MS}.
1628 |
1629 |
1630 | %% Enforce a maximum frequency of error reports
1631 | error_report(E) ->
1632 | T = timestamp(),
1633 | Key = {?MODULE, prev_error},
1634 | case get(Key) of
1635 | T1 when T - T1 > ?MAX_ERROR_RPT_INTERVAL_US ->
1636 | error_logger:error_report(E),
1637 | put(Key, T);
1638 | _ ->
1639 | ignore
1640 | end.
1641 |
1642 | %% get_value(K, [{_, _, Opts}|T], Default) ->
1643 | %% case lists:keyfind(K, 1, Opts) of
1644 | %% false ->
1645 | %% get_value(
1646 |
1647 | get_all_values(K, Opts, Default) ->
1648 | case get_all_values(K, Opts) of
1649 | [] ->
1650 | Default;
1651 | Other ->
1652 | Other
1653 | end.
1654 |
1655 | get_all_values(K, [{_, _, Opts}|T]) ->
1656 | proplists:get_value(K, Opts, []) ++ get_all_values(K, T);
1657 | get_all_values(K, [{K, V}|T]) ->
1658 | %% shouldn't happen - right, dialyzer?
1659 | [V | get_all_values(K, T)];
1660 | get_all_values(_, []) ->
1661 | [].
1662 |
1663 | get_first_value(K, [{_,_,Opts}|T], Default) ->
1664 | case lists:keyfind(K, 1, Opts) of
1665 | false ->
1666 | get_first_value(K, T, Default);
1667 | {_, V} ->
1668 | V
1669 | end;
1670 | get_first_value(K, [{K, V}|_], _) ->
1671 | %% Again, shouldn't happen
1672 | V;
1673 | get_first_value(_, [], Default) ->
1674 | Default.
1675 |
1676 |
--------------------------------------------------------------------------------
/src/jobs_stateful_simple.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_stateful_simple).
2 |
3 | -export([init/2,
4 | next/3,
5 | handle_call/4]).
6 |
7 | -include("jobs.hrl").
8 |
9 | init(F, Info) when is_function(F, 2) ->
10 | #stateful{f = F, st = F(init, Info)}.
11 |
12 | next(_Opaque, #stateful{f = F, st = St} = P, Info) ->
13 | case F(St, Info) of
14 | {V, St1} ->
15 | {V, P#stateful{st = St1}};
16 | Other ->
17 | erlang:error({bad_stateful_next, Other})
18 | end.
19 |
20 | handle_call(Req, From, #stateful{f = F, st = St} = P, Info) ->
21 | case F({call, Req, From, St}, Info) of
22 | {reply, Reply, St1} ->
23 | {reply, Reply, P#stateful{st = St1}};
24 | {noreply, St1} ->
25 | {noreply, P#stateful{st = St1}}
26 | end.
27 |
--------------------------------------------------------------------------------
/test/jobs_eqc_queue.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_eqc_queue).
2 |
3 | -ifdef(EQC).
4 | -include_lib("eqc/include/eqc.hrl").
5 | -include("jobs.hrl").
6 |
7 | -compile(export_all).
8 |
9 | -record(model,
10 | { time = jobs_lib:timestamp(),
11 | st = undefined }).
12 |
13 | test() ->
14 | test(300).
15 |
16 | test(N) ->
17 | meck:new(jobs_lib, [passthrough]),
18 | eqc:module({numtests, N}, ?MODULE),
19 | meck:unload(jobs_lib).
20 |
21 | g_job() ->
22 | {make_ref(), make_ref()}.
23 |
24 | g_scheduling_order() ->
25 | elements([fifo, lifo]).
26 |
27 | g_options(jobs_queue) -> return([{type, fifo}]);
28 | g_options(jobs_queue_list) -> return([{type, lifo}]).
29 |
30 | g_queue_record() ->
31 | ?LET(N, nat(),
32 | #queue { max_time = N }).
33 |
34 | g_start_time() ->
35 | ?LET({T, N}, {jobs_lib:timestamp(), nat()},
36 | T + N).
37 |
38 | g_time_advance() ->
39 | ?LET(N, nat(), N+1).
40 |
41 | g_model_type() ->
42 | oneof([jobs_queue_list, jobs_queue]).
43 |
44 | g_model(Ty) ->
45 | ?SIZED(Size, g_model(Size, Ty)).
46 |
47 | g_model(0, Ty) ->
48 | oneof([{call, ?MODULE, new, [Ty,
49 | g_options(Ty),
50 | g_queue_record(),
51 | g_start_time()]}]);
52 | g_model(N, Ty) ->
53 | frequency([{1, g_model(0, Ty)},
54 | {N,
55 | ?LET(M, g_model(max(0, N-2), Ty),
56 | frequency(
57 | [
58 | {200, {call, ?MODULE, advance_time,
59 | [M, g_time_advance()]}},
60 | {200, {call, ?MODULE, in, [Ty, g_job(), M]}},
61 | {100, {call, ?MODULE, out, [Ty, choose(0,100), M]}},
62 | {20, {call, ?MODULE, timedout, [Ty, M]}},
63 | {1, {call, ?MODULE, empty, [Ty, M]}}
64 | ]))}
65 | ]).
66 |
67 | new(Mod, Opts, Q, T) ->
68 | advance_time(
69 | #model { st = Mod:new(Opts, Q),
70 | time = T}, 1).
71 |
72 | advance_time(#model { time = T} = M, N) ->
73 | M#model { time = T + N}.
74 |
75 | timedout(Mod, #model { st = Q} = M) ->
76 | set_time(M),
77 | NQ = case Mod:timedout(Q) of
78 | [] -> Q;
79 | {_, Q1} -> Q1
80 | end,
81 | advance_time(M#model { st = NQ }, 1).
82 |
83 | timedout_obs(Mod, #model { st = Q} = M) ->
84 | set_time(M),
85 | case Mod:timedout(Q) of
86 | [] -> [];
87 | {TO, _} -> lists:sort(TO)
88 | end.
89 |
90 | in(Mod, Job, #model { time = T, st = Q} = M) ->
91 | set_time(M),
92 | advance_time(
93 | M#model { st = Mod:in(T, Job, Q)},
94 | 1).
95 |
96 | out(Mod, N, #model { st = Q} = M) ->
97 | set_time(M),
98 | NQ = element(2, Mod:out(N, Q)),
99 | advance_time(
100 | M#model { st = NQ },
101 | 1).
102 |
103 | empty(Mod, #model { st = Q} = M) ->
104 | set_time(M),
105 | advance_time(M#model { st = Mod:empty(Q)}, 1).
106 |
107 | is_empty(M, #model { st = Q}) ->
108 | M:is_empty(Q).
109 |
110 | peek(M, #model { st = Q}) ->
111 | M:peek(Q).
112 |
113 | all(M, #model { st = Q}) ->
114 | M:all(Q).
115 |
116 | info(M, I, #model { st = Q}) ->
117 | M:info(I, Q).
118 |
119 | g_info() ->
120 | oneof([oldest_job, length, max_time]).
121 |
122 | obs() ->
123 | ?LET(Ty, g_model_type(),
124 | begin
125 | M = g_model(Ty),
126 | oneof([{call, ?MODULE, all, [Ty, M]},
127 | {call, ?MODULE, peek, [Ty, M]},
128 | {call, ?MODULE, info, [Ty, g_info(), M]},
129 | {call, ?MODULE, timedout_obs, [Ty, M]},
130 | {call, ?MODULE, is_empty, [Ty, M]}])
131 | end).
132 |
133 | model({call, ?MODULE, F, [W | Args]}) when W == jobs_queue;
134 | W == jobs_queue_list ->
135 | apply(?MODULE, F, model([jobs_queue_model | Args]));
136 | model({call, ?MODULE, F, Args}) ->
137 | apply(?MODULE, F, model(Args));
138 | model([H|T]) ->
139 | [model(H) | model(T)];
140 | model(X) ->
141 | X.
142 |
143 | prop_oldest_job_match() ->
144 | ?LET(Ty, g_model_type(),
145 | ?FORALL(M, g_model(Ty),
146 | begin
147 | R = eval(M),
148 | Repr = Ty:representation(R#model.st),
149 | OJ = proplists:get_value(oldest_job, Repr),
150 | Cts = proplists:get_value(contents, Repr),
151 | case OJ of
152 | undefined ->
153 | Cts == [];
154 | V ->
155 | lists:min([TS || {TS, _} <- Cts]) == V
156 | end
157 | end)).
158 |
159 | prop_queue() ->
160 | ?LET(Ty, g_model_type(),
161 | ?FORALL(M, g_model(Ty),
162 | equals(
163 | catching(fun() ->
164 | R = eval(M),
165 | Ty:representation(R#model.st)
166 | end),
167 | catching(fun () ->
168 | R = model(M),
169 | jobs_queue_model:representation(R#model.st)
170 | end)))).
171 |
172 | prop_observe() ->
173 | ?FORALL(Obs, obs(),
174 | equals(
175 | catch eval(Obs),
176 | catch model(Obs))).
177 |
178 | catching(F) ->
179 | try F()
180 | catch C:E ->
181 | {exception, C, E}
182 | end.
183 |
184 | set_time(#model { time = T}) ->
185 | meck:expect(jobs_lib, timestamp, fun() -> T end).
186 |
187 | -endif.
188 |
--------------------------------------------------------------------------------
/test/jobs_queue_model.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_queue_model).
2 |
3 | -include("jobs.hrl").
4 |
5 | -compile(export_all).
6 |
7 | new(Options, Q) ->
8 | case proplists:get_value(type, Options) of
9 | fifo ->
10 | Q#queue { type = fifo,
11 | st = queue:new() };
12 | lifo ->
13 | Q#queue { type = lifo,
14 | st = queue:new() }
15 | end.
16 |
17 | is_empty(#queue { st = Q}) ->
18 | queue:is_empty(Q).
19 |
20 | info(oldest_job, #queue { oldest_job = OJ}) ->
21 | OJ;
22 | info(max_time, #queue { max_time = MT}) -> MT;
23 | info(length, #queue { st = Q}) ->
24 | queue:len(Q).
25 |
26 | timedout(#queue { max_time = undefined}) ->
27 | %% This return value is highly illogical, but it is what the code returns!
28 | [];
29 | timedout(#queue { type = Ty,
30 | max_time = TO, st = Queue} = Q) ->
31 | Now = jobs_lib:timestamp(),
32 | QL = queue:to_list(Queue),
33 | {Left, Timedout} = lists:partition(
34 | fun({TS, _}) ->
35 | not(is_expired(TS, Now, TO))
36 | end, QL),
37 | OJ = get_oldest_job(Left),
38 | {case Ty of
39 | fifo -> Timedout;
40 | lifo -> lists:reverse(Timedout)
41 | end, Q#queue { oldest_job = OJ,
42 | st = queue:from_list(Left)}}.
43 |
44 | is_expired(TS, Now, TO) ->
45 | MS = Now - TS,
46 | MS > TO.
47 |
48 | get_oldest_job([]) -> undefined;
49 | get_oldest_job(L) ->
50 | lists:min([TS || {TS, _} <- L]).
51 |
52 | peek(#queue { type = fifo, st = Q }) ->
53 | case queue:peek(Q) of
54 | empty -> undefined;
55 | {value, K} -> K
56 | end;
57 | peek(#queue { type = lifo, st = Q }) ->
58 | case queue:peek_r(Q) of
59 | empty -> undefined;
60 | {value, K} -> K
61 | end.
62 |
63 | all(#queue { type = fifo, st = Q}) ->
64 | queue:to_list(Q);
65 | all(#queue { type = lifo, st = Q}) ->
66 | queue:to_list(queue:reverse(Q)).
67 |
68 | in(TS, E, #queue { st = Q,
69 | oldest_job = OJ } = S) ->
70 | S#queue { st = queue:in({TS, E}, Q),
71 | oldest_job = case OJ of undefined -> TS;
72 | _ -> OJ
73 | end}.
74 |
75 | out(N, #queue { type = Ty, st = Q} = S) ->
76 | {Elems, NQ} = out(Ty, N, Q, []),
77 | {Elems, S#queue { st = NQ,
78 | oldest_job = set_oldest_job(Ty, NQ) }}.
79 |
80 | set_oldest_job(fifo, Q) ->
81 | case queue:out(Q) of
82 | {{value, {TS, _}}, _} ->
83 | TS;
84 | {empty, _} ->
85 | undefined
86 | end;
87 | set_oldest_job(lifo, Q) ->
88 | case queue:out(Q) of
89 | {{value, {TS, _}}, _} ->
90 | TS;
91 | {empty, _} ->
92 | undefined
93 | end.
94 |
95 | out(fifo, 0, Q, Taken) ->
96 | {lists:reverse(Taken), Q};
97 | out(lifo, 0, Q, Taken) ->
98 | {Taken, Q};
99 | out(fifo, K, Q, Acc) when K > 0 ->
100 | case queue:out(Q) of
101 | {{value, E}, NQ} ->
102 | out(fifo, K-1, NQ, [E | Acc]);
103 | {empty, NQ} ->
104 | out(fifo, 0, NQ, Acc)
105 | end;
106 | out(lifo, K, Q, Acc) ->
107 | case queue:out_r(Q) of
108 | {{value, E}, NQ} ->
109 | out(lifo, K-1, NQ, [E | Acc]);
110 | {empty, NQ} ->
111 | out(lifo, 0, NQ, Acc)
112 | end.
113 |
114 |
115 | empty(#queue {} = Q) ->
116 | Q#queue { st = queue:new(),
117 | oldest_job = undefined }.
118 |
119 | representation(#queue { type = fifo, st = Q, oldest_job = OJ} ) ->
120 | Cts = queue:to_list(Q),
121 | [{oldest_job, OJ},
122 | {contents, Cts}];
123 | representation(#queue { type = lifo, st = Q, oldest_job = OJ} ) ->
124 | Cts = queue:to_list(queue:reverse(Q)),
125 | [{oldest_job, OJ},
126 | {contents, Cts}];
127 | representation(O) ->
128 | io:format("Otherwise: ~p", [O]),
129 | exit(fail).
130 |
131 |
132 |
--------------------------------------------------------------------------------
/test/jobs_sampler_slave.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_sampler_slave).
2 |
3 | -behaviour(jobs_sampler).
4 |
5 | -export([init/2,
6 | sample/2,
7 | handle_msg/3,
8 | calc/2]).
9 |
10 | -define(NOTEST, 1).
11 |
12 | init(_Name, {Type, Levels}) ->
13 | {ok, {Type, Levels}}.
14 |
15 |
16 | handle_msg({test, log, V}, _T, S) ->
17 | {log, V, S};
18 | handle_msg(_Msg, _T, S) ->
19 | {ignore, S}.
20 |
21 |
22 | sample(_, _S) ->
23 | ignore.
24 |
25 | calc(History, {Type, Levels} = S) ->
26 | {[{test,jobs_sampler:calc(Type, Levels, History)}], S}.
27 |
28 |
--------------------------------------------------------------------------------
/test/jobs_server_tests.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_server_tests).
2 |
3 |
4 | -include_lib("eunit/include/eunit.hrl").
5 |
6 | rate_test_() ->
7 | {foreachx,
8 | fun(Type) -> start_test_server(Type) end,
9 | fun(_, _) -> stop_server() end,
10 | [{{rate,1}, fun(_,_) -> [fun() -> serial(1,2,2) end] end}
11 | , {{rate, 5}, fun(_,_) -> [fun() -> serial(5,5,1) end] end}
12 | , {{rate, 50}, fun(_,_) -> [fun() -> serial(50,50,1) end] end}
13 | , {{rate, 100}, fun(_,_) -> [fun() -> serial(100,100,1) end] end}
14 | , {{rate, 300}, fun(_,_) -> [fun() -> serial(300,300,1) end] end}
15 | , {{rate, 500}, fun(_,_) -> [fun() -> serial(500,500,1) end] end}
16 | , {{rate, 1000}, fun(_,_) -> [fun() -> serial(1000,1000,1) end] end}
17 | %% , {{rate, 100}, fun(O,_) -> [fun() -> rate_test(O,1) end] end}
18 | , {{rate, 400}, fun(_,_) -> [fun() -> par_run(400,400,1) end] end}
19 | , {{rate, 600}, fun(_,_) -> [fun() -> par_run(600,600,1) end] end}
20 | , {{rate,1000}, fun(_,_) -> [fun() -> par_run(1000,1000,1) end] end}
21 | , {{rate,2000}, fun(_,_) -> [fun() -> par_run(2000,2000,1) end] end}
22 | %% , {[{rate,100},
23 | %% {group,50}], fun(O,_) -> [fun() -> max_rate_test(O,1) end] end}
24 | , {{count,3}, fun(_,_) -> [fun() -> counter_run(30,1) end] end}
25 | , {{timeout,500}, fun(_,_) -> [fun() ->
26 | ?debugVal(timeout_test(500))
27 | end] end}
28 | ]}.
29 |
30 |
31 |
32 | serial(R, N, TargetRatio) ->
33 | Expected = (N div R) * 1000000 * TargetRatio,
34 | ?debugVal({R,N,Expected}),
35 | {T,Ts} = tc(fun() -> run_jobs(q,N) end),
36 | time_eval(R, N, T, Ts, Expected).
37 |
38 | par_run(R, N, TargetRatio) ->
39 | Expected = (N div R) * 1000000 * TargetRatio,
40 | ?debugVal({R,N,Expected}),
41 | {T,Ts} = tc(fun() -> pmap(fun() ->
42 | run_job(q, one_job(time))
43 | end, N)
44 | end),
45 | time_eval(R, N, T, Ts, Expected).
46 |
47 | counter_run(N, Target) ->
48 | ?debugVal({N, Target}),
49 | {T, Ts} = tc(fun() ->
50 | pmap(fun() -> run_job(q, one_job(count)) end, N)
51 | end),
52 | ?debugVal({T,Ts}).
53 |
54 | timeout_test(T) ->
55 | case timer:tc(jobs, ask, [q]) of
56 | {US, {error, timeout}} ->
57 | case (US div 1000) - T of
58 | Diff when Diff < 5 ->
59 | ok;
60 | Other ->
61 | error({timeout_too_late, Other})
62 | end;
63 | Other ->
64 | error({timeout_expected, Other})
65 | end.
66 |
67 | time_eval(_R, _N, T, Ts, Expected) ->
68 | [{Hd,_}|Tl] = lists:sort(Ts),
69 | Diffs = [X-Hd || {X,_} <- Tl],
70 | Ratio = T/Expected,
71 | Max = lists:max(Diffs),
72 | {Mean, Variance} = time_variance(Diffs),
73 | io:fwrite(user,
74 | "Time: ~p, Ratio = ~.1f, Max = ~p, "
75 | "Mean = ~.1f, Variance = ~.1f~n",
76 | [T, Ratio, Max, Mean, Variance]).
77 |
78 |
79 | time_variance(L) ->
80 | N = length(L),
81 | Mean = lists:sum(L) / N,
82 | SQ = fun(X) -> X*X end,
83 | {Mean, math:sqrt(lists:sum([SQ(X-Mean) || X <- L]) / N)}.
84 |
85 |
86 |
87 | %% counter_test(Count) ->
88 | %% start_test_server({count,Count}),
89 | %% Res = tc(fun() ->
90 | %% pmap(fun() -> jobs:run(q, one_job(count)) end, Count * 2)
91 | %% end),
92 | %% io:fwrite(user, "~p~n", [Res]),
93 | %% stop_server().
94 |
95 |
96 | pmap(F, N) ->
97 | Pids = [spawn_monitor(fun() -> exit(F()) end) || _ <- lists:seq(1,N)],
98 | collect(Pids).
99 |
100 | collect([{_P,Ref}|Ps]) ->
101 | receive
102 | {'DOWN', Ref, _, _, Res} ->
103 | [Res|collect(Ps)]
104 | end;
105 | collect([]) ->
106 | [].
107 |
108 | start_test_server(Conf) ->
109 | start_test_server(true, Conf).
110 |
111 | start_test_server(Silent, {rate,Rate}) ->
112 | start_with_conf(Silent, [{queues, [{q, [{regulators,
113 | [{rate,[
114 | {limit, Rate}]
115 | }]}
116 | %% , {mod, jobs_queue_list}
117 | ]}
118 | ]}
119 | ]),
120 | Rate;
121 | start_test_server(Silent, [{rate,Rate},{group,Grp}]) ->
122 | start_with_conf(Silent,
123 | [{group_rates, [{gr, [{limit, Grp}]}]},
124 | {queues, [{q, [{regulators,
125 | [{rate,[{limit, Rate}]},
126 | {group_rate, gr}]}
127 | ]}
128 | ]}
129 | ]),
130 | Grp;
131 | start_test_server(Silent, {count, Count}) ->
132 | start_with_conf(Silent,
133 | [{queues, [{q, [{regulators,
134 | [{counter,[
135 | {limit, Count}
136 | ]
137 | }]}
138 | ]}
139 | ]}
140 | ]);
141 | start_test_server(Silent, {timeout, T}) ->
142 | start_with_conf(Silent,
143 | [{queues, [{q, [{regulators,
144 | [{counter,[
145 | {limit, 0}
146 | ]}
147 | ]},
148 | {max_time, T}
149 | ]}
150 | ]}
151 | ]).
152 |
153 |
154 | start_with_conf(Silent, Conf) ->
155 | application:unload(jobs),
156 | application:load(jobs),
157 | [application:set_env(jobs, K, V) || {K,V} <- Conf],
158 | if Silent == true ->
159 | error_logger:delete_report_handler(error_logger_tty_h);
160 | true ->
161 | ok
162 | end,
163 | application:start(jobs).
164 |
165 |
166 | stop_server() ->
167 | application:stop(jobs).
168 |
169 | tc(F) ->
170 | T1 = erlang:now(),
171 | R = (catch F()),
172 | T2 = erlang:now(),
173 | {timer:now_diff(T2,T1), R}.
174 |
175 | run_jobs(Q,N) ->
176 | [run_job(Q, one_job(time)) || _ <- lists:seq(1,N)].
177 |
178 | run_job(Q,F) ->
179 | timer:tc(jobs,run,[Q,F]).
180 |
181 | one_job(time) ->
182 | fun timestamp/0;
183 | one_job(count) ->
184 | fun() ->
185 | 1
186 | end.
187 |
188 |
189 | timestamp() ->
190 | jobs_server:timestamp().
191 |
--------------------------------------------------------------------------------
/test/jobs_tests.erl:
--------------------------------------------------------------------------------
1 | -module(jobs_tests).
2 |
3 | -compile(export_all).
4 |
5 | -include_lib("eunit/include/eunit.hrl").
6 |
7 |
8 |
9 | msg_test_() ->
10 | Rate = 100,
11 | {foreach,
12 | fun() -> with_msg_sampler(Rate) end,
13 | fun(_) -> stop_jobs() end,
14 | [
15 | {with, [fun apply_feedback/1]}
16 | ]}.
17 |
18 | dist_test_() ->
19 | Rate = 100,
20 | Name = jobs_eunit_slave,
21 | {foreach,
22 | fun() ->
23 | ?assertEqual(Rate, with_msg_sampler(Rate)),
24 | Remote = start_slave(Name),
25 | ?assertEqual(Rate,
26 | rpc:call(Remote, ?MODULE, with_msg_sampler, [Rate])),
27 | {Remote, Rate}
28 | end,
29 | fun({Remote, _}) ->
30 | Res = rpc:call(Remote, erlang, halt, []),
31 | io:fwrite(user, "Halting remote: ~p~n", [Res]),
32 | stop_jobs()
33 | end,
34 | [
35 | {with, [fun apply_feedback/1]}
36 | ]}.
37 |
38 |
39 |
40 | with_msg_sampler(Rate) ->
41 | application:unload(jobs),
42 | ok = application:load(jobs),
43 | [application:set_env(jobs, K, V) ||
44 | {K,V} <- [{queues, [{q, [{regulators,
45 | [{rate, [
46 | {limit, Rate},
47 | {modifiers,
48 | [{test,10, {max,5}}]}]}]}
49 | ]}
50 | ]},
51 | {samplers, [{test, jobs_sampler_slave,
52 | {value, [{1,1},{2,2},{3,3}]}}
53 | ]}
54 | ]
55 | ],
56 | ok = application:start(jobs),
57 | Rate.
58 |
59 | start_slave(Name) ->
60 | case node() of
61 | nonode@nohost ->
62 | os:cmd("epmd -daemon"),
63 | {ok, _} = net_kernel:start([jobs_eunit_master, shortnames]);
64 | _ ->
65 | ok
66 | end,
67 | {ok, Node} = slave:start(host(), Name, "-pa . -pz ../ebin"),
68 | io:fwrite(user, "Slave node: ~p~n", [Node]),
69 | Node.
70 |
71 | host() ->
72 | [Name, Host] = re:split(atom_to_list(node()), "@", [{return, list}]),
73 | list_to_atom(Host).
74 |
75 |
76 | stop_jobs() ->
77 | dbg:stop(),
78 | application:stop(jobs).
79 |
80 | apply_feedback(Rate) when is_integer(Rate) ->
81 | ?assertEqual(R0=get_rate(), Rate),
82 | io:fwrite(user, "R0 = ~p~n", [R0]),
83 | kick_sampler(1),
84 | io:fwrite(user, "get_rate() -> ~p~n", [get_rate()]),
85 | ?assertEqual(get_rate(), Rate - 10),
86 | kick_sampler(2),
87 | io:fwrite(user, "get_rate() -> ~p~n", [get_rate()]),
88 | ?assertEqual(get_rate(), Rate - 20),
89 | kick_sampler(3),
90 | io:fwrite(user, "get_rate() -> ~p~n", [get_rate()]),
91 | ?assertEqual(get_rate(), Rate - 30);
92 | apply_feedback({Remote, Rate}) ->
93 | ?assertEqual(R0=get_rate(), Rate),
94 | io:fwrite(user, "R0 = ~p~n", [R0]),
95 | ?assertEqual(rpc:call(Remote,?MODULE,get_rate,[]), Rate),
96 | kick_sampler(Remote, 1),
97 | io:fwrite(user, "[Remote] get_rate() -> ~p~n", [get_rate()]),
98 | ?assertEqual(get_rate(), Rate - 5),
99 | kick_sampler(Remote, 2),
100 | io:fwrite(user, "[Remote] get_rate() -> ~p~n", [get_rate()]),
101 | ?assertEqual(get_rate(), Rate - 10),
102 | kick_sampler(Remote, 3),
103 | io:fwrite(user, "[Remote] get_rate() -> ~p~n", [get_rate()]),
104 | ?assertEqual(get_rate(), Rate - 15).
105 |
106 |
107 | get_rate() ->
108 | jobs:queue_info(q, rate_limit).
109 |
110 | kick_sampler(N) ->
111 | jobs_sampler ! {test, log, N},
112 | timer:sleep(1000).
113 |
114 |
115 | kick_sampler(Remote, N) ->
116 | io:fwrite("Kicking sampler (N=~p) at ~p~n", [N, Remote]),
117 | {jobs_sampler, Remote} ! {test, log, N},
118 | timer:sleep(1000).
119 |
120 |
--------------------------------------------------------------------------------
/test/t.erl:
--------------------------------------------------------------------------------
1 | -module(t).
2 |
3 | -compile(export_all).
4 |
5 | t() ->
6 | t(300).
7 |
8 | t(N) ->
9 | jobs_eqc_queue:test(N).
10 |
--------------------------------------------------------------------------------