Repository: learningequality/kolibri

Issue:
'Database is Locked' error when channel importing is running
We get errors in `jonboiser`'s `available-channels` branch. It's not reliably replicating (!), but the general observations are:

- It happens during `startremotechannelimport` or `diskchannelimport`
- The actual error is raised when another API or session call happens

My sense here is that it's caused by the long content import and annotation phase of channel importing.

Possible solutions:
- shorten length of transactions
- do stuff outside of transactions, before committing them


Discussion:
see also https://github.com/learningequality/kolibri/issues/2271, https://github.com/learningequality/kolibri/issues/2171
@aronasorman for help replicating, you might try adding long `time.sleep()`s inside suspicious sections to increase likelihood of concurrent DB access
^ That's a good suggestion, I'll try it out when I look at this on Monday!

Having a hard time replicating on my machine, so I'll fall back to either trying to slow down IO for a program, or using our designated slow laptop to try to replicate this.

Resolving patch:
diff --git a/kolibri/content/management/commands/importcontent.py b/kolibri/content/management/commands/importcontent.py
--- a/kolibri/content/management/commands/importcontent.py
+++ b/kolibri/content/management/commands/importcontent.py
@@ -173,7 +173,7 @@ def _transfer(self, method, channel_id, path=None, node_ids=None, exclude_node_i
                     os.remove(dest)
                 self.cancel()
             else:
-                annotation.set_availability(file_checksums_to_annotate)
+                annotation.set_availability(channel_id, file_checksums_to_annotate)
 
     def handle_async(self, *args, **options):
         if options['command'] == 'network':
diff --git a/kolibri/content/utils/annotation.py b/kolibri/content/utils/annotation.py
--- a/kolibri/content/utils/annotation.py
+++ b/kolibri/content/utils/annotation.py
@@ -110,7 +110,7 @@ def set_local_file_availability_from_disk(checksums=None):
 
     mark_local_files_as_available(checksums_to_update)
 
-def recurse_availability_up_tree():
+def recurse_availability_up_tree(channel_id):
     bridge = Bridge(app_name=CONTENT_APP_NAME)
 
     ContentNodeClass = bridge.get_class(ContentNode)
@@ -138,16 +138,18 @@ def recurse_availability_up_tree():
         logging.info('Setting availability of ContentNode objects with children for level {level}'.format(level=level))
         # Only modify topic availability here
         connection.execute(ContentNodeTable.update().where(
-            ContentNodeTable.c.level == level - 1).where(
-            ContentNodeTable.c.kind == content_kinds.TOPIC).values(available=exists(available_nodes)).execution_options(autocommit=True))
+            and_(
+                ContentNodeTable.c.level == level - 1,
+                ContentNodeTable.c.channel_id == channel_id,
+                ContentNodeTable.c.kind == content_kinds.TOPIC)).values(available=exists(available_nodes)).execution_options(autocommit=True))
 
     bridge.end()
 
-def set_availability(checksums=None):
+def set_availability(channel_id, checksums=None):
     if checksums is None:
         set_local_file_availability_from_disk()
     else:
         mark_local_files_as_available(checksums)
 
     set_leaf_node_availability_from_local_file_availability()
-    recurse_availability_up_tree()
+    recurse_availability_up_tree(channel_id)


Test patch:
diff --git a/kolibri/content/test/test_annotation.py b/kolibri/content/test/test_annotation.py
--- a/kolibri/content/test/test_annotation.py
+++ b/kolibri/content/test/test_annotation.py
@@ -61,19 +61,19 @@ def setUp(self):
 
     def test_all_content_nodes_available(self):
         ContentNode.objects.exclude(kind=content_kinds.TOPIC).update(available=True)
-        recurse_availability_up_tree()
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
         self.assertTrue(ContentNode.objects.get(id="da7ecc42e62553eebc8121242746e88a").available)
         self.assertTrue(ContentNode.objects.get(id="2e8bac07947855369fe2d77642dfc870").available)
 
     def test_no_content_nodes_available(self):
         ContentNode.objects.filter(kind=content_kinds.TOPIC).update(available=True)
-        recurse_availability_up_tree()
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
         # 2, as there are two childless topics in the fixture, these cannot exist in real databases
         self.assertEqual(ContentNode.objects.filter(kind=content_kinds.TOPIC).filter(available=True).count(), 2)
 
     def test_one_content_node_available(self):
         ContentNode.objects.filter(id='32a941fb77c2576e8f6b294cde4c3b0c').update(available=True)
-        recurse_availability_up_tree()
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
         # Check parent is available
         self.assertTrue(ContentNode.objects.get(id='da7ecc42e62553eebc8121242746e88a').available)