summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/server
diff options
context:
space:
mode:
authorRichard Purdie <richard.purdie@linuxfoundation.org>2022-12-30 22:13:45 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2022-12-31 17:05:17 +0000
commit4c57c6eeecc43d0479380144f3073e61a8b43375 (patch)
tree75988728828f7c8528d89815ed1f48353f25d40a /bitbake/lib/bb/server
parent3cc9aed5a59d7b72b98ef40727102c98b031f911 (diff)
downloadpoky-4c57c6eeecc43d0479380144f3073e61a8b43375.tar.gz
bitbake: server/process: Run idle commands in a separate idle thread
When bitbake is off running heavier "idle" commands, it doesn't service it's command socket which means stopping/interrupting it is hard. It also means we can't "ping" from the UI to know if it is still alive. For those reasons, split idle command execution into it's own thread. The commands are generally already self containted so this is easier than expected. We do have to be careful to only handle inotify poll() from a single thread at a time. It also means we always have to use a thread lock when sending events since both the idle thread and the command thread may generate log messages (and hence events). The patch depends on previous fixes to the builtins locking in event.py and the heartbeat enable/disable changes as well as other locking additions. We use a condition to signal from the idle thread when other sections of code can continue, thanks to Joshua Watt for the review and tweaks squashed into this patch. We do have some sync points where we need to ensure any currently executing commands have finished before we can start a new async command for example. (Bitbake rev: 67dd9a5e84811df8869a82da6a37a41ee8fe94e2) Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/bb/server')
-rw-r--r--bitbake/lib/bb/server/process.py105
-rw-r--r--bitbake/lib/bb/server/xmlrpcserver.py2
2 files changed, 69 insertions, 38 deletions
diff --git a/bitbake/lib/bb/server/process.py b/bitbake/lib/bb/server/process.py
index 2aee9ef051..ac7749d36c 100644
--- a/bitbake/lib/bb/server/process.py
+++ b/bitbake/lib/bb/server/process.py
@@ -92,8 +92,11 @@ class ProcessServer():
92 self.maxuiwait = 30 92 self.maxuiwait = 30
93 self.xmlrpc = False 93 self.xmlrpc = False
94 94
95 self.idle = None
96 # Need a lock for _idlefuns changes
95 self._idlefuns = {} 97 self._idlefuns = {}
96 self._idlefuncsLock = threading.Lock() 98 self._idlefuncsLock = threading.Lock()
99 self.idle_cond = threading.Condition(self._idlefuncsLock)
97 100
98 self.bitbake_lock = lock 101 self.bitbake_lock = lock
99 self.bitbake_lock_name = lockname 102 self.bitbake_lock_name = lockname
@@ -151,6 +154,12 @@ class ProcessServer():
151 154
152 return ret 155 return ret
153 156
157 def wait_for_idle(self, timeout=30):
158 # Wait for the idle loop to have cleared
159 with self.idle_cond:
160 # FIXME - the 1 is the inotify processing in cooker which always runs
161 self.idle_cond.wait_for(lambda: len(self._idlefuns) <= 1, timeout)
162
154 def main(self): 163 def main(self):
155 self.cooker.pre_serve() 164 self.cooker.pre_serve()
156 165
@@ -174,6 +183,12 @@ class ProcessServer():
174 self.controllersock.close() 183 self.controllersock.close()
175 self.controllersock = False 184 self.controllersock = False
176 if self.haveui: 185 if self.haveui:
186 # Wait for the idle loop to have cleared (30s max)
187 self.wait_for_idle(30)
188 if self.cooker.command.currentAsyncCommand is not None:
189 serverlog("Idle loop didn't finish queued commands after 30s, exiting.")
190 self.quit = True
191
177 fds.remove(self.command_channel) 192 fds.remove(self.command_channel)
178 bb.event.unregister_UIHhandler(self.event_handle, True) 193 bb.event.unregister_UIHhandler(self.event_handle, True)
179 self.command_channel_reply.writer.close() 194 self.command_channel_reply.writer.close()
@@ -185,7 +200,7 @@ class ProcessServer():
185 self.cooker.clientComplete() 200 self.cooker.clientComplete()
186 self.haveui = False 201 self.haveui = False
187 ready = select.select(fds,[],[],0)[0] 202 ready = select.select(fds,[],[],0)[0]
188 if newconnections: 203 if newconnections and not self.quit:
189 serverlog("Starting new client") 204 serverlog("Starting new client")
190 conn = newconnections.pop(-1) 205 conn = newconnections.pop(-1)
191 fds.append(conn) 206 fds.append(conn)
@@ -257,7 +272,7 @@ class ProcessServer():
257 continue 272 continue
258 try: 273 try:
259 serverlog("Running command %s" % command) 274 serverlog("Running command %s" % command)
260 self.command_channel_reply.send(self.cooker.command.runCommand(command)) 275 self.command_channel_reply.send(self.cooker.command.runCommand(command, self))
261 serverlog("Command Completed (socket: %s)" % os.path.exists(self.sockname)) 276 serverlog("Command Completed (socket: %s)" % os.path.exists(self.sockname))
262 except Exception as e: 277 except Exception as e:
263 stack = traceback.format_exc() 278 stack = traceback.format_exc()
@@ -285,6 +300,9 @@ class ProcessServer():
285 300
286 ready = self.idle_commands(.1, fds) 301 ready = self.idle_commands(.1, fds)
287 302
303 if self.idle:
304 self.idle.join()
305
288 serverlog("Exiting (socket: %s)" % os.path.exists(self.sockname)) 306 serverlog("Exiting (socket: %s)" % os.path.exists(self.sockname))
289 # Remove the socket file so we don't get any more connections to avoid races 307 # Remove the socket file so we don't get any more connections to avoid races
290 # The build directory could have been renamed so if the file isn't the one we created 308 # The build directory could have been renamed so if the file isn't the one we created
@@ -300,7 +318,7 @@ class ProcessServer():
300 self.sock.close() 318 self.sock.close()
301 319
302 try: 320 try:
303 self.cooker.shutdown(True) 321 self.cooker.shutdown(True, idle=False)
304 self.cooker.notifier.stop() 322 self.cooker.notifier.stop()
305 self.cooker.confignotifier.stop() 323 self.cooker.confignotifier.stop()
306 except: 324 except:
@@ -359,47 +377,60 @@ class ProcessServer():
359 msg.append(":\n%s" % procs) 377 msg.append(":\n%s" % procs)
360 serverlog("".join(msg)) 378 serverlog("".join(msg))
361 379
362 def idle_commands(self, delay, fds=None): 380 def idle_thread(self):
363 def remove_idle_func(function): 381 def remove_idle_func(function):
364 with self._idlefuncsLock: 382 with self._idlefuncsLock:
365 del self._idlefuns[function] 383 del self._idlefuns[function]
384 self.idle_cond.notify_all()
366 385
367 nextsleep = delay 386 while not self.quit:
368 if not fds: 387 nextsleep = 0.1
369 fds = [] 388 fds = []
370 389
371 with self._idlefuncsLock: 390 with self._idlefuncsLock:
372 items = list(self._idlefuns.items()) 391 items = list(self._idlefuns.items())
373 392
374 for function, data in items: 393 for function, data in items:
375 try: 394 try:
376 retval = function(self, data, False) 395 retval = function(self, data, False)
377 if isinstance(retval, idleFinish): 396 if isinstance(retval, idleFinish):
378 serverlog("Removing idle function %s at idleFinish" % str(function)) 397 serverlog("Removing idle function %s at idleFinish" % str(function))
379 remove_idle_func(function) 398 remove_idle_func(function)
380 self.cooker.command.finishAsyncCommand(retval.msg) 399 self.cooker.command.finishAsyncCommand(retval.msg)
381 nextsleep = None 400 nextsleep = None
382 elif retval is False: 401 elif retval is False:
383 serverlog("Removing idle function %s" % str(function)) 402 serverlog("Removing idle function %s" % str(function))
403 remove_idle_func(function)
404 nextsleep = None
405 elif retval is True:
406 nextsleep = None
407 elif isinstance(retval, float) and nextsleep:
408 if (retval < nextsleep):
409 nextsleep = retval
410 elif nextsleep is None:
411 continue
412 else:
413 fds = fds + retval
414 except SystemExit:
415 raise
416 except Exception as exc:
417 if not isinstance(exc, bb.BBHandledException):
418 logger.exception('Running idle function')
384 remove_idle_func(function) 419 remove_idle_func(function)
385 nextsleep = None 420 serverlog("Exception %s broke the idle_thread, exiting" % traceback.format_exc())
386 elif retval is True: 421 self.quit = True
387 nextsleep = None 422
388 elif isinstance(retval, float) and nextsleep: 423 if nextsleep is not None:
389 if (retval < nextsleep): 424 select.select(fds,[],[],nextsleep)[0]
390 nextsleep = retval 425
391 elif nextsleep is None: 426 def idle_commands(self, delay, fds=None):
392 continue 427 nextsleep = delay
393 else: 428 if not fds:
394 fds = fds + retval 429 fds = []
395 except SystemExit: 430
396 raise 431 if not self.idle:
397 except Exception as exc: 432 self.idle = threading.Thread(target=self.idle_thread)
398 if not isinstance(exc, bb.BBHandledException): 433 self.idle.start()
399 logger.exception('Running idle function')
400 remove_idle_func(function)
401 serverlog("Exception %s broke the idle_thread, exiting" % traceback.format_exc())
402 self.quit = True
403 434
404 # Create new heartbeat event? 435 # Create new heartbeat event?
405 now = time.time() 436 now = time.time()
@@ -592,7 +623,7 @@ def execServer(lockfd, readypipeinfd, lockname, sockname, server_timeout, xmlrpc
592 writer = ConnectionWriter(readypipeinfd) 623 writer = ConnectionWriter(readypipeinfd)
593 try: 624 try:
594 featureset = [] 625 featureset = []
595 cooker = bb.cooker.BBCooker(featureset, server.register_idle_function) 626 cooker = bb.cooker.BBCooker(featureset, server.register_idle_function, server.wait_for_idle)
596 cooker.configuration.profile = profile 627 cooker.configuration.profile = profile
597 except bb.BBHandledException: 628 except bb.BBHandledException:
598 return None 629 return None
diff --git a/bitbake/lib/bb/server/xmlrpcserver.py b/bitbake/lib/bb/server/xmlrpcserver.py
index 01f55538ae..2e65dc34a9 100644
--- a/bitbake/lib/bb/server/xmlrpcserver.py
+++ b/bitbake/lib/bb/server/xmlrpcserver.py
@@ -118,7 +118,7 @@ class BitBakeXMLRPCServerCommands():
118 """ 118 """
119 Run a cooker command on the server 119 Run a cooker command on the server
120 """ 120 """
121 return self.server.cooker.command.runCommand(command, self.server.readonly) 121 return self.server.cooker.command.runCommand(command, self.server, self.server.readonly)
122 122
123 def getEventHandle(self): 123 def getEventHandle(self):
124 return self.event_handle 124 return self.event_handle