blob: d510a0e2440663c2557dc21b2f25a7eec1aa4894 [file] [log] [blame]
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +04001"""Manage Hiccup stats.
2
3This module provides a command to compute statistics of
4heartbeats, crashes, and versions sent from Hiccup clients.
5"""
6import datetime
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +02007from typing import Dict, List, Optional
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +04008
9from django.core.management.base import BaseCommand
10from django.db import transaction
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020011from django.db.models import Count, F, Model, Q, QuerySet
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040012from django.db.models.functions import TruncDate
13import pytz
14
15from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020016 RadioVersion,
17 RadioVersionDaily,
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040018 StatsMetadata,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020019 Version,
20 VersionDaily,
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020021 _VersionStats,
22 _DailyVersionStats,
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040023)
24from crashreports.models import Crashreport, HeartBeat
25
26
27# pylint: disable=too-few-public-methods
28# Classes in this file inherit from each other and are not method containers.
29
30
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020031class _ReportCounterFilter:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040032 """Filter reports matching a report counter requirements.
33
34 Attributes:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020035 model: The report model.
36 name: The human-readable report counter name.
37 field_name:
38 The counter name as defined in the stats model where it is a field.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040039
40 """
41
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020042 def __init__(self, model: Model, name: str, field_name: str) -> None:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040043 """Initialise the filter.
44
45 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020046 model: The report model.
47 name: The human-readable report counter name.
48 field_name:
49 The counter name as defined in the stats model where it is a
50 field.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040051
52 """
53 self.model = model
54 self.name = name
55 self.field_name = field_name
56
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020057 def filter(self, query_objects: QuerySet) -> QuerySet:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040058 """Filter the reports.
59
60 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020061 query_objects: The reports to filter.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040062 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020063 The reports matching this report counter requirements.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040064
65 """
66 # pylint: disable=no-self-use
67 # self is potentially used by subclasses.
68 return query_objects
69
70
71class HeartBeatCounterFilter(_ReportCounterFilter):
72 """The heartbeats counter filter."""
73
74 def __init__(self):
75 """Initialise the filter."""
76 super(HeartBeatCounterFilter, self).__init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020077 model=HeartBeat, name="heartbeats", field_name="heartbeats"
78 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040079
80
81class CrashreportCounterFilter(_ReportCounterFilter):
82 """The crashreports counter filter.
83
84 Attributes:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +020085 include_boot_reasons:
86 The boot reasons assumed to characterise this crashreport ("OR"ed).
87 exclude_boot_reasons:
88 The boot reasons assumed to *not* characterise this crashreport (
89 "AND"ed).
90 inclusive_filter:
91 The boot reasons filter for filtering reports that should be
92 included.
93 exclusive_filter:
94 The boot reasons filter for filtering reports that should *not*
95 be included.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +040096
97 """
98
99 def __init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200100 self,
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200101 name: str,
102 field_name: str,
103 include_boot_reasons: Optional[List[str]] = None,
104 exclude_boot_reasons: Optional[List[str]] = None,
105 ) -> None:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400106 """Initialise the filter.
107
108 One or both of `include_boot_reasons` and `exclude_boot_reasons` must
109 be specified.
110
111 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200112 name: The human-readable report counter name.
113 field_name:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400114 The counter name as defined in the stats model where it is a
115 field.
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200116 include_boot_reasons:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400117 The boot reasons assumed to characterise this crashreport
118 ("OR"ed).
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200119 exclude_boot_reasons:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400120 The boot reasons assumed to *not* characterise this
121 crashreport ("AND"ed).
122 Raises:
123 ValueError:
124 None of `include_boot_reasons` and `exclude_boot_reasons` have
125 been supplied.
126
127 """
128 if not include_boot_reasons and not exclude_boot_reasons:
129 raise ValueError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200130 "One or both of `include_boot_reasons` and "
131 "`exclude_boot_reasons` must be specified."
132 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400133
134 super(CrashreportCounterFilter, self).__init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200135 model=Crashreport, name=name, field_name=field_name
136 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400137
138 # Cache the boot reasons inclusive filter
139 self.include_boot_reasons = include_boot_reasons
140 self.inclusive_filter = self._create_query_filter(include_boot_reasons)
141
142 # Cache the boot reasons exclusive filter
143 self.exclude_boot_reasons = exclude_boot_reasons
144 self.exclusive_filter = self._create_query_filter(exclude_boot_reasons)
145
146 @staticmethod
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200147 def _create_query_filter(reasons: Optional[List[str]]) -> Q:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400148 """Combine boot reasons into one filter.
149
150 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200151 reasons: List of boot reasons to include in filter.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400152 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200153 Query that matches either of reasons as boot_reason if list is
154 not empty, otherwise None.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400155
156 """
157 if not reasons:
158 return None
159
160 query = Q(boot_reason=reasons[0])
161 for reason in reasons[1:]:
162 query = query | Q(boot_reason=reason)
163 return query
164
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200165 def filter(self, query_objects: QuerySet) -> QuerySet:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400166 """Filter the reports according to the inclusive and exclusive fitlers.
167
168 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200169 query_objects: The reports to filter.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400170 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200171 The reports matching this report counter requirements.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400172
173 """
174 if self.inclusive_filter:
175 query_objects = query_objects.filter(self.inclusive_filter)
176 if self.exclusive_filter:
177 query_objects = query_objects.exclude(self.exclusive_filter)
178
179 return query_objects
180
181
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200182class _StatsModelsEngine:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400183 """Stats models engine.
184
185 An engine to update general stats (_VersionStats) and their daily
186 counterparts (_DailyVersionStats).
187 """
188
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200189 def __init__(
190 self,
191 stats_model: _VersionStats,
192 daily_stats_model: _DailyVersionStats,
193 version_field_name: str,
194 ) -> None:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400195 """Initialise the engine.
196
197 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200198 stats_model: The _VersionStats model to update stats for.
199 daily_stats_model: The _DailyVersionStats model to update stats for.
200 version_field_name:
201 The version field name as specified in the stats models.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400202
203 """
204 self.stats_model = stats_model
205 self.daily_stats_model = daily_stats_model
206 self.version_field_name = version_field_name
207
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200208 def _valid_objects(self, query_objects: QuerySet) -> QuerySet:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400209 """Filter out invalid reports.
210
211 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200212 All the valid reports.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400213
214 """
215 # pylint: disable=no-self-use
216 # self is potentially used by subclasses.
217 return query_objects
218
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200219 def _objects_within_period(
220 self,
221 query_objects: QuerySet,
222 up_to: datetime.datetime,
223 starting_from: Optional[datetime.datetime] = None,
224 ) -> QuerySet:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400225 """Retrieve the reports within a specific period of time.
226
227 The objects are filtered considering a specific period of time to allow
228 for comparable results between subclasses. The lower bound should be
229 omitted for the first update but always set for later calls. The upper
230 bound must be specified to avoid race conditions.
231
232 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200233 query_objects: The reports to filter.
234 up_to: The maximum timestamp to consider (inclusive).
235 starting_from: The minimum timestamp to consider (exclusive).
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400236 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200237 The reports received within a specific period of time.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400238
239 """
240 # pylint: disable=no-self-use
241 # self might be used by subclasses.
242 query_objects = query_objects.filter(created_at__lte=up_to)
243 if starting_from:
244 query_objects = query_objects.filter(created_at__gt=starting_from)
245
246 return query_objects
247
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200248 def _unique_objects_per_day(self, query_objects: QuerySet) -> QuerySet:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400249 """Count the unique reports per version per day.
250
251 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200252 query_objects: The reports to count.
253 Returns: The unique reports grouped per version per day.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400254
255 """
256 return (
Mitja Nikolaus0c34e402018-08-31 11:17:43 +0200257 query_objects.annotate(_report_day=TruncDate("date"))
258 .values(self.version_field_name, "_report_day")
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200259 .annotate(count=Count("date", distinct=True))
260 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400261
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200262 def delete_stats(self) -> Dict[str, int]:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400263 """Delete the general and daily stats instances.
264
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200265 Returns: The count of deleted entries per model name.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400266
267 """
268 # Clear the general stats, the daily stats will be deleted by cascading
269 # effect
270 _, count_per_model = self.stats_model.objects.all().delete()
271 return count_per_model
272
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200273 def update_stats(
274 self,
275 report_counter: _ReportCounterFilter,
276 up_to: datetime.datetime,
277 starting_from: Optional[datetime.datetime] = None,
278 ) -> Dict[Model, Dict[str, int]]:
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400279 """Update the statistics of the general and daily stats entries.
280
281 The algorithm works as follow:
282 1. The reports are filtered considering a specific period of time to
283 allow for comparable results between subclasses. The lower bound
284 should be omitted for the first update but always set for later
285 calls. The upper bound must be specified to avoid race conditions.
286 2. The report counter requirements are applied to the reports.
287 3. The reports are grouped per day and per version, a counter is
288 generated.
289 4. Each report group count is used to update specific daily stats,
290 while the sum of them per version updates the general stats.
291
292 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200293 report_counter: The report counter to update the stats with.
294 up_to: The maximum timestamp to consider (inclusive).
295 starting_from: The minimum timestamp to consider (exclusive).
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400296 Returns:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200297 The number of added entries and the number of updated entries
298 bundled in a dict, respectively hashed with the keys 'created'
299 and 'updated', per model name.
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400300
301 """
302 counts_per_model = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200303 self.stats_model: {"created": 0, "updated": 0},
304 self.daily_stats_model: {"created": 0, "updated": 0},
305 }
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400306
307 query_objects = self._valid_objects(report_counter.model.objects.all())
308 # Only include reports from the interesting period of time
309 query_objects = self._objects_within_period(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200310 query_objects, up_to, starting_from
311 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400312 # Apply the report counter requirements
313 query_objects = report_counter.filter(query_objects)
314 # Chain our own filters
315 query_objects = self._unique_objects_per_day(query_objects)
316
317 # Explicitly use the iterator() method to avoid caching as we will
318 # not re-use the QuerySet
319 for query_object in query_objects.iterator():
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200320 report_day = query_object["_report_day"]
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400321 # Use a dict to be able to dereference the field name
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200322 stats, created = self.stats_model.objects.get_or_create(
323 **{
324 self.version_field_name: query_object[
325 self.version_field_name
326 ],
327 "defaults": {
328 "first_seen_on": report_day,
329 "released_on": report_day,
330 },
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400331 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200332 )
333 counts_per_model[self.stats_model][
334 ("created" if created else "updated")
335 ] += 1
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400336
337 # Reports are coming in an unordered manner, a late report can
338 # be older (device time wise). Make sure that the current reports
339 # creation date is taken into account in the version history.
340 if not created and stats.first_seen_on > report_day:
341 # Avoid changing the released_on field if it is different than
342 # the default value (i.e. equals to the value of first_seen_on)
343 # since it indicates that it was manually changed.
344 if stats.released_on == stats.first_seen_on:
345 stats.released_on = report_day
346 stats.first_seen_on = report_day
347
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200348 daily_stats, created = self.daily_stats_model.objects.get_or_create(
349 version=stats, date=report_day
350 )
351 counts_per_model[self.daily_stats_model][
352 ("created" if created else "updated")
353 ] += 1
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400354
355 setattr(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200356 stats,
357 report_counter.field_name,
358 F(report_counter.field_name) + query_object["count"],
359 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400360 setattr(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200361 daily_stats,
362 report_counter.field_name,
363 F(report_counter.field_name) + query_object["count"],
364 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400365
366 stats.save()
367 daily_stats.save()
368
369 return counts_per_model
370
371
372class VersionStatsEngine(_StatsModelsEngine):
373 """Version stats engine.
374
375 An engine to update a counter of general stats (Version) and their daily
376 counterparts (VersionDaily).
377 """
378
379 def __init__(self):
380 """Initialise the engine."""
381 super(VersionStatsEngine, self).__init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200382 stats_model=Version,
383 daily_stats_model=VersionDaily,
384 version_field_name="build_fingerprint",
385 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400386
387
388class RadioVersionStatsEngine(_StatsModelsEngine):
389 """Radio version stats engine.
390
391 An engine to update a counter of general stats (RadioVersion) and their
392 daily counterparts (RadioVersionDaily).
393 """
394
395 def __init__(self):
396 """Initialise the engine."""
397 super(RadioVersionStatsEngine, self).__init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200398 stats_model=RadioVersion,
399 daily_stats_model=RadioVersionDaily,
400 version_field_name="radio_version",
401 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400402
403 def _valid_objects(self, query_objects):
404 # For legacy reasons, the version field might be null
405 return query_objects.filter(radio_version__isnull=False)
406
407
408class Command(BaseCommand):
409 """Management command to compute Hiccup statistics."""
410
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200411 _STATS_MODELS_ENGINES = [VersionStatsEngine(), RadioVersionStatsEngine()]
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400412
413 # All the report counters that are listed in the stats models
414 _REPORT_COUNTER_FILTERS = [
415 HeartBeatCounterFilter(),
416 CrashreportCounterFilter(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200417 name="crashes",
418 field_name="prob_crashes",
419 include_boot_reasons=Crashreport.CRASH_BOOT_REASONS,
420 ),
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400421 CrashreportCounterFilter(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200422 name="smpl",
423 field_name="smpl",
424 include_boot_reasons=Crashreport.SMPL_BOOT_REASONS,
425 ),
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400426 CrashreportCounterFilter(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200427 name="other",
428 field_name="other",
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400429 exclude_boot_reasons=(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200430 Crashreport.SMPL_BOOT_REASONS + Crashreport.CRASH_BOOT_REASONS
431 ),
432 ),
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400433 ]
434
435 help = __doc__
436
437 def add_arguments(self, parser):
438 """Add custom arguments to the command."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200439 parser.add_argument("action", choices=["reset", "update"])
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400440
441 def handle(self, *args, **options):
442 """Carry out the command executive logic."""
443 # pylint: disable=attribute-defined-outside-init
444 # self.debug is only ever read through calls of handle().
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200445 self.debug = int(options["verbosity"]) >= 2
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400446
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200447 if options["action"] == "reset":
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400448 self.delete_all_stats()
449 self.update_all_stats()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200450 elif options["action"] == "update":
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400451 self.update_all_stats()
452
453 def _success(self, msg, *args, **kwargs):
454 # pylint: disable=no-member
455 # Members of Style are generated and cannot be statically inferred.
456 self.stdout.write(self.style.SUCCESS(msg), *args, **kwargs)
457
458 def delete_all_stats(self):
459 """Delete the statistics from all stats models."""
460 with transaction.atomic():
461 for engine in self._STATS_MODELS_ENGINES:
462 counts_per_model = engine.delete_stats()
463 if self.debug:
464 # Default the count of deleted models to 0 if missing
465 if not counts_per_model:
466 counts_per_model = {
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200467 engine.stats_model._meta.label: 0,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200468 engine.daily_stats_model._meta.label: 0,
469 }
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400470 for model, count in counts_per_model.items():
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200471 name = model.split(".")[-1]
472 self._success("{} {} deleted".format(count, name))
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400473
474 # Reset the metadata
475 count, _ = StatsMetadata.objects.all().delete()
476 if self.debug:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200477 self._success("{} StatsMetadata deleted".format(count))
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400478
479 def update_all_stats(self):
480 """Update the statistics from all stats models."""
481 try:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200482 previous_update = StatsMetadata.objects.latest("updated_at")
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400483 starting_from = previous_update.updated_at
484 except StatsMetadata.DoesNotExist:
485 starting_from = None
486 # Fix the upper limit to avoid race conditions with new reports sent
487 # while we are updating the different statistics
488 up_to = datetime.datetime.now(tz=pytz.utc)
489
490 for engine in self._STATS_MODELS_ENGINES:
491 with transaction.atomic():
492 for filter_ in self._REPORT_COUNTER_FILTERS:
493 counts_per_model = engine.update_stats(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200494 filter_, up_to, starting_from
495 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400496 if self.debug:
497 for model, counts in counts_per_model.items():
498 for action, count in counts.items():
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200499 msg = "{} {} {} for counter {}".format(
500 count, model.__name__, action, filter_.name
501 )
Borjan Tchakaloffb98dba72018-03-16 11:04:47 +0400502 self._success(msg)
503
504 StatsMetadata(updated_at=up_to).save()