3 # Copyright 2011 Google Inc. All Rights Reserved.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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 # vim: set ts=4 sw=4 et tw=79:
18 """Implementation of a counter with persistent storage."""
20 __author__
= 'Greg Darke <darke@google.com>'
26 from google
.appengine
.api
import backends
27 from google
.appengine
.api
import runtime
28 from google
.appengine
.api
import taskqueue
29 from google
.appengine
.ext
import db
30 from google
.appengine
.ext
import webapp
31 from google
.appengine
.ext
.webapp
.util
import run_wsgi_app
34 class CounterModel(db
.Model
):
35 value
= db
.IntegerProperty(default
=0)
39 def get_or_new(cls
, name
):
40 model
= cls
.get_by_key_name(name
)
42 model
= cls(key_name
=name
)
46 class CounterStore(object):
49 self
._has
_shutdown
= False
50 self
._batch
_size
= 100
52 def get_value(self
, name
):
53 if name
not in self
._store
:
54 model
= CounterModel
.get_or_new(name
)
55 self
._store
[name
] = model
57 model
= self
._store
[name
]
60 def inc_value(self
, name
, delta
):
61 model
= self
.get_value(name
)
64 if self
._has
_shutdown
:
65 # Since the shutdown hook may be called at any time, we need to
66 # protect ourselves against this.
70 def _put_counters(self
, counters
):
72 for counter
in counters
:
73 counter
._dirty
= False
75 def _write_in_batches(self
, counters
):
76 """Write out the dirty entries from 'counters' in batches.
78 The batch size is determined by self._batch_size.
81 counters: An iterable containing instances of CounterModel.
84 for counter
in counters
:
86 to_put
.append(counter
)
88 if len(to_put
) >= self
._batch
_size
:
89 self
._put
_counters
(to_put
)
93 self
._put
_counters
(to_put
)
95 def flush_to_datastore(self
):
96 """Write the dirty entries from _store to datastore."""
97 self
._write
_in
_batches
(self
._store
.itervalues())
100 def shutdown_hook(self
):
101 """Ensures all counters are written to datastore."""
102 self
.flush_to_datastore()
103 self
._has
_shutdown
= True
106 class StartHandler(webapp
.RequestHandler
):
107 """Handler for '/_ah/start'.
109 This url is called once when the backend is started.
112 runtime
.set_shutdown_hook(_counter_store
.shutdown_hook
)
115 class CounterHandler(webapp
.RequestHandler
):
116 """Handler for counter/{get,inc,dec}.
118 This handler is protected by login: admin in app.yaml.
121 def _write_error(self
, error_message
):
122 self
.response
.error(400)
123 self
.response
.out
.write(error_message
)
125 def post(self
, method
):
126 """Handler a post to counter/{get,inc,dec}.
128 The 'method' parameter is parsed from the url by a regex capture group.
131 method: The type of operation to perform against the counter store.
132 It must be one of 'get', 'inc' or 'dec'.
134 key_name
= self
.request
.get('name')
135 delta
= self
.request
.get('delta')
138 self
._write
_error
('Request did not have a "key_name" parameter.')
142 model
= _counter_store
.get_value(key_name
)
145 self
._write
_error
('Request did not have a "delta" parameter.')
150 self
._write
_error
('"delta" is not an integer.')
154 model
= _counter_store
.inc_value(key_name
, delta
)
155 elif method
== 'dec':
156 model
= _counter_store
.inc_value(key_name
, -delta
)
158 self
.response
.headers
['Content-Type'] = 'text/plain'
159 self
.response
.out
.write('%d' % model
.value
)
162 _handlers
= [(r
'/_ah/start', StartHandler
),
163 (r
'/backend/counter/(get|inc|dec)$', CounterHandler
)]
165 application
= webapp
.WSGIApplication(_handlers
)
166 _counter_store
= CounterStore()
170 run_wsgi_app(application
)
172 if __name__
== '__main__':