├── .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 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
jobs
jobs_app
jobs_info
jobs_lib
jobs_prod_simple
jobs_queue
jobs_queue_list
jobs_sampler
jobs_sampler_cpu
jobs_sampler_history
jobs_sampler_mnesia
jobs_server
jobs_stateful_simple
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 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
jobs
jobs_app
jobs_info
jobs_lib
jobs_queue
jobs_queue_list
jobs_sampler
jobs_sampler_cpu
jobs_sampler_history
jobs_sampler_mnesia
jobs_server
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 |
add_counter/2Adds a named counter to the load regulator on the current node.
add_group_rate/2Adds a group rate regulator to the load regulator on the current node.
add_queue/2Installs a new queue in the load regulator on the current node.
ask/1Asks permission to run a job of Type.
ask_queue/2Sends a synchronous request to a specific queue.
delete_counter/1Deletes a named counter from the load regulator on the current node.
delete_group_rate/1
delete_queue/1Deletes the named queue from the load regulator on the current node.
dequeue/2
done/1Signals completion of an executed task.
enqueue/2
info/1
job_info/1Retrieves job-specific information from the Opaque data object.
modify_counter/2
modify_group_rate/2
modify_regulator/4
queue_info/1
queue_info/2
run/2Executes Function() when permission has been granted by job regulator.
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 |
{Name, Options}
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 |
{Name, standard_rate, R}
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 |
{Name, standard_counter, N}
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 |
{Name, producer, F, Options}
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 |
init/1
start/2
stop/1
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 |
pp/1
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 |
timestamp/0
timestamp_to_datetime/1
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 |
init/2
next/3
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/1Return all the job entries in the queue, not removing them from the queue.
behaviour_info/1
delete/1Queue is being deleted; remove any external data structures.
empty/1
in/3Enqueue a job reference; return the updated queue.
info/2Return information about the queue.
is_empty/1
new/2Instantiate a new queue.
out/2Dequeue a batch of N jobs; return the modified queue.
peek/1Looks at the first item in the queue, without removing it.
representation/1A representation of a queue which can be inspected.
timedout/1Return 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 | 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 |
all/1
delete/1
empty/1
in/3
info/2
is_empty/1
new/2
out/2
peek/1
representation/1
timedout/1
timedout/2
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 |
behaviour_info/1
calc/3
code_change/3
end_subscription/0
handle_call/3
handle_cast/2
handle_info/2
init/1
start_link/0
start_link/1
subscribe/0Subscribes to feedback indicator information.
tell_sampler/2
terminate/2
trigger_sample/0
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 |
calc/2
handle_msg/3
init/2
sample/2
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 |
add/2
from_list/2
new/1
take_last/2
to_list/1
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 |
calc/2
handle_msg/3
init/2
sample/2
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 |
add_counter/2
add_group_rate/2
add_queue/2
ask/0
ask/1
ask_queue/2Invoke the Q:handle_call/3 function (if it exists).
code_change/3
delete_counter/1
delete_group_rate/1
delete_queue/1
dequeue/2
done/1
enqueue/2
handle_call/3
handle_cast/2
handle_info/2
info/1
init/1
modify_counter/2
modify_group_rate/2
modify_regulator/4
queue_info/1
queue_info/2
run/1
run/2
set_modifiers/1
start_link/0
start_link/1
terminate/2
timestamp/0
timestamp_to_datetime/1
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 |
handle_call/4
init/2
next/3
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 | --------------------------------------------------------------------------------