Repository: edgedb/edgedb

Issue:
on_demand compiler pool doesn't seem to scale up properly
1. Run `edb server --compiler-pool-mode=on_demand --compiler-pool-size=16`
2. Run `edb cli migration create` on a big schema that will take a while; I used the one from https://github.com/edgedb/edgedb/issues/4677
3. Try running `select 1` another cli.

It will hang for a long time. (It might sneak in somewhere in the middle of the migration, which is multiple commands.)

on_demand is the default for dev mode, so I was confused for a while. This reproduces without any compiler flags to `edb server` but I wanted to be explicit


Discussion:
The on_demand compiler pool only scales up when the **number of queued requests** goes up in a 3-seconds window. That means, the first `select 1` will only start the 3-seconds window (with 1 queued request) and you'll need a second `select 1+2` within 3 seconds to trigger the scale-up. This was designed to avoid scaling up for a busy-balanced scenario when the queue size is stable, but we can always change it (like count the starting queue size by 1 less so that we can adapt to the scenario mentioned in this issue).

Resolving patch:
diff --git a/edb/server/compiler_pool/pool.py b/edb/server/compiler_pool/pool.py
--- a/edb/server/compiler_pool/pool.py
+++ b/edb/server/compiler_pool/pool.py
@@ -939,7 +939,6 @@ def __init__(self, *, pool_size, **kwargs):
         super().__init__(pool_size=1, **kwargs)
         self._worker_transports = {}
         self._expected_num_workers = 0
-        self._scale_up_handle = None
         self._scale_down_handle = None
         self._max_num_workers = pool_size
 
@@ -958,9 +957,9 @@ async def _stop(self):
     async def _acquire_worker(
         self, *, condition=None, weighter=None, **compiler_args
     ):
+        scale_up_handle = None
         if (
-            self._running and
-            self._scale_up_handle is None
+            self._running
             and self._workers_queue.qsize() == 0
             and (
                 len(self._workers)
@@ -968,17 +967,19 @@ async def _acquire_worker(
                 < self._max_num_workers
             )
         ):
-            self._scale_up_handle = self._loop.call_later(
-                ADAPTIVE_SCALE_UP_WAIT_TIME,
-                self._maybe_scale_up,
-                self._workers_queue.count_waiters() + 1,
+            scale_up_handle = self._loop.call_later(
+                ADAPTIVE_SCALE_UP_WAIT_TIME, self._maybe_scale_up
             )
         if self._scale_down_handle is not None:
             self._scale_down_handle.cancel()
             self._scale_down_handle = None
-        return await super()._acquire_worker(
-            condition=condition, weighter=weighter, **compiler_args
-        )
+        try:
+            return await super()._acquire_worker(
+                condition=condition, weighter=weighter, **compiler_args
+            )
+        finally:
+            if scale_up_handle is not None:
+                scale_up_handle.cancel()
 
     def _release_worker(self, worker, *, put_in_front: bool = True):
         if self._scale_down_handle is not None:
@@ -1025,26 +1026,20 @@ def process_exited(self):
                     self._worker_transports.pop(pid, None)
 
     async def _create_worker(self):
-        try:
-            # Creates a single compiler worker process.
-            transport = await self._create_compiler_process()
-            self._worker_transports[transport.get_pid()] = transport
-            self._expected_num_workers += 1
-        finally:
-            self._scale_up_handle = None
+        # Creates a single compiler worker process.
+        transport = await self._create_compiler_process()
+        self._worker_transports[transport.get_pid()] = transport
+        self._expected_num_workers += 1
 
-    def _maybe_scale_up(self, starting_num_waiters):
+    def _maybe_scale_up(self):
         if not self._running:
             return
-        if self._workers_queue.count_waiters() > starting_num_waiters:
-            logger.info(
-                "Compile requests are queuing up in the past %d seconds, "
-                "spawn a new compiler worker process now.",
-                ADAPTIVE_SCALE_UP_WAIT_TIME,
-            )
-            self._loop.create_task(self._create_worker())
-        else:
-            self._scale_up_handle = None
+        logger.info(
+            "A compile request has waited for more than %d seconds, "
+            "spawn a new compiler worker process now.",
+            ADAPTIVE_SCALE_UP_WAIT_TIME,
+        )
+        self._loop.create_task(self._create_worker())
 
     def _scale_down(self):
         self._scale_down_handle = None


Test patch:
diff --git a/tests/test_server_compiler.py b/tests/test_server_compiler.py
--- a/tests/test_server_compiler.py
+++ b/tests/test_server_compiler.py
@@ -379,7 +379,7 @@ async def test_server_compiler_pool_with_server(self):
             self.assertEqual(sd.call_system_api('/server/status/ready'), 'OK')
 
 
-class TestCompilerPool(tbs.TestCase):
+class TestCompilerPool(tbs.TestCase, tb.PreloadParserGrammarMixin):
     @classmethod
     def setUpClass(cls):
         super().setUpClass()
@@ -407,12 +407,6 @@ async def _test_pool_disconnect_queue(self, pool_class):
                         self._std_schema),
                 ),
             )
-            # HACK: For adaptive pool, force the creation of a second
-            # worker. This is needed to work around issue #4680, where
-            # we won't scale up from a single connection.
-            if issubclass(pool_class, pool.SimpleAdaptivePool):
-                await pool_._create_worker()
-
             try:
                 w1 = await pool_._acquire_worker()
                 w2 = await pool_._acquire_worker()