Merge lp:~eday/burrow/prototype-conversion into lp:burrow
- prototype-conversion
- Merge into trunk
Proposed by
Eric Day
Status: | Merged |
---|---|
Approved by: | Eric Day |
Approved revision: | 2 |
Merged at revision: | 2 |
Proposed branch: | lp:~eday/burrow/prototype-conversion |
Merge into: | lp:burrow |
Diff against target: |
1874 lines (+1780/-0) 17 files modified
Authors (+1/-0) ChangeLog (+4/-0) LICENSE (+202/-0) MANIFEST.in (+4/-0) bin/burrow (+18/-0) bin/burrowd (+34/-0) burrow/__init__.py (+19/-0) burrowd/__init__.py (+131/-0) burrowd/backend/__init__.py (+94/-0) burrowd/backend/memory.py (+176/-0) burrowd/backend/sqlite.py (+250/-0) burrowd/config.py (+55/-0) burrowd/frontend/__init__.py (+30/-0) burrowd/frontend/wsgi.py (+276/-0) etc/burrowd.conf (+99/-0) setup.py (+74/-0) test/frontend/test_wsgi.py (+313/-0) |
To merge this branch: | bzr merge lp:~eday/burrow/prototype-conversion |
Related bugs: | |
Related blueprints: |
Core setup and modules for the project
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Burrow Core Team | Pending | ||
Review via email:
|
Commit message
Description of the change
Python prototype conversion to get new trunk started.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'Authors' |
2 | --- Authors 1970-01-01 00:00:00 +0000 |
3 | +++ Authors 2011-03-17 23:47:24 +0000 |
4 | @@ -0,0 +1,1 @@ |
5 | +Eric Day <eday@oddments.org> |
6 | |
7 | === added file 'ChangeLog' |
8 | --- ChangeLog 1970-01-01 00:00:00 +0000 |
9 | +++ ChangeLog 2011-03-17 23:47:24 +0000 |
10 | @@ -0,0 +1,4 @@ |
11 | +2011-03-17 Eric Day <eday@oddments.org> |
12 | + |
13 | + Created new burrow trunk. |
14 | + |
15 | |
16 | === added file 'LICENSE' |
17 | --- LICENSE 1970-01-01 00:00:00 +0000 |
18 | +++ LICENSE 2011-03-17 23:47:24 +0000 |
19 | @@ -0,0 +1,202 @@ |
20 | + |
21 | + Apache License |
22 | + Version 2.0, January 2004 |
23 | + http://www.apache.org/licenses/ |
24 | + |
25 | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
26 | + |
27 | + 1. Definitions. |
28 | + |
29 | + "License" shall mean the terms and conditions for use, reproduction, |
30 | + and distribution as defined by Sections 1 through 9 of this document. |
31 | + |
32 | + "Licensor" shall mean the copyright owner or entity authorized by |
33 | + the copyright owner that is granting the License. |
34 | + |
35 | + "Legal Entity" shall mean the union of the acting entity and all |
36 | + other entities that control, are controlled by, or are under common |
37 | + control with that entity. For the purposes of this definition, |
38 | + "control" means (i) the power, direct or indirect, to cause the |
39 | + direction or management of such entity, whether by contract or |
40 | + otherwise, or (ii) ownership of fifty percent (50%) or more of the |
41 | + outstanding shares, or (iii) beneficial ownership of such entity. |
42 | + |
43 | + "You" (or "Your") shall mean an individual or Legal Entity |
44 | + exercising permissions granted by this License. |
45 | + |
46 | + "Source" form shall mean the preferred form for making modifications, |
47 | + including but not limited to software source code, documentation |
48 | + source, and configuration files. |
49 | + |
50 | + "Object" form shall mean any form resulting from mechanical |
51 | + transformation or translation of a Source form, including but |
52 | + not limited to compiled object code, generated documentation, |
53 | + and conversions to other media types. |
54 | + |
55 | + "Work" shall mean the work of authorship, whether in Source or |
56 | + Object form, made available under the License, as indicated by a |
57 | + copyright notice that is included in or attached to the work |
58 | + (an example is provided in the Appendix below). |
59 | + |
60 | + "Derivative Works" shall mean any work, whether in Source or Object |
61 | + form, that is based on (or derived from) the Work and for which the |
62 | + editorial revisions, annotations, elaborations, or other modifications |
63 | + represent, as a whole, an original work of authorship. For the purposes |
64 | + of this License, Derivative Works shall not include works that remain |
65 | + separable from, or merely link (or bind by name) to the interfaces of, |
66 | + the Work and Derivative Works thereof. |
67 | + |
68 | + "Contribution" shall mean any work of authorship, including |
69 | + the original version of the Work and any modifications or additions |
70 | + to that Work or Derivative Works thereof, that is intentionally |
71 | + submitted to Licensor for inclusion in the Work by the copyright owner |
72 | + or by an individual or Legal Entity authorized to submit on behalf of |
73 | + the copyright owner. For the purposes of this definition, "submitted" |
74 | + means any form of electronic, verbal, or written communication sent |
75 | + to the Licensor or its representatives, including but not limited to |
76 | + communication on electronic mailing lists, source code control systems, |
77 | + and issue tracking systems that are managed by, or on behalf of, the |
78 | + Licensor for the purpose of discussing and improving the Work, but |
79 | + excluding communication that is conspicuously marked or otherwise |
80 | + designated in writing by the copyright owner as "Not a Contribution." |
81 | + |
82 | + "Contributor" shall mean Licensor and any individual or Legal Entity |
83 | + on behalf of whom a Contribution has been received by Licensor and |
84 | + subsequently incorporated within the Work. |
85 | + |
86 | + 2. Grant of Copyright License. Subject to the terms and conditions of |
87 | + this License, each Contributor hereby grants to You a perpetual, |
88 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
89 | + copyright license to reproduce, prepare Derivative Works of, |
90 | + publicly display, publicly perform, sublicense, and distribute the |
91 | + Work and such Derivative Works in Source or Object form. |
92 | + |
93 | + 3. Grant of Patent License. Subject to the terms and conditions of |
94 | + this License, each Contributor hereby grants to You a perpetual, |
95 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
96 | + (except as stated in this section) patent license to make, have made, |
97 | + use, offer to sell, sell, import, and otherwise transfer the Work, |
98 | + where such license applies only to those patent claims licensable |
99 | + by such Contributor that are necessarily infringed by their |
100 | + Contribution(s) alone or by combination of their Contribution(s) |
101 | + with the Work to which such Contribution(s) was submitted. If You |
102 | + institute patent litigation against any entity (including a |
103 | + cross-claim or counterclaim in a lawsuit) alleging that the Work |
104 | + or a Contribution incorporated within the Work constitutes direct |
105 | + or contributory patent infringement, then any patent licenses |
106 | + granted to You under this License for that Work shall terminate |
107 | + as of the date such litigation is filed. |
108 | + |
109 | + 4. Redistribution. You may reproduce and distribute copies of the |
110 | + Work or Derivative Works thereof in any medium, with or without |
111 | + modifications, and in Source or Object form, provided that You |
112 | + meet the following conditions: |
113 | + |
114 | + (a) You must give any other recipients of the Work or |
115 | + Derivative Works a copy of this License; and |
116 | + |
117 | + (b) You must cause any modified files to carry prominent notices |
118 | + stating that You changed the files; and |
119 | + |
120 | + (c) You must retain, in the Source form of any Derivative Works |
121 | + that You distribute, all copyright, patent, trademark, and |
122 | + attribution notices from the Source form of the Work, |
123 | + excluding those notices that do not pertain to any part of |
124 | + the Derivative Works; and |
125 | + |
126 | + (d) If the Work includes a "NOTICE" text file as part of its |
127 | + distribution, then any Derivative Works that You distribute must |
128 | + include a readable copy of the attribution notices contained |
129 | + within such NOTICE file, excluding those notices that do not |
130 | + pertain to any part of the Derivative Works, in at least one |
131 | + of the following places: within a NOTICE text file distributed |
132 | + as part of the Derivative Works; within the Source form or |
133 | + documentation, if provided along with the Derivative Works; or, |
134 | + within a display generated by the Derivative Works, if and |
135 | + wherever such third-party notices normally appear. The contents |
136 | + of the NOTICE file are for informational purposes only and |
137 | + do not modify the License. You may add Your own attribution |
138 | + notices within Derivative Works that You distribute, alongside |
139 | + or as an addendum to the NOTICE text from the Work, provided |
140 | + that such additional attribution notices cannot be construed |
141 | + as modifying the License. |
142 | + |
143 | + You may add Your own copyright statement to Your modifications and |
144 | + may provide additional or different license terms and conditions |
145 | + for use, reproduction, or distribution of Your modifications, or |
146 | + for any such Derivative Works as a whole, provided Your use, |
147 | + reproduction, and distribution of the Work otherwise complies with |
148 | + the conditions stated in this License. |
149 | + |
150 | + 5. Submission of Contributions. Unless You explicitly state otherwise, |
151 | + any Contribution intentionally submitted for inclusion in the Work |
152 | + by You to the Licensor shall be under the terms and conditions of |
153 | + this License, without any additional terms or conditions. |
154 | + Notwithstanding the above, nothing herein shall supersede or modify |
155 | + the terms of any separate license agreement you may have executed |
156 | + with Licensor regarding such Contributions. |
157 | + |
158 | + 6. Trademarks. This License does not grant permission to use the trade |
159 | + names, trademarks, service marks, or product names of the Licensor, |
160 | + except as required for reasonable and customary use in describing the |
161 | + origin of the Work and reproducing the content of the NOTICE file. |
162 | + |
163 | + 7. Disclaimer of Warranty. Unless required by applicable law or |
164 | + agreed to in writing, Licensor provides the Work (and each |
165 | + Contributor provides its Contributions) on an "AS IS" BASIS, |
166 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
167 | + implied, including, without limitation, any warranties or conditions |
168 | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
169 | + PARTICULAR PURPOSE. You are solely responsible for determining the |
170 | + appropriateness of using or redistributing the Work and assume any |
171 | + risks associated with Your exercise of permissions under this License. |
172 | + |
173 | + 8. Limitation of Liability. In no event and under no legal theory, |
174 | + whether in tort (including negligence), contract, or otherwise, |
175 | + unless required by applicable law (such as deliberate and grossly |
176 | + negligent acts) or agreed to in writing, shall any Contributor be |
177 | + liable to You for damages, including any direct, indirect, special, |
178 | + incidental, or consequential damages of any character arising as a |
179 | + result of this License or out of the use or inability to use the |
180 | + Work (including but not limited to damages for loss of goodwill, |
181 | + work stoppage, computer failure or malfunction, or any and all |
182 | + other commercial damages or losses), even if such Contributor |
183 | + has been advised of the possibility of such damages. |
184 | + |
185 | + 9. Accepting Warranty or Additional Liability. While redistributing |
186 | + the Work or Derivative Works thereof, You may choose to offer, |
187 | + and charge a fee for, acceptance of support, warranty, indemnity, |
188 | + or other liability obligations and/or rights consistent with this |
189 | + License. However, in accepting such obligations, You may act only |
190 | + on Your own behalf and on Your sole responsibility, not on behalf |
191 | + of any other Contributor, and only if You agree to indemnify, |
192 | + defend, and hold each Contributor harmless for any liability |
193 | + incurred by, or claims asserted against, such Contributor by reason |
194 | + of your accepting any such warranty or additional liability. |
195 | + |
196 | + END OF TERMS AND CONDITIONS |
197 | + |
198 | + APPENDIX: How to apply the Apache License to your work. |
199 | + |
200 | + To apply the Apache License to your work, attach the following |
201 | + boilerplate notice, with the fields enclosed by brackets "[]" |
202 | + replaced with your own identifying information. (Don't include |
203 | + the brackets!) The text should be enclosed in the appropriate |
204 | + comment syntax for the file format. We also recommend that a |
205 | + file or class name and description of purpose be included on the |
206 | + same "printed page" as the copyright notice for easier |
207 | + identification within third-party archives. |
208 | + |
209 | + Copyright [yyyy] [name of copyright owner] |
210 | + |
211 | + Licensed under the Apache License, Version 2.0 (the "License"); |
212 | + you may not use this file except in compliance with the License. |
213 | + You may obtain a copy of the License at |
214 | + |
215 | + http://www.apache.org/licenses/LICENSE-2.0 |
216 | + |
217 | + Unless required by applicable law or agreed to in writing, software |
218 | + distributed under the License is distributed on an "AS IS" BASIS, |
219 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
220 | + See the License for the specific language governing permissions and |
221 | + limitations under the License. |
222 | |
223 | === added file 'MANIFEST.in' |
224 | --- MANIFEST.in 1970-01-01 00:00:00 +0000 |
225 | +++ MANIFEST.in 2011-03-17 23:47:24 +0000 |
226 | @@ -0,0 +1,4 @@ |
227 | +graft etc |
228 | +include Authors |
229 | +include ChangeLog |
230 | +include LICENSE |
231 | |
232 | === added directory 'bin' |
233 | === added file 'bin/burrow' |
234 | --- bin/burrow 1970-01-01 00:00:00 +0000 |
235 | +++ bin/burrow 2011-03-17 23:47:24 +0000 |
236 | @@ -0,0 +1,18 @@ |
237 | +#!/usr/bin/env python |
238 | +# Copyright (C) 2011 OpenStack LLC. |
239 | +# |
240 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
241 | +# you may not use this file except in compliance with the License. |
242 | +# You may obtain a copy of the License at |
243 | +# |
244 | +# http://www.apache.org/licenses/LICENSE-2.0 |
245 | +# |
246 | +# Unless required by applicable law or agreed to in writing, software |
247 | +# distributed under the License is distributed on an "AS IS" BASIS, |
248 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
249 | +# See the License for the specific language governing permissions and |
250 | +# limitations under the License. |
251 | + |
252 | +''' |
253 | +Burrow command line client. |
254 | +''' |
255 | |
256 | === added file 'bin/burrowd' |
257 | --- bin/burrowd 1970-01-01 00:00:00 +0000 |
258 | +++ bin/burrowd 2011-03-17 23:47:24 +0000 |
259 | @@ -0,0 +1,34 @@ |
260 | +#!/usr/bin/env python |
261 | +# Copyright (C) 2011 OpenStack LLC. |
262 | +# |
263 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
264 | +# you may not use this file except in compliance with the License. |
265 | +# You may obtain a copy of the License at |
266 | +# |
267 | +# http://www.apache.org/licenses/LICENSE-2.0 |
268 | +# |
269 | +# Unless required by applicable law or agreed to in writing, software |
270 | +# distributed under the License is distributed on an "AS IS" BASIS, |
271 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
272 | +# See the License for the specific language governing permissions and |
273 | +# limitations under the License. |
274 | + |
275 | +''' |
276 | +Burrow server. |
277 | +''' |
278 | + |
279 | +import os |
280 | +import sys |
281 | + |
282 | +# If ../burrowd/__init__.py exists, add ../ to the Python search path so |
283 | +# that it will override whatever may be installed in the default Python |
284 | +# search path. |
285 | +BASE_DIRECTORY = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir) |
286 | +BASE_DIRECTORY = os.path.normpath(BASE_DIRECTORY) |
287 | +if os.path.exists(os.path.join(BASE_DIRECTORY, 'burrowd', '__init__.py')): |
288 | + sys.path.insert(0, BASE_DIRECTORY) |
289 | + |
290 | +import burrowd |
291 | + |
292 | +if __name__ == '__main__': |
293 | + burrowd.Burrowd(sys.argv[1:]).run() |
294 | |
295 | === added directory 'burrow' |
296 | === added file 'burrow/__init__.py' |
297 | --- burrow/__init__.py 1970-01-01 00:00:00 +0000 |
298 | +++ burrow/__init__.py 2011-03-17 23:47:24 +0000 |
299 | @@ -0,0 +1,19 @@ |
300 | +# Copyright (C) 2011 OpenStack LLC. |
301 | +# |
302 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
303 | +# you may not use this file except in compliance with the License. |
304 | +# You may obtain a copy of the License at |
305 | +# |
306 | +# http://www.apache.org/licenses/LICENSE-2.0 |
307 | +# |
308 | +# Unless required by applicable law or agreed to in writing, software |
309 | +# distributed under the License is distributed on an "AS IS" BASIS, |
310 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
311 | +# See the License for the specific language governing permissions and |
312 | +# limitations under the License. |
313 | + |
314 | +'''Main client module for burrow.''' |
315 | + |
316 | + |
317 | +class Burrow(object): |
318 | + pass |
319 | |
320 | === added directory 'burrowd' |
321 | === added file 'burrowd/__init__.py' |
322 | --- burrowd/__init__.py 1970-01-01 00:00:00 +0000 |
323 | +++ burrowd/__init__.py 2011-03-17 23:47:24 +0000 |
324 | @@ -0,0 +1,131 @@ |
325 | +# Copyright (C) 2011 OpenStack LLC. |
326 | +# |
327 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
328 | +# you may not use this file except in compliance with the License. |
329 | +# You may obtain a copy of the License at |
330 | +# |
331 | +# http://www.apache.org/licenses/LICENSE-2.0 |
332 | +# |
333 | +# Unless required by applicable law or agreed to in writing, software |
334 | +# distributed under the License is distributed on an "AS IS" BASIS, |
335 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
336 | +# See the License for the specific language governing permissions and |
337 | +# limitations under the License. |
338 | + |
339 | +'''Main server module for burrow.''' |
340 | + |
341 | +import ConfigParser |
342 | +import gettext |
343 | +import logging |
344 | +import logging.config |
345 | +import sys |
346 | + |
347 | +import eventlet |
348 | + |
349 | +import burrowd.config |
350 | + |
351 | +# This installs the _(...) function as a built-in so all other modules |
352 | +# don't need to. |
353 | +gettext.install('burrowd') |
354 | + |
355 | +# Default configuration values for this module. |
356 | +DEFAULT_BACKEND = 'burrowd.backend.sqlite' |
357 | +DEFAULT_FRONTENDS = 'burrowd.frontend.wsgi' |
358 | +DEFAULT_THREAD_POOL_SIZE = 1000 |
359 | + |
360 | + |
361 | +class Burrowd(object): |
362 | + '''Server class for burrow.''' |
363 | + |
364 | + def __init__(self, config_files=[], add_default_log_handler=True): |
365 | + '''Initialize a server using the config files from the given |
366 | + list. This is passed directly to ConfigParser.read(), so |
367 | + files should be in ConfigParser format. This will load all |
368 | + frontend and backend classes from the configuration.''' |
369 | + if len(config_files) > 0: |
370 | + logging.config.fileConfig(config_files) |
371 | + self._config = ConfigParser.ConfigParser() |
372 | + self._config.read(config_files) |
373 | + self.config = burrowd.config.Config(self._config, 'burrowd') |
374 | + self.log = get_logger(self.config) |
375 | + if add_default_log_handler: |
376 | + self._add_default_log_handler() |
377 | + self._import_backend() |
378 | + self._import_frontends() |
379 | + |
380 | + def _add_default_log_handler(self): |
381 | + '''Add a default log handler it one has not been set.''' |
382 | + root_log = logging.getLogger() |
383 | + if len(root_log.handlers) > 0 or len(self.log.handlers) > 0: |
384 | + return |
385 | + handler = logging.StreamHandler() |
386 | + handler.setLevel(logging.DEBUG) |
387 | + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
388 | + handler.setFormatter(logging.Formatter(log_format)) |
389 | + root_log.addHandler(handler) |
390 | + |
391 | + def _import_backend(self): |
392 | + '''Load backend given in the 'backend' option.''' |
393 | + backend = self.config.get('backend', DEFAULT_BACKEND) |
394 | + config = (self._config, backend) |
395 | + self.backend = import_class(backend, 'Backend')(config) |
396 | + |
397 | + def _import_frontends(self): |
398 | + '''Load frontends given in the 'frontends' option.''' |
399 | + self.frontends = [] |
400 | + frontends = self.config.get('frontends', DEFAULT_FRONTENDS) |
401 | + for frontend in frontends.split(','): |
402 | + frontend = frontend.split(':') |
403 | + if len(frontend) == 1: |
404 | + frontend.append(None) |
405 | + config = (self._config, frontend[0], frontend[1]) |
406 | + frontend = import_class(frontend[0], 'Frontend') |
407 | + frontend = frontend(config, self.backend) |
408 | + self.frontends.append(frontend) |
409 | + |
410 | + def run(self): |
411 | + '''Create the thread pool and start the main server loop. Wait |
412 | + for the pool to complete, but possibly run forever if the |
413 | + frontends and backend never remove threads.''' |
414 | + thread_pool_size = self.config.getint('thread_pool_size', |
415 | + DEFAULT_THREAD_POOL_SIZE) |
416 | + thread_pool = eventlet.GreenPool(size=int(thread_pool_size)) |
417 | + self.backend.run(thread_pool) |
418 | + for frontend in self.frontends: |
419 | + frontend.run(thread_pool) |
420 | + self.log.info(_('Waiting for all threads to exit')) |
421 | + try: |
422 | + thread_pool.waitall() |
423 | + except KeyboardInterrupt: |
424 | + pass |
425 | + |
426 | + |
427 | +class Module(object): |
428 | + '''Common module class for burrow.''' |
429 | + |
430 | + def __init__(self, config): |
431 | + self.config = burrowd.config.Config(*config) |
432 | + self.log = get_logger(self.config) |
433 | + self.log.debug(_('Module created')) |
434 | + |
435 | + |
436 | +def get_logger(config): |
437 | + '''Create a logger from the given config.''' |
438 | + log = logging.getLogger(config.section) |
439 | + log_level = config.get('log_level', 'DEBUG') |
440 | + log_level = logging.getLevelName(log_level) |
441 | + if isinstance(log_level, int): |
442 | + log.setLevel(log_level) |
443 | + return log |
444 | + |
445 | + |
446 | +def import_class(module_name, class_name=None): |
447 | + '''Import a class given a full module.class name.''' |
448 | + if class_name is None: |
449 | + module_name, _separator, class_name = module_name.rpartition('.') |
450 | + try: |
451 | + __import__(module_name) |
452 | + return getattr(sys.modules[module_name], class_name) |
453 | + except (ImportError, ValueError, AttributeError), exception: |
454 | + raise ImportError(_('Class %s.%s cannot be found (%s)') % |
455 | + (module_name, class_name, exception)) |
456 | |
457 | === added directory 'burrowd/backend' |
458 | === added file 'burrowd/backend/__init__.py' |
459 | --- burrowd/backend/__init__.py 1970-01-01 00:00:00 +0000 |
460 | +++ burrowd/backend/__init__.py 2011-03-17 23:47:24 +0000 |
461 | @@ -0,0 +1,94 @@ |
462 | +# Copyright (C) 2011 OpenStack LLC. |
463 | +# |
464 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
465 | +# you may not use this file except in compliance with the License. |
466 | +# You may obtain a copy of the License at |
467 | +# |
468 | +# http://www.apache.org/licenses/LICENSE-2.0 |
469 | +# |
470 | +# Unless required by applicable law or agreed to in writing, software |
471 | +# distributed under the License is distributed on an "AS IS" BASIS, |
472 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
473 | +# See the License for the specific language governing permissions and |
474 | +# limitations under the License. |
475 | + |
476 | +'''Backends for the burrow server.''' |
477 | + |
478 | +import eventlet |
479 | + |
480 | +import burrowd |
481 | + |
482 | + |
483 | +class Backend(burrowd.Module): |
484 | + '''Interface that backend implementations must provide.''' |
485 | + |
486 | + def __init__(self, config): |
487 | + super(Backend, self).__init__(config) |
488 | + self.queues = {} |
489 | + |
490 | + def run(self, thread_pool): |
491 | + thread_pool.spawn_n(self._clean) |
492 | + |
493 | + def _clean(self): |
494 | + while True: |
495 | + self.clean() |
496 | + eventlet.sleep(1) |
497 | + |
498 | + def delete_accounts(self): |
499 | + pass |
500 | + |
501 | + def get_accounts(self): |
502 | + return [] |
503 | + |
504 | + def delete_account(self, account): |
505 | + pass |
506 | + |
507 | + def get_queues(self, account): |
508 | + return [] |
509 | + |
510 | + def queue_exists(self, account, queue): |
511 | + return False |
512 | + |
513 | + def delete_messages(self, account, queue, limit, marker, match_hidden): |
514 | + return [] |
515 | + |
516 | + def get_messages(self, account, queue, limit, marker, match_hidden): |
517 | + return [] |
518 | + |
519 | + def update_messages(self, account, queue, limit, marker, match_hidden, ttl, |
520 | + hide): |
521 | + return [] |
522 | + |
523 | + def delete_message(self, account, queue, message_id): |
524 | + return None |
525 | + |
526 | + def get_message(self, account, queue, message_id): |
527 | + return None |
528 | + |
529 | + def put_message(self, account, queue, message_id, ttl, hide, body): |
530 | + return True |
531 | + |
532 | + def update_message(self, account, queue, message_id, ttl, hide): |
533 | + return None |
534 | + |
535 | + def clean(self): |
536 | + '''This method should remove all messages with an expired |
537 | + TTL and make hidden messages that have an expired hide time |
538 | + visible again.''' |
539 | + pass |
540 | + |
541 | + def notify(self, account, queue): |
542 | + queue = '%s/%s' % (account, queue) |
543 | + if queue in self.queues: |
544 | + self.queues[queue].put(0) |
545 | + |
546 | + def wait(self, account, queue, seconds): |
547 | + queue = '%s/%s' % (account, queue) |
548 | + if queue not in self.queues: |
549 | + self.queues[queue] = eventlet.Queue() |
550 | + try: |
551 | + self.queues[queue].get(timeout=seconds) |
552 | + except Exception: |
553 | + pass |
554 | + if self.queues[queue].getting() == 0: |
555 | + del self.queues[queue] |
556 | |
557 | === added file 'burrowd/backend/memory.py' |
558 | --- burrowd/backend/memory.py 1970-01-01 00:00:00 +0000 |
559 | +++ burrowd/backend/memory.py 2011-03-17 23:47:24 +0000 |
560 | @@ -0,0 +1,176 @@ |
561 | +# Copyright (C) 2011 OpenStack LLC. |
562 | +# |
563 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
564 | +# you may not use this file except in compliance with the License. |
565 | +# You may obtain a copy of the License at |
566 | +# |
567 | +# http://www.apache.org/licenses/LICENSE-2.0 |
568 | +# |
569 | +# Unless required by applicable law or agreed to in writing, software |
570 | +# distributed under the License is distributed on an "AS IS" BASIS, |
571 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
572 | +# See the License for the specific language governing permissions and |
573 | +# limitations under the License. |
574 | + |
575 | +'''Memory backend for the burrow server.''' |
576 | + |
577 | +import time |
578 | + |
579 | +import burrowd.backend |
580 | + |
581 | + |
582 | +class Backend(burrowd.backend.Backend): |
583 | + |
584 | + def __init__(self, config): |
585 | + super(Backend, self).__init__(config) |
586 | + self.accounts = {} |
587 | + |
588 | + def delete_accounts(self): |
589 | + self.accounts.clear() |
590 | + |
591 | + def get_accounts(self): |
592 | + return self.accounts.keys() |
593 | + |
594 | + def delete_account(self, account): |
595 | + del self.accounts[account] |
596 | + |
597 | + def get_queues(self, account): |
598 | + if account not in self.accounts: |
599 | + return [] |
600 | + return self.accounts[account].keys() |
601 | + |
602 | + def queue_exists(self, account, queue): |
603 | + return account in self.accounts and queue in self.accounts[account] |
604 | + |
605 | + def delete_messages(self, account, queue, limit, marker, match_hidden): |
606 | + messages = self._scan_queue(account, queue, limit, marker, |
607 | + match_hidden, delete=True) |
608 | + if len(self.accounts[account][queue]) == 0: |
609 | + del self.accounts[account][queue] |
610 | + if len(self.accounts[account]) == 0: |
611 | + del self.accounts[account] |
612 | + return messages |
613 | + |
614 | + def get_messages(self, account, queue, limit, marker, match_hidden): |
615 | + return self._scan_queue(account, queue, limit, marker, match_hidden) |
616 | + |
617 | + def update_messages(self, account, queue, limit, marker, match_hidden, ttl, |
618 | + hide): |
619 | + return self._scan_queue(account, queue, limit, marker, match_hidden, |
620 | + ttl=ttl, hide=hide) |
621 | + |
622 | + def delete_message(self, account, queue, message_id): |
623 | + for index in range(0, len(self.accounts[account][queue])): |
624 | + message = self.accounts[account][queue][index] |
625 | + if message['id'] == message_id: |
626 | + del self.accounts[account][queue][index] |
627 | + if len(self.accounts[account][queue]) == 0: |
628 | + del self.accounts[account][queue] |
629 | + if len(self.accounts[account]) == 0: |
630 | + del self.accounts[account] |
631 | + return message |
632 | + return None |
633 | + |
634 | + def get_message(self, account, queue, message_id): |
635 | + for index in range(0, len(self.accounts[account][queue])): |
636 | + message = self.accounts[account][queue][index] |
637 | + if message['id'] == message_id: |
638 | + return message |
639 | + return None |
640 | + |
641 | + def put_message(self, account, queue, message_id, ttl, hide, body): |
642 | + if account not in self.accounts: |
643 | + self.accounts[account] = {} |
644 | + if queue not in self.accounts[account]: |
645 | + self.accounts[account][queue] = [] |
646 | + for index in range(0, len(self.accounts[account][queue])): |
647 | + message = self.accounts[account][queue][index] |
648 | + if message['id'] == message_id: |
649 | + message['ttl'] = ttl |
650 | + message['hide'] = hide |
651 | + message['body'] = body |
652 | + if hide == 0: |
653 | + self.notify(account, queue) |
654 | + return False |
655 | + message = dict(id=message_id, ttl=ttl, hide=hide, body=body) |
656 | + self.accounts[account][queue].append(message) |
657 | + self.notify(account, queue) |
658 | + return True |
659 | + |
660 | + def update_message(self, account, queue, message_id, ttl, hide): |
661 | + for index in range(0, len(self.accounts[account][queue])): |
662 | + message = self.accounts[account][queue][index] |
663 | + if message['id'] == message_id: |
664 | + if ttl is not None: |
665 | + message['ttl'] = ttl |
666 | + if hide is not None: |
667 | + message['hide'] = hide |
668 | + if hide == 0: |
669 | + self.notify(account, queue) |
670 | + return message |
671 | + return None |
672 | + |
673 | + def clean(self): |
674 | + now = int(time.time()) |
675 | + for account in self.accounts.keys(): |
676 | + for queue in self.accounts[account].keys(): |
677 | + index = 0 |
678 | + notify = False |
679 | + total = len(self.accounts[account][queue]) |
680 | + while index < total: |
681 | + message = self.accounts[account][queue][index] |
682 | + if 0 < message['ttl'] <= now: |
683 | + del self.accounts[account][queue][index] |
684 | + total -= 1 |
685 | + else: |
686 | + if 0 < message['hide'] <= now: |
687 | + message['hide'] = 0 |
688 | + notify = True |
689 | + index += 1 |
690 | + if notify: |
691 | + self.notify(account, queue) |
692 | + if len(self.accounts[account][queue]) == 0: |
693 | + del self.accounts[account][queue] |
694 | + if len(self.accounts[account]) == 0: |
695 | + del self.accounts[account] |
696 | + |
697 | + def _scan_queue(self, account, queue, limit, marker, match_hidden, |
698 | + ttl=None, hide=None, delete=False): |
699 | + index = 0 |
700 | + notify = False |
701 | + if marker is not None: |
702 | + found = False |
703 | + for index in range(0, len(self.accounts[account][queue])): |
704 | + message = self.accounts[account][queue][index] |
705 | + if message['id'] == marker: |
706 | + index += 1 |
707 | + found = True |
708 | + break |
709 | + if not found: |
710 | + index = 0 |
711 | + messages = [] |
712 | + total = len(self.accounts[account][queue]) |
713 | + while index < total: |
714 | + message = self.accounts[account][queue][index] |
715 | + if not match_hidden and message['hide'] != 0: |
716 | + index += 1 |
717 | + continue |
718 | + if ttl is not None: |
719 | + message['ttl'] = ttl |
720 | + if hide is not None: |
721 | + message['hide'] = hide |
722 | + if hide == 0: |
723 | + notify = True |
724 | + if delete: |
725 | + del self.accounts[account][queue][index] |
726 | + total -= 1 |
727 | + else: |
728 | + index += 1 |
729 | + messages.append(message) |
730 | + if limit: |
731 | + limit -= 1 |
732 | + if limit == 0: |
733 | + break |
734 | + if notify: |
735 | + self.notify(account, queue) |
736 | + return messages |
737 | |
738 | === added file 'burrowd/backend/sqlite.py' |
739 | --- burrowd/backend/sqlite.py 1970-01-01 00:00:00 +0000 |
740 | +++ burrowd/backend/sqlite.py 2011-03-17 23:47:24 +0000 |
741 | @@ -0,0 +1,250 @@ |
742 | +# Copyright (C) 2011 OpenStack LLC. |
743 | +# |
744 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
745 | +# you may not use this file except in compliance with the License. |
746 | +# You may obtain a copy of the License at |
747 | +# |
748 | +# http://www.apache.org/licenses/LICENSE-2.0 |
749 | +# |
750 | +# Unless required by applicable law or agreed to in writing, software |
751 | +# distributed under the License is distributed on an "AS IS" BASIS, |
752 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
753 | +# See the License for the specific language governing permissions and |
754 | +# limitations under the License. |
755 | + |
756 | +'''Memory backend for the burrow server.''' |
757 | + |
758 | +import sqlite3 |
759 | +import time |
760 | + |
761 | +import burrowd.backend |
762 | + |
763 | +# Default configuration values for this module. |
764 | +DEFAULT_DATABASE = ':memory:' |
765 | + |
766 | + |
767 | +class Backend(burrowd.backend.Backend): |
768 | + |
769 | + def __init__(self, config): |
770 | + super(Backend, self).__init__(config) |
771 | + database = self.config.get('database', DEFAULT_DATABASE) |
772 | + self.db = sqlite3.connect(database) |
773 | + queries = [ |
774 | + 'CREATE TABLE queues (' |
775 | + 'account VARCHAR(255) NOT NULL,' |
776 | + 'queue VARCHAR(255) NOT NULL,' |
777 | + 'PRIMARY KEY (account, queue))', |
778 | + 'CREATE TABLE messages (' |
779 | + 'queue INT UNSIGNED NOT NULL,' |
780 | + 'name VARCHAR(255) NOT NULL,' |
781 | + 'ttl INT UNSIGNED NOT NULL,' |
782 | + 'hide INT UNSIGNED NOT NULL,' |
783 | + 'body BLOB NOT NULL,' |
784 | + 'PRIMARY KEY (queue, name))'] |
785 | + for query in queries: |
786 | + self.db.execute(query) |
787 | + |
788 | + def delete_accounts(self): |
789 | + self.db.execute("DELETE FROM queues") |
790 | + self.db.execute("DELETE FROM messages") |
791 | + |
792 | + def get_accounts(self): |
793 | + result = self.db.execute("SELECT account FROM queues").fetchall() |
794 | + return [row[0] for row in result] |
795 | + |
796 | + def delete_account(self, account): |
797 | + query = "SELECT rowid FROM queues WHERE account='%s'" % account |
798 | + result = self.db.execute(query).fetchall() |
799 | + if len(result) == 0: |
800 | + return |
801 | + queues = [str(queue[0]) for queue in result] |
802 | + query = "DELETE FROM messages WHERE queue IN (%s)" % (','.join(queues)) |
803 | + self.db.execute(query) |
804 | + self.db.execute("DELETE FROM queues WHERE account='%s'" % account) |
805 | + |
806 | + def get_queues(self, account): |
807 | + query = "SELECT queue FROM queues WHERE account='%s'" % account |
808 | + result = self.db.execute(query).fetchall() |
809 | + return [row[0] for row in result] |
810 | + |
811 | + def queue_exists(self, account, queue): |
812 | + query = "SELECT COUNT(*) FROM queues " \ |
813 | + "WHERE account='%s' AND queue='%s'" % \ |
814 | + (account, queue) |
815 | + result = self.db.execute(query).fetchall() |
816 | + if len(result) == 0: |
817 | + return False |
818 | + self.rowid = result[0][0] |
819 | + return True |
820 | + |
821 | + def delete_messages(self, account, queue, limit, marker, match_hidden): |
822 | + messages = self.get_messages(account, queue, limit, marker, |
823 | + match_hidden) |
824 | + ids = [message['id'] for message in messages] |
825 | + query = "DELETE FROM messages WHERE queue=%d AND name IN (%s)" % \ |
826 | + (self.rowid, ','.join(ids)) |
827 | + self.db.execute(query) |
828 | + query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ |
829 | + self.rowid |
830 | + if len(self.db.execute(query).fetchall()) == 0: |
831 | + query = "DELETE FROM queues WHERE rowid=%d" % self.rowid |
832 | + self.db.execute(query) |
833 | + return messages |
834 | + |
835 | + def get_messages(self, account, queue, limit, marker, match_hidden): |
836 | + if marker is not None: |
837 | + query = "SELECT rowid FROM messages " \ |
838 | + "WHERE queue=%d AND name='%s'" % \ |
839 | + (self.rowid, marker) |
840 | + result = self.db.execute(query).fetchall() |
841 | + if len(result) == 0: |
842 | + marker = None |
843 | + else: |
844 | + marker = result[0][0] |
845 | + query = "SELECT name,ttl,hide,body FROM messages WHERE queue=%d" % \ |
846 | + self.rowid |
847 | + if match_hidden is False: |
848 | + query += " AND hide == 0" |
849 | + if marker is not None: |
850 | + query += " AND rowid > %d" % marker |
851 | + if limit is not None: |
852 | + query += " LIMIT %d" % limit |
853 | + result = self.db.execute(query).fetchall() |
854 | + messages = [] |
855 | + for row in result: |
856 | + messages.append(dict(id=row[0], ttl=row[1], hide=row[2], |
857 | + body=row[3])) |
858 | + return messages |
859 | + |
860 | + def update_messages(self, account, queue, limit, marker, match_hidden, ttl, |
861 | + hide): |
862 | + messages = self.get_messages(account, queue, limit, marker, |
863 | + match_hidden) |
864 | + query = "UPDATE messages SET" |
865 | + comma = '' |
866 | + if ttl is not None: |
867 | + query += "%s ttl=%d" % (comma, ttl) |
868 | + comma = ',' |
869 | + if hide is not None: |
870 | + query += "%s hide=%d" % (comma, hide) |
871 | + comma = ',' |
872 | + if comma == '': |
873 | + return (False, message) |
874 | + ids = [] |
875 | + for message in messages: |
876 | + ids.append(message['id']) |
877 | + if ttl is not None: |
878 | + message['ttl'] = ttl |
879 | + if hide is not None: |
880 | + message['hide'] = hide |
881 | + query += " WHERE queue=%d AND name IN (%s)" % \ |
882 | + (self.rowid, ','.join(ids)) |
883 | + self.db.execute(query) |
884 | + self.notify(account, queue) |
885 | + return messages |
886 | + |
887 | + def delete_message(self, account, queue, message_id): |
888 | + message = self.get_message(account, queue, message_id) |
889 | + if message is None: |
890 | + return None |
891 | + query = "DELETE FROM messages WHERE queue=%d AND name='%s'" % \ |
892 | + (self.rowid, message_id) |
893 | + self.db.execute(query) |
894 | + query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ |
895 | + self.rowid |
896 | + if len(self.db.execute(query).fetchall()) == 0: |
897 | + query = "DELETE FROM queues WHERE rowid=%d" % self.rowid |
898 | + self.db.execute(query) |
899 | + return message |
900 | + |
901 | + def get_message(self, account, queue, message_id): |
902 | + query = "SELECT name,ttl,hide,body FROM messages " \ |
903 | + "WHERE queue=%d AND name='%s'" % (self.rowid, message_id) |
904 | + result = self.db.execute(query).fetchall() |
905 | + if len(result) == 0: |
906 | + return None |
907 | + row = result[0] |
908 | + return dict(id=row[0], ttl=row[1], hide=row[2], body=row[3]) |
909 | + |
910 | + def put_message(self, account, queue, message_id, ttl, hide, body): |
911 | + query = "SELECT rowid FROM queues " \ |
912 | + "WHERE account='%s' AND queue='%s'" % (account, queue) |
913 | + result = self.db.execute(query).fetchall() |
914 | + if len(result) == 0: |
915 | + query = "INSERT INTO queues VALUES ('%s', '%s')" % (account, queue) |
916 | + rowid = self.db.execute(query).lastrowid |
917 | + else: |
918 | + rowid = result[0][0] |
919 | + query = "SELECT rowid FROM messages WHERE queue=%d AND name='%s'" % \ |
920 | + (rowid, message_id) |
921 | + result = self.db.execute(query).fetchall() |
922 | + if len(result) == 0: |
923 | + query = "INSERT INTO messages VALUES (%d, '%s', %d, %d, '%s')" % \ |
924 | + (rowid, message_id, ttl, hide, body) |
925 | + self.db.execute(query) |
926 | + self.notify(account, queue) |
927 | + return True |
928 | + query = "UPDATE messages SET ttl=%d, hide=%d, body='%s'" \ |
929 | + "WHERE rowid=%d" % (ttl, hide, body, result[0][0]) |
930 | + self.db.execute(query) |
931 | + if hide == 0: |
932 | + self.notify(account, queue) |
933 | + return False |
934 | + |
935 | + def update_message(self, account, queue, message_id, ttl, hide): |
936 | + message = self.get_message(account, queue, message_id) |
937 | + if message is None: |
938 | + return None |
939 | + query = "UPDATE messages SET" |
940 | + comma = '' |
941 | + if ttl is not None: |
942 | + query += "%s ttl=%d" % (comma, ttl) |
943 | + comma = ',' |
944 | + if hide is not None: |
945 | + query += "%s hide=%d" % (comma, hide) |
946 | + comma = ',' |
947 | + if comma == '': |
948 | + return message |
949 | + query += " WHERE queue=%d AND name='%s'" % (self.rowid, message_id) |
950 | + self.db.execute(query) |
951 | + if hide == 0: |
952 | + self.notify(account, queue) |
953 | + return message |
954 | + |
955 | + def clean(self): |
956 | + now = int(time.time()) |
957 | + query = "SELECT rowid,queue FROM messages " \ |
958 | + "WHERE ttl > 0 AND ttl <= %d" % now |
959 | + result = self.db.execute(query).fetchall() |
960 | + if len(result) > 0: |
961 | + messages = [] |
962 | + queues = [] |
963 | + for row in result: |
964 | + messages.append(str(row[0])) |
965 | + queues.append(row[1]) |
966 | + query = 'DELETE FROM messages WHERE rowid in (%s)' % \ |
967 | + ','.join(messages) |
968 | + self.db.execute(query) |
969 | + for queue in queues: |
970 | + query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ |
971 | + queue |
972 | + if len(self.db.execute(query).fetchall()) == 0: |
973 | + query = "DELETE FROM queues WHERE rowid=%d" % queue |
974 | + self.db.execute(query) |
975 | + query = "SELECT rowid,queue FROM messages WHERE " \ |
976 | + "hide > 0 AND hide <= %d" % now |
977 | + result = self.db.execute(query).fetchall() |
978 | + if len(result) > 0: |
979 | + messages = [] |
980 | + queues = [] |
981 | + for row in result: |
982 | + messages.append(str(row[0])) |
983 | + queues.append(row[1]) |
984 | + query = 'UPDATE messages SET hide=0 WHERE rowid in (%s)' % \ |
985 | + ','.join(messages) |
986 | + self.db.execute(query) |
987 | + for queue in queues: |
988 | + query = "SELECT account,queue FROM queues WHERE rowid=%d" % \ |
989 | + queue |
990 | + result = self.db.execute(query).fetchall()[0] |
991 | + self.notify(result[0], result[1]) |
992 | |
993 | === added file 'burrowd/config.py' |
994 | --- burrowd/config.py 1970-01-01 00:00:00 +0000 |
995 | +++ burrowd/config.py 2011-03-17 23:47:24 +0000 |
996 | @@ -0,0 +1,55 @@ |
997 | +# Copyright (C) 2011 OpenStack LLC. |
998 | +# |
999 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1000 | +# you may not use this file except in compliance with the License. |
1001 | +# You may obtain a copy of the License at |
1002 | +# |
1003 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1004 | +# |
1005 | +# Unless required by applicable law or agreed to in writing, software |
1006 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1007 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1008 | +# See the License for the specific language governing permissions and |
1009 | +# limitations under the License. |
1010 | + |
1011 | +'''Configuration module for the burrow server.''' |
1012 | + |
1013 | +import ConfigParser |
1014 | + |
1015 | + |
1016 | +class Config(object): |
1017 | + '''Configuration class that wraps the ConfigParser get* |
1018 | + methods. These wrappers automatically check for options in |
1019 | + a specific instance section first before the regular section |
1020 | + (section:instance and then section). They will also return a |
1021 | + default value if given instead of throwing an exception.''' |
1022 | + |
1023 | + def __init__(self, config, section, instance=None): |
1024 | + self.config = config |
1025 | + self.section = section |
1026 | + if instance is None: |
1027 | + self.instance = None |
1028 | + else: |
1029 | + self.instance = '%s:%s' % (section, instance) |
1030 | + |
1031 | + def get(self, option, default=None): |
1032 | + return self._get(self.config.get, option, default) |
1033 | + |
1034 | + def getboolean(self, option, default=None): |
1035 | + return self._get(self.config.getboolean, option, default) |
1036 | + |
1037 | + def getfloat(self, option, default=None): |
1038 | + return self._get(self.config.getfloat, option, default) |
1039 | + |
1040 | + def getint(self, option, default=None): |
1041 | + return self._get(self.config.getint, option, default) |
1042 | + |
1043 | + def _get(self, method, option, default): |
1044 | + if self.instance is not None: |
1045 | + if self.config.has_option(self.instance, option): |
1046 | + return method(self.instance, option) |
1047 | + if self.config.has_option(self.section, option): |
1048 | + return method(self.section, option) |
1049 | + if self.config.has_option(ConfigParser.DEFAULTSECT, option): |
1050 | + return method(ConfigParser.DEFAULTSECT, option) |
1051 | + return default |
1052 | |
1053 | === added directory 'burrowd/frontend' |
1054 | === added file 'burrowd/frontend/__init__.py' |
1055 | --- burrowd/frontend/__init__.py 1970-01-01 00:00:00 +0000 |
1056 | +++ burrowd/frontend/__init__.py 2011-03-17 23:47:24 +0000 |
1057 | @@ -0,0 +1,30 @@ |
1058 | +# Copyright (C) 2011 OpenStack LLC. |
1059 | +# |
1060 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1061 | +# you may not use this file except in compliance with the License. |
1062 | +# You may obtain a copy of the License at |
1063 | +# |
1064 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1065 | +# |
1066 | +# Unless required by applicable law or agreed to in writing, software |
1067 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1068 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1069 | +# See the License for the specific language governing permissions and |
1070 | +# limitations under the License. |
1071 | + |
1072 | +'''Frontends for the burrow server.''' |
1073 | + |
1074 | +import burrowd |
1075 | + |
1076 | + |
1077 | +class Frontend(burrowd.Module): |
1078 | + '''Interface that frontend implementations must provide.''' |
1079 | + |
1080 | + def __init__(self, config, backend): |
1081 | + super(Frontend, self).__init__(config) |
1082 | + self.backend = backend |
1083 | + |
1084 | + def run(self, thread_pool): |
1085 | + '''Run the frontend instance, adding any threads to the |
1086 | + thread_pool if needed.''' |
1087 | + pass |
1088 | |
1089 | === added file 'burrowd/frontend/wsgi.py' |
1090 | --- burrowd/frontend/wsgi.py 1970-01-01 00:00:00 +0000 |
1091 | +++ burrowd/frontend/wsgi.py 2011-03-17 23:47:24 +0000 |
1092 | @@ -0,0 +1,276 @@ |
1093 | +# Copyright (C) 2011 OpenStack LLC. |
1094 | +# |
1095 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1096 | +# you may not use this file except in compliance with the License. |
1097 | +# You may obtain a copy of the License at |
1098 | +# |
1099 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1100 | +# |
1101 | +# Unless required by applicable law or agreed to in writing, software |
1102 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1103 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1104 | +# See the License for the specific language governing permissions and |
1105 | +# limitations under the License. |
1106 | + |
1107 | +'''WSGI frontend for the burrow server.''' |
1108 | + |
1109 | +import json |
1110 | +import time |
1111 | + |
1112 | +import eventlet |
1113 | +import eventlet.wsgi |
1114 | +import routes |
1115 | +import routes.middleware |
1116 | +import webob.dec |
1117 | +import webob.exc |
1118 | + |
1119 | +import burrowd.frontend |
1120 | + |
1121 | +# Default configuration values for this module. |
1122 | +DEFAULT_HOST = '0.0.0.0' |
1123 | +DEFAULT_PORT = 8080 |
1124 | +DEFAULT_BACKLOG = 64 |
1125 | +DEFAULT_SSL = False |
1126 | +DEFAULT_SSL_CERTFILE = 'example.pem' |
1127 | +DEFAULT_SSL_KEYFILE = 'example.key' |
1128 | +DEFAULT_THREAD_POOL_SIZE = 0 |
1129 | +DEFAULT_TTL = 600 |
1130 | +DEFAULT_HIDE = 0 |
1131 | + |
1132 | + |
1133 | +def queue_exists(method): |
1134 | + '''Decorator to ensure an account and queue exists. If the wait |
1135 | + option is given, this will block until a message in the queue is |
1136 | + ready or the timeout expires.''' |
1137 | + def wrapper(self, req, account, queue, *args, **kwargs): |
1138 | + wait = 0 |
1139 | + if 'wait' in req.params: |
1140 | + wait = int(req.params['wait']) |
1141 | + if wait > 0: |
1142 | + wait += time.time() |
1143 | + res = webob.exc.HTTPNotFound() |
1144 | + while True: |
1145 | + if self.backend.queue_exists(account, queue): |
1146 | + res = method(self, req, account, queue, *args, **kwargs) |
1147 | + if wait == 0 or res.status_int != 404: |
1148 | + break |
1149 | + now = time.time() |
1150 | + if wait - now > 0: |
1151 | + self.backend.wait(account, queue, wait - now) |
1152 | + if wait < time.time(): |
1153 | + break |
1154 | + return res |
1155 | + return wrapper |
1156 | + |
1157 | + |
1158 | +class Frontend(burrowd.frontend.Frontend): |
1159 | + |
1160 | + def __init__(self, config, backend): |
1161 | + super(Frontend, self).__init__(config, backend) |
1162 | + self.default_ttl = self.config.get('default_ttl', DEFAULT_TTL) |
1163 | + self.default_hide = self.config.get('default_hide', DEFAULT_HIDE) |
1164 | + mapper = routes.Mapper() |
1165 | + mapper.connect('/', action='root') |
1166 | + mapper.connect('/{account}', action='account') |
1167 | + mapper.connect('/{account}/{queue}', action='queue') |
1168 | + mapper.connect('/{account}/{queue}/{message_id}', action='message') |
1169 | + self._routes = routes.middleware.RoutesMiddleware(self._route, mapper) |
1170 | + |
1171 | + def run(self, thread_pool): |
1172 | + '''Create the listening socket and start the thread that runs |
1173 | + the WSGI server. This extra thread is needed since the WSGI |
1174 | + server function blocks.''' |
1175 | + host = self.config.get('host', DEFAULT_HOST) |
1176 | + port = self.config.getint('port', DEFAULT_PORT) |
1177 | + backlog = self.config.getint('backlog', DEFAULT_BACKLOG) |
1178 | + socket = eventlet.listen((host, port), backlog=backlog) |
1179 | + self.log.info(_('Listening on %s:%d') % (host, port)) |
1180 | + if self.config.getboolean('ssl', DEFAULT_SSL): |
1181 | + certfile = self.config.get('ssl_certfile', DEFAULT_SSL_CERTFILE) |
1182 | + keyfile = self.config.get('ssl_keyfile', DEFAULT_SSL_KEYFILE) |
1183 | + socket = eventlet.green.ssl.wrap_socket(socket, certfile=certfile, |
1184 | + keyfile=keyfile) |
1185 | + thread_pool.spawn_n(self._run, socket, thread_pool) |
1186 | + |
1187 | + def _run(self, socket, thread_pool): |
1188 | + '''Thread to run the WSGI server.''' |
1189 | + thread_pool_size = self.config.getint('thread_pool_size', |
1190 | + DEFAULT_THREAD_POOL_SIZE) |
1191 | + log_format = '%(client_ip)s "%(request_line)s" %(status_code)s ' \ |
1192 | + '%(body_length)s %(wall_seconds).6f' |
1193 | + if thread_pool_size == 0: |
1194 | + eventlet.wsgi.server(socket, self, log=WSGILog(self.log), |
1195 | + log_format=log_format, custom_pool=thread_pool) |
1196 | + else: |
1197 | + eventlet.wsgi.server(socket, self, log=WSGILog(self.log), |
1198 | + log_format=log_format, max_size=thread_pool_size) |
1199 | + |
1200 | + def __call__(self, *args, **kwargs): |
1201 | + return self._routes(*args, **kwargs) |
1202 | + |
1203 | + @webob.dec.wsgify |
1204 | + def _route(self, req): |
1205 | + args = req.environ['wsgiorg.routing_args'][1] |
1206 | + if not args: |
1207 | + return webob.exc.HTTPNotFound() |
1208 | + action = args.pop('action') |
1209 | + method = getattr(self, '_%s_%s' % (req.method.lower(), action), False) |
1210 | + if not method: |
1211 | + return webob.exc.HTTPBadRequest() |
1212 | + return method(req, **args) |
1213 | + |
1214 | + @webob.dec.wsgify |
1215 | + def _delete_root(self, req): |
1216 | + self.backend.delete_accounts() |
1217 | + return webob.exc.HTTPNoContent() |
1218 | + |
1219 | + @webob.dec.wsgify |
1220 | + def _get_root(self, req): |
1221 | + accounts = self.backend.get_accounts() |
1222 | + if len(accounts) == 0: |
1223 | + return webob.exc.HTTPNotFound() |
1224 | + return webob.exc.HTTPOk(body=json.dumps(accounts, indent=2)) |
1225 | + |
1226 | + @webob.dec.wsgify |
1227 | + def _delete_account(self, req, account): |
1228 | + self.backend.delete_account(account) |
1229 | + return webob.exc.HTTPNoContent() |
1230 | + |
1231 | + @webob.dec.wsgify |
1232 | + def _get_account(self, req, account): |
1233 | + queues = self.backend.get_queues(account) |
1234 | + if len(queues) == 0: |
1235 | + return webob.exc.HTTPNotFound() |
1236 | + return webob.exc.HTTPOk(body=json.dumps(queues, indent=2)) |
1237 | + |
1238 | + @webob.dec.wsgify |
1239 | + @queue_exists |
1240 | + def _delete_queue(self, req, account, queue): |
1241 | + limit, marker, match_hidden = self._parse_filters(req) |
1242 | + messages = self.backend.delete_messages(account, queue, limit, marker, |
1243 | + match_hidden) |
1244 | + return self._return_messages(req, account, queue, messages, 'none') |
1245 | + |
1246 | + @webob.dec.wsgify |
1247 | + @queue_exists |
1248 | + def _get_queue(self, req, account, queue): |
1249 | + limit, marker, match_hidden = self._parse_filters(req) |
1250 | + messages = self.backend.get_messages(account, queue, limit, marker, |
1251 | + match_hidden) |
1252 | + return self._return_messages(req, account, queue, messages, 'all') |
1253 | + |
1254 | + @webob.dec.wsgify |
1255 | + @queue_exists |
1256 | + def _post_queue(self, req, account, queue): |
1257 | + limit, marker, match_hidden = self._parse_filters(req) |
1258 | + ttl, hide = self._parse_metadata(req) |
1259 | + messages = self.backend.update_messages(account, queue, limit, marker, |
1260 | + match_hidden, ttl, hide) |
1261 | + return self._return_messages(req, account, queue, messages, 'all') |
1262 | + |
1263 | + @webob.dec.wsgify |
1264 | + @queue_exists |
1265 | + def _delete_message(self, req, account, queue, message_id): |
1266 | + message = self.backend.delete_message(account, queue, message_id) |
1267 | + if message is None: |
1268 | + return webob.exc.HTTPNotFound() |
1269 | + return self._return_message(req, account, queue, message, 'none') |
1270 | + |
1271 | + @webob.dec.wsgify |
1272 | + @queue_exists |
1273 | + def _get_message(self, req, account, queue, message_id): |
1274 | + message = self.backend.get_message(account, queue, message_id) |
1275 | + if message is None: |
1276 | + return webob.exc.HTTPNotFound() |
1277 | + return self._return_message(req, account, queue, message, 'all') |
1278 | + |
1279 | + @webob.dec.wsgify |
1280 | + @queue_exists |
1281 | + def _post_message(self, req, account, queue, message_id): |
1282 | + ttl, hide = self._parse_metadata(req) |
1283 | + message = self.backend.update_message(account, queue, message_id, ttl, |
1284 | + hide) |
1285 | + if message is None: |
1286 | + return webob.exc.HTTPNotFound() |
1287 | + return self._return_message(req, account, queue, message, 'id') |
1288 | + |
1289 | + @webob.dec.wsgify |
1290 | + def _put_message(self, req, account, queue, message_id): |
1291 | + (ttl, hide) = self._parse_metadata(req, self.default_ttl, |
1292 | + self.default_hide) |
1293 | + if self.backend.put_message(account, queue, message_id, ttl, hide, \ |
1294 | + req.body): |
1295 | + return webob.exc.HTTPCreated() |
1296 | + return webob.exc.HTTPNoContent() |
1297 | + |
1298 | + def _filter_message(self, detail, message): |
1299 | + if detail == 'id': |
1300 | + return dict(id=message['id']) |
1301 | + elif detail == 'metadata': |
1302 | + message = message.copy() |
1303 | + del message['body'] |
1304 | + return message |
1305 | + elif detail == 'all': |
1306 | + return message |
1307 | + return None |
1308 | + |
1309 | + def _return_message(self, req, account, queue, message, detail): |
1310 | + if 'detail' in req.params: |
1311 | + detail = req.params['detail'] |
1312 | + message = self._filter_message(detail, message) |
1313 | + if message is not None: |
1314 | + body = {account: {queue: [message]}} |
1315 | + return webob.exc.HTTPOk(body=json.dumps(body, indent=2)) |
1316 | + return webob.exc.HTTPNoContent() |
1317 | + |
1318 | + def _return_messages(self, req, account, queue, messages, detail): |
1319 | + if len(messages) == 0: |
1320 | + return webob.exc.HTTPNotFound() |
1321 | + if 'detail' in req.params: |
1322 | + detail = req.params['detail'] |
1323 | + filtered_messages = [] |
1324 | + for message in messages: |
1325 | + message = self._filter_message(detail, message) |
1326 | + if message is not None: |
1327 | + filtered_messages.append(message) |
1328 | + if len(filtered_messages) == 0: |
1329 | + return webob.exc.HTTPNoContent() |
1330 | + body = {account: {queue: filtered_messages}} |
1331 | + return webob.exc.HTTPOk(body=json.dumps(body, indent=2)) |
1332 | + |
1333 | + def _parse_filters(self, req): |
1334 | + limit = None |
1335 | + if 'limit' in req.params: |
1336 | + limit = int(req.params['limit']) |
1337 | + marker = None |
1338 | + if 'marker' in req.params: |
1339 | + marker = req.params['marker'] |
1340 | + match_hidden = False |
1341 | + if 'hidden' in req.params and req.params['hidden'].lower() == 'true': |
1342 | + match_hidden = True |
1343 | + return limit, marker, match_hidden |
1344 | + |
1345 | + def _parse_metadata(self, req, default_ttl=None, default_hide=None): |
1346 | + if 'ttl' in req.params: |
1347 | + ttl = int(req.params['ttl']) |
1348 | + else: |
1349 | + ttl = default_ttl |
1350 | + if ttl is not None and ttl > 0: |
1351 | + ttl += int(time.time()) |
1352 | + if 'hide' in req.params: |
1353 | + hide = int(req.params['hide']) |
1354 | + else: |
1355 | + hide = default_hide |
1356 | + if hide is not None and hide > 0: |
1357 | + hide += int(time.time()) |
1358 | + return ttl, hide |
1359 | + |
1360 | + |
1361 | +class WSGILog(object): |
1362 | + '''Class for eventlet.wsgi.server to forward logging messages.''' |
1363 | + |
1364 | + def __init__(self, log): |
1365 | + self.log = log |
1366 | + |
1367 | + def write(self, message): |
1368 | + self.log.debug(message.rstrip()) |
1369 | |
1370 | === added directory 'etc' |
1371 | === added file 'etc/burrowd.conf' |
1372 | --- etc/burrowd.conf 1970-01-01 00:00:00 +0000 |
1373 | +++ etc/burrowd.conf 2011-03-17 23:47:24 +0000 |
1374 | @@ -0,0 +1,99 @@ |
1375 | +[DEFAULT] |
1376 | + |
1377 | +# Log level to use. All sections below prefixed with 'burrowd' can define |
1378 | +# this to override this default. |
1379 | +log_level = DEBUG |
1380 | + |
1381 | +# Default expiration time in seconds to set for messages. |
1382 | +default_ttl = 600 |
1383 | + |
1384 | +# Default hide time in seconds to set for messages. |
1385 | +default_hide = 0 |
1386 | + |
1387 | + |
1388 | +[burrowd] |
1389 | + |
1390 | +# Backend to use for storing messages. |
1391 | +backend = burrowd.backend.sqlite |
1392 | + |
1393 | +# Comma separated list of frontends to run. |
1394 | +# frontends = burrowd.frontend.wsgi,burrowd.frontend.wsgi:ssl |
1395 | +frontends = burrowd.frontend.wsgi |
1396 | + |
1397 | +# Size of the thread pool to use for the server. |
1398 | +thread_pool_size = 1000 |
1399 | + |
1400 | + |
1401 | +[burrowd.backend.sqlite] |
1402 | + |
1403 | +# Database file to use, passed to sqlite3.connect. |
1404 | +database = :memory: |
1405 | + |
1406 | + |
1407 | +[burrowd.frontend.wsgi] |
1408 | + |
1409 | +# Host to listen on. |
1410 | +host = 0.0.0.0 |
1411 | + |
1412 | +# Port to listen on. |
1413 | +port = 8080 |
1414 | + |
1415 | +# Size of backlog for listener socket. |
1416 | +backlog = 64 |
1417 | + |
1418 | +# Whether to enable SSL. |
1419 | +ssl = False |
1420 | + |
1421 | +# If SSL is enabled, which certfile to use. |
1422 | +ssl_certfile = example.pem |
1423 | + |
1424 | +# If SSL is enabled, which keyfile to use. |
1425 | +ssl_keyfile = example.key |
1426 | + |
1427 | +# Size of thread pool for the WSGI server. If the size is 0, use the main |
1428 | +# burrowd thread pool. |
1429 | +thread_pool_size = 0 |
1430 | + |
1431 | +# Default expiration time in seconds to set for messages. This overrides |
1432 | +# the value in the DEFAULT section. |
1433 | +# default_ttl = 600 |
1434 | + |
1435 | +# Default hide time in seconds to set for messages. This overrides the |
1436 | +# value in the DEFAULT section. |
1437 | +# default_hide = 0 |
1438 | + |
1439 | + |
1440 | +[burrowd.frontend.wsgi:ssl] |
1441 | + |
1442 | +# Port to listen on. |
1443 | +port = 8443 |
1444 | + |
1445 | +# Whether to enable SSL. |
1446 | +ssl = True |
1447 | + |
1448 | + |
1449 | +# Logging configuration following the logging.config format. |
1450 | + |
1451 | +[loggers] |
1452 | +keys=root |
1453 | + |
1454 | +[logger_root] |
1455 | +qualname=root |
1456 | +level=WARNING |
1457 | +handlers=console |
1458 | + |
1459 | +[handlers] |
1460 | +keys=console |
1461 | + |
1462 | +[handler_console] |
1463 | +class=StreamHandler |
1464 | +level=DEBUG |
1465 | +formatter=simple |
1466 | +args=(sys.stdout,) |
1467 | + |
1468 | +[formatters] |
1469 | +keys=simple |
1470 | + |
1471 | +[formatter_simple] |
1472 | +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s |
1473 | +datefmt= |
1474 | |
1475 | === added file 'setup.py' |
1476 | --- setup.py 1970-01-01 00:00:00 +0000 |
1477 | +++ setup.py 2011-03-17 23:47:24 +0000 |
1478 | @@ -0,0 +1,74 @@ |
1479 | +#!/usr/bin/python |
1480 | +# Copyright (C) 2011 OpenStack LLC. |
1481 | +# |
1482 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1483 | +# you may not use this file except in compliance with the License. |
1484 | +# You may obtain a copy of the License at |
1485 | +# |
1486 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1487 | +# |
1488 | +# Unless required by applicable law or agreed to in writing, software |
1489 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1490 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1491 | +# See the License for the specific language governing permissions and |
1492 | +# limitations under the License. |
1493 | + |
1494 | +from setuptools import setup, find_packages |
1495 | +from setuptools.command.sdist import sdist |
1496 | +import os |
1497 | +import subprocess |
1498 | +try: |
1499 | + from babel.messages import frontend |
1500 | +except ImportError: |
1501 | + frontend = None |
1502 | + |
1503 | + |
1504 | +class local_sdist(sdist): |
1505 | + """Customized sdist hook - builds the ChangeLog file from VC first""" |
1506 | + |
1507 | + def run(self): |
1508 | + if os.path.isdir('.bzr'): |
1509 | + # We're in a bzr branch |
1510 | + |
1511 | + log_cmd = subprocess.Popen(["bzr", "log", "--gnu"], |
1512 | + stdout=subprocess.PIPE) |
1513 | + changelog = log_cmd.communicate()[0] |
1514 | + with open("ChangeLog", "w") as changelog_file: |
1515 | + changelog_file.write(changelog) |
1516 | + sdist.run(self) |
1517 | + |
1518 | + |
1519 | +name = 'burrow' |
1520 | + |
1521 | + |
1522 | +cmdclass = {'sdist': local_sdist} |
1523 | + |
1524 | + |
1525 | +if frontend: |
1526 | + cmdclass.update({ |
1527 | + 'compile_catalog': frontend.compile_catalog, |
1528 | + 'extract_messages': frontend.extract_messages, |
1529 | + 'init_catalog': frontend.init_catalog, |
1530 | + 'update_catalog': frontend.update_catalog}) |
1531 | + |
1532 | + |
1533 | +setup( |
1534 | + name=name, |
1535 | + version='0.1', |
1536 | + description='Burrow', |
1537 | + license='Apache License (2.0)', |
1538 | + author='OpenStack, LLC.', |
1539 | + author_email='openstack-admins@lists.launchpad.net', |
1540 | + url='https://launchpad.net/burrow', |
1541 | + packages=find_packages(exclude=['test', 'bin']), |
1542 | + test_suite='nose.collector', |
1543 | + cmdclass=cmdclass, |
1544 | + classifiers=[ |
1545 | + 'Development Status :: 3 - Alpha', |
1546 | + 'License :: OSI Approved :: Apache Software License', |
1547 | + 'Operating System :: POSIX :: Linux', |
1548 | + 'Programming Language :: Python :: 2.6', |
1549 | + 'Environment :: No Input/Output (Daemon)'], |
1550 | + scripts=[ |
1551 | + 'bin/burrow', |
1552 | + 'bin/burrowd']) |
1553 | |
1554 | === added directory 'test' |
1555 | === added file 'test/__init__.py' |
1556 | === added directory 'test/frontend' |
1557 | === added file 'test/frontend/__init__.py' |
1558 | === added file 'test/frontend/test_wsgi.py' |
1559 | --- test/frontend/test_wsgi.py 1970-01-01 00:00:00 +0000 |
1560 | +++ test/frontend/test_wsgi.py 2011-03-17 23:47:24 +0000 |
1561 | @@ -0,0 +1,313 @@ |
1562 | +# Copyright (C) 2011 OpenStack LLC. |
1563 | +# |
1564 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1565 | +# you may not use this file except in compliance with the License. |
1566 | +# You may obtain a copy of the License at |
1567 | +# |
1568 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1569 | +# |
1570 | +# Unless required by applicable law or agreed to in writing, software |
1571 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1572 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1573 | +# See the License for the specific language governing permissions and |
1574 | +# limitations under the License. |
1575 | + |
1576 | +import ConfigParser |
1577 | +import json |
1578 | +import time |
1579 | +import unittest |
1580 | + |
1581 | +import eventlet |
1582 | +import webob |
1583 | + |
1584 | +import burrowd.backend.memory |
1585 | +import burrowd.backend.sqlite |
1586 | +import burrowd.frontend.wsgi |
1587 | + |
1588 | + |
1589 | +class TestWSGIMemory(unittest.TestCase): |
1590 | + '''Unittests for the WSGI frontend to SQLite backend.''' |
1591 | + backend_class = burrowd.backend.memory.Backend |
1592 | + |
1593 | + def setUp(self): |
1594 | + config = (ConfigParser.ConfigParser(), 'test') |
1595 | + self.backend = self.backend_class(config) |
1596 | + self.frontend = burrowd.frontend.wsgi.Frontend(config, self.backend) |
1597 | + self.frontend.default_ttl = 0 |
1598 | + self._get_url('/', status=404) |
1599 | + self._get_url('/a', status=404) |
1600 | + self._get_url('/a/q', status=404) |
1601 | + |
1602 | + def tearDown(self): |
1603 | + self._get_url('/a/q', status=404) |
1604 | + self._get_url('/a', status=404) |
1605 | + self._get_url('/', status=404) |
1606 | + |
1607 | + def test_account(self): |
1608 | + self._put_url('/a/q/1') |
1609 | + accounts = self._get_url('/') |
1610 | + self.assertEquals(accounts, ['a']) |
1611 | + self._delete_url('/a') |
1612 | + |
1613 | + def test_queue(self): |
1614 | + self._put_url('/a/q/1') |
1615 | + accounts = self._get_url('/a') |
1616 | + self.assertEquals(accounts, ['q']) |
1617 | + self._delete_url('/a/q') |
1618 | + |
1619 | + def test_message(self): |
1620 | + self._put_url('/a/q/1', body='b') |
1621 | + accounts = self._get_url('/a/q') |
1622 | + self.assertMessages(accounts, 'a', 'q', [self.message('1', body='b')]) |
1623 | + self._delete_url('/a/q/1') |
1624 | + |
1625 | + def test_message_post(self): |
1626 | + self._put_url('/a/q/1', body='b') |
1627 | + for x in range(0, 3): |
1628 | + accounts = self._post_url('/a/q/1?ttl=%d&hide=%d' % (x, x)) |
1629 | + self.assertEquals(accounts, {'a': {'q': [{'id': '1'}]}}) |
1630 | + accounts = self._get_url('/a/q?hidden=true') |
1631 | + message = self.message('1', x, x, body='b') |
1632 | + self.assertMessages(accounts, 'a', 'q', [message]) |
1633 | + self._delete_url('/a/q/1') |
1634 | + |
1635 | + def test_message_put(self): |
1636 | + for x in range(0, 3): |
1637 | + url = '/a/q/1?ttl=%d&hide=%d' % (x, x) |
1638 | + status = 201 if x == 0 else 204 |
1639 | + self._put_url(url, body=str(x), status=status) |
1640 | + accounts = self._get_url('/a/q?hidden=true') |
1641 | + message = self.message('1', x, x, body=str(x)) |
1642 | + self.assertMessages(accounts, 'a', 'q', [message]) |
1643 | + self._delete_url('/a/q/1') |
1644 | + |
1645 | + def test_message_delete_limit(self): |
1646 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1647 | + accounts = self._delete_url('/a/q?limit=3&detail=all', status=200) |
1648 | + messages = [] |
1649 | + messages.append(self.message('1')) |
1650 | + messages.append(self.message('2')) |
1651 | + messages.append(self.message('3')) |
1652 | + self.assertMessages(accounts, 'a', 'q', messages) |
1653 | + accounts = self._delete_url('/a/q?limit=3&detail=all', status=200) |
1654 | + message = self.message('4') |
1655 | + self.assertMessages(accounts, 'a', 'q', [message]) |
1656 | + |
1657 | + def test_message_get_limit(self): |
1658 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1659 | + for x in range(0, 4): |
1660 | + accounts = self._get_url('/a/q?limit=3') |
1661 | + messages = [] |
1662 | + for y in range(x, 4)[:3]: |
1663 | + messages.append(self.message(str(y + 1))) |
1664 | + self.assertMessages(accounts, 'a', 'q', messages) |
1665 | + self._delete_url('/a/q/%d' % (x + 1)) |
1666 | + |
1667 | + def test_message_post_limit(self): |
1668 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1669 | + for x in range(0, 4): |
1670 | + accounts = self._post_url('/a/q?limit=3&ttl=%d&detail=all' % x) |
1671 | + messages = [] |
1672 | + for y in range(x, 4)[:3]: |
1673 | + messages.append(self.message(str(y + 1), x)) |
1674 | + self.assertMessages(accounts, 'a', 'q', messages) |
1675 | + self._delete_url('/a/q/%d' % (x + 1)) |
1676 | + |
1677 | + def test_message_delete_marker(self): |
1678 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1679 | + accounts = self._delete_url('/a/q?marker=2&detail=all', status=200) |
1680 | + messages = [] |
1681 | + messages.append(self.message('3')) |
1682 | + messages.append(self.message('4')) |
1683 | + self.assertMessages(accounts, 'a', 'q', messages) |
1684 | + accounts = self._delete_url('/a/q?marker=5&detail=all', status=200) |
1685 | + messages = [] |
1686 | + messages.append(self.message('1')) |
1687 | + messages.append(self.message('2')) |
1688 | + self.assertMessages(accounts, 'a', 'q', messages) |
1689 | + |
1690 | + def test_message_get_marker(self): |
1691 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1692 | + for x in range(0, 4): |
1693 | + accounts = self._get_url('/a/q?marker=%d' % x) |
1694 | + messages = [] |
1695 | + for y in range(x, 4): |
1696 | + messages.append(self.message(str(y + 1))) |
1697 | + self.assertMessages(accounts, 'a', 'q', messages) |
1698 | + self._delete_url('/a/q/%d' % (x + 1)) |
1699 | + |
1700 | + def test_message_post_marker(self): |
1701 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1702 | + for x in range(0, 4): |
1703 | + url = '/a/q?marker=%d&ttl=%d&detail=all' % (x, x) |
1704 | + accounts = self._post_url(url) |
1705 | + messages = [] |
1706 | + for y in range(x, 4): |
1707 | + messages.append(self.message(str(y + 1), x)) |
1708 | + self.assertMessages(accounts, 'a', 'q', messages) |
1709 | + self._delete_url('/a/q/%d' % (x + 1)) |
1710 | + |
1711 | + def test_message_delete_limit_marker(self): |
1712 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1713 | + url = '/a/q?limit=2&marker=1&detail=all' |
1714 | + accounts = self._delete_url(url, status=200) |
1715 | + messages = [] |
1716 | + messages.append(self.message('2')) |
1717 | + messages.append(self.message('3')) |
1718 | + self.assertMessages(accounts, 'a', 'q', messages) |
1719 | + url = '/a/q?limit=2&marker=5&detail=all' |
1720 | + accounts = self._delete_url(url, status=200) |
1721 | + messages = [] |
1722 | + messages.append(self.message('1')) |
1723 | + messages.append(self.message('4')) |
1724 | + self.assertMessages(accounts, 'a', 'q', messages) |
1725 | + |
1726 | + def test_message_get_limit_marker(self): |
1727 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1728 | + for x in range(0, 4): |
1729 | + accounts = self._get_url('/a/q?limit=2&marker=%d' % x) |
1730 | + messages = [] |
1731 | + for y in range(x, 4)[:2]: |
1732 | + messages.append(self.message(str(y + 1))) |
1733 | + self.assertMessages(accounts, 'a', 'q', messages) |
1734 | + self._delete_url('/a/q/%d' % (x + 1)) |
1735 | + |
1736 | + def test_message_post_limit_marker(self): |
1737 | + [self._put_url('/a/q/%d' % x) for x in range(1, 5)] |
1738 | + for x in range(0, 4): |
1739 | + url = '/a/q?limit=2&marker=%d&ttl=%d&detail=all' % (x, x) |
1740 | + accounts = self._post_url(url) |
1741 | + messages = [] |
1742 | + for y in range(x, 4)[:2]: |
1743 | + messages.append(self.message(str(y + 1), x)) |
1744 | + self.assertMessages(accounts, 'a', 'q', messages) |
1745 | + self._delete_url('/a/q/%d' % (x + 1)) |
1746 | + |
1747 | + def test_message_ttl(self): |
1748 | + self._put_url('/a/q/1?ttl=1') |
1749 | + accounts = self._get_url('/a/q/1') |
1750 | + message = self.message('1', 1) |
1751 | + self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)]) |
1752 | + time.sleep(1) |
1753 | + self.backend.clean() |
1754 | + self._get_url('/a/q/1', status=404) |
1755 | + self._put_url('/a/q/1') |
1756 | + accounts = self._get_url('/a/q/1') |
1757 | + self.assertMessages(accounts, 'a', 'q', [self.message('1')]) |
1758 | + self._post_url('/a/q/1?ttl=1') |
1759 | + accounts = self._get_url('/a/q/1') |
1760 | + self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)]) |
1761 | + time.sleep(1) |
1762 | + self.backend.clean() |
1763 | + self._get_url('/a/q/1', status=404) |
1764 | + |
1765 | + def test_message_hide(self): |
1766 | + self._put_url('/a/q/1?hide=1') |
1767 | + accounts = self._get_url('/a/q/1') |
1768 | + self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)]) |
1769 | + time.sleep(1) |
1770 | + self.backend.clean() |
1771 | + accounts = self._get_url('/a/q/1') |
1772 | + self.assertMessages(accounts, 'a', 'q', [self.message('1')]) |
1773 | + self._post_url('/a/q/1?hide=1') |
1774 | + accounts = self._get_url('/a/q/1') |
1775 | + self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)]) |
1776 | + time.sleep(1) |
1777 | + self.backend.clean() |
1778 | + accounts = self._get_url('/a/q/1') |
1779 | + self.assertMessages(accounts, 'a', 'q', [self.message('1')]) |
1780 | + self._delete_url('/a/q/1') |
1781 | + |
1782 | + def _message_wait(self): |
1783 | + accounts = self._get_url('/a/q?wait=2') |
1784 | + self.assertMessages(accounts, 'a', 'q', [self.message('1')]) |
1785 | + self.success = True |
1786 | + |
1787 | + def test_message_put_wait(self): |
1788 | + self.success = False |
1789 | + thread = eventlet.spawn(self._message_wait) |
1790 | + eventlet.spawn_after(0.2, self._put_url, '/a/q/1') |
1791 | + thread.wait() |
1792 | + self.assertTrue(self.success) |
1793 | + self._delete_url('/a/q/1') |
1794 | + |
1795 | + def test_message_put_wait_overwrite(self): |
1796 | + self.success = False |
1797 | + self._put_url('/a/q/1?hide=10') |
1798 | + thread = eventlet.spawn(self._message_wait) |
1799 | + eventlet.spawn_after(0.2, self._put_url, '/a/q/1?hide=0', status=204) |
1800 | + thread.wait() |
1801 | + self.assertTrue(self.success) |
1802 | + self._delete_url('/a/q/1') |
1803 | + |
1804 | + def test_message_put_wait_cleanup(self): |
1805 | + self.success = False |
1806 | + self._put_url('/a/q/1?hide=1') |
1807 | + thread = eventlet.spawn(self._message_wait) |
1808 | + eventlet.spawn_after(1, self.backend.clean) |
1809 | + thread.wait() |
1810 | + self.assertTrue(self.success) |
1811 | + self._delete_url('/a/q/1') |
1812 | + |
1813 | + def test_message_post_wait(self): |
1814 | + self.success = False |
1815 | + self._put_url('/a/q/1?hide=10') |
1816 | + thread = eventlet.spawn(self._message_wait) |
1817 | + eventlet.spawn_after(0.2, self._post_url, '/a/q/1?hide=0') |
1818 | + thread.wait() |
1819 | + self.assertTrue(self.success) |
1820 | + self._delete_url('/a/q/1') |
1821 | + |
1822 | + def test_message_post_wait_queue(self): |
1823 | + self.success = False |
1824 | + self._put_url('/a/q/1?hide=10') |
1825 | + thread = eventlet.spawn(self._message_wait) |
1826 | + eventlet.spawn_after(0.2, self._post_url, '/a/q?hide=0&hidden=true') |
1827 | + thread.wait() |
1828 | + self.assertTrue(self.success) |
1829 | + self._delete_url('/a/q/1') |
1830 | + |
1831 | + def message(self, id, ttl=0, hide=0, body=''): |
1832 | + return dict(id=id, ttl=ttl, hide=hide, body=body) |
1833 | + |
1834 | + def assertMessages(self, accounts, account, queue, messages): |
1835 | + self.assertEquals(len(accounts), 1) |
1836 | + self.assertEquals(len(accounts['a']), 1) |
1837 | + self.assertEquals(len(accounts['a']['q']), len(messages)) |
1838 | + for x in range(0, len(messages)): |
1839 | + self.assertEquals(accounts['a']['q'][x]['id'], messages[x]['id']) |
1840 | + ttl = messages[x]['ttl'] |
1841 | + if ttl > 0: |
1842 | + ttl += int(time.time()) |
1843 | + self.assertAlmostEquals(accounts['a']['q'][0]['ttl'], ttl) |
1844 | + hide = messages[x]['hide'] |
1845 | + if hide > 0: |
1846 | + hide += int(time.time()) |
1847 | + self.assertAlmostEquals(accounts['a']['q'][0]['hide'], hide) |
1848 | + body = messages[x]['body'] |
1849 | + self.assertEquals(accounts['a']['q'][x]['body'], body) |
1850 | + |
1851 | + def _delete_url(self, url, status=204, **kwargs): |
1852 | + return self._url('DELETE', url, status=status, **kwargs) |
1853 | + |
1854 | + def _get_url(self, url, **kwargs): |
1855 | + return self._url('GET', url, **kwargs) |
1856 | + |
1857 | + def _post_url(self, url, **kwargs): |
1858 | + return self._url('POST', url, **kwargs) |
1859 | + |
1860 | + def _put_url(self, url, status=201, **kwargs): |
1861 | + return self._url('PUT', url, status=status, **kwargs) |
1862 | + |
1863 | + def _url(self, method, url, body='', status=200): |
1864 | + req = webob.Request.blank(url, method=method, body=body) |
1865 | + res = req.get_response(self.frontend) |
1866 | + self.assertEquals(res.status_int, status) |
1867 | + if status == 200: |
1868 | + return json.loads(res.body) |
1869 | + return None |
1870 | + |
1871 | + |
1872 | +class TestWSGISQLite(TestWSGIMemory): |
1873 | + '''Unittests for the WSGI frontend to SQLite backend.''' |
1874 | + backend_class = burrowd.backend.sqlite.Backend |