Lock affected rows when assigning local IDs

Lock the targeted device when calculating the next heartbeat and crash
report local IDs. Lock the targeted crash report when calculating the
next log file local ID.

This change prevents race conditions that have occured previously.

Issue: HIC-251
Change-Id: I80a8498849e20769b0ab36afe68e6c20b3108c6d
diff --git a/crashreports/models.py b/crashreports/models.py
index f76c894..a405620 100644
--- a/crashreports/models.py
+++ b/crashreports/models.py
@@ -46,17 +46,19 @@
     @transaction.atomic
     def get_crashreport_key(self):
         """Get the next key for a crashreport and update the ID-counter."""
-        ret = self.next_per_crashreport_key
-        self.next_per_crashreport_key = self.next_per_crashreport_key + 1
-        self.save()
+        device = Device.objects.select_for_update().get(id=self.id)
+        ret = device.next_per_crashreport_key
+        device.next_per_crashreport_key += 1
+        device.save()
         return ret
 
     @transaction.atomic
     def get_heartbeat_key(self):
         """Get the next key for a heartbeat and update the ID-counter."""
-        ret = self.next_per_heartbeat_key
-        self.next_per_heartbeat_key = self.next_per_heartbeat_key + 1
-        self.save()
+        device = Device.objects.select_for_update().get(id=self.id)
+        ret = device.next_per_heartbeat_key
+        device.next_per_heartbeat_key += 1
+        device.save()
         return ret
 
 
@@ -118,9 +120,10 @@
     @transaction.atomic
     def get_logfile_key(self):
         """Get the next key for a log file and update the ID-counter."""
-        ret = self.next_logfile_key
-        self.next_logfile_key = self.next_logfile_key + 1
-        self.save()
+        crashreport = Crashreport.objects.select_for_update().get(id=self.id)
+        ret = crashreport.next_logfile_key
+        crashreport.next_logfile_key += 1
+        crashreport.save()
         return ret
 
     def save(
diff --git a/crashreports/tests/test_rest_api_crashreports.py b/crashreports/tests/test_rest_api_crashreports.py
index b09ca1d..11b8e8e 100644
--- a/crashreports/tests/test_rest_api_crashreports.py
+++ b/crashreports/tests/test_rest_api_crashreports.py
@@ -1,5 +1,4 @@
 """Tests for the crashreports REST API."""
-import unittest
 from datetime import timedelta
 
 from django.db import connection
@@ -54,7 +53,6 @@
         pass
 
 
-@unittest.skip("Fails because of race condition when assigning local IDs")
 class CrashreportRaceConditionsTestCase(RaceConditionsTestCase):
     """Test cases for crashreport race conditions."""
 
diff --git a/crashreports/tests/test_rest_api_heartbeats.py b/crashreports/tests/test_rest_api_heartbeats.py
index d2c3e1a..ac2e03e 100644
--- a/crashreports/tests/test_rest_api_heartbeats.py
+++ b/crashreports/tests/test_rest_api_heartbeats.py
@@ -1,6 +1,5 @@
 """Tests for the heartbeats REST API."""
 from datetime import timedelta, datetime
-import unittest
 
 import pytz
 from django.db import connection
@@ -322,7 +321,6 @@
         self.assertEqual(response.data["date"], str(data["date"].date()))
 
 
-@unittest.skip("Fails because of race condition when assigning local IDs")
 class HeartBeatRaceConditionsTestCase(RaceConditionsTestCase):
     """Test cases for heartbeat race conditions."""
 
diff --git a/crashreports/tests/test_rest_api_logfiles.py b/crashreports/tests/test_rest_api_logfiles.py
index b0aeb34..1cf2e55 100644
--- a/crashreports/tests/test_rest_api_logfiles.py
+++ b/crashreports/tests/test_rest_api_logfiles.py
@@ -3,7 +3,6 @@
 import os
 import shutil
 import tempfile
-import unittest
 import zipfile
 
 from django.conf import settings
@@ -153,7 +152,6 @@
         shutil.rmtree(settings.MEDIA_ROOT)
 
 
-@unittest.skip("Fails because of race condition when assigning local IDs")
 class LogfileRaceConditionsTestCase(RaceConditionsTestCase):
     """Test cases for logfile race conditions."""