zuul/zuul/lib/commandsocket.py
James E. Blair a160484a86 Add zuul-scheduler tenant-reconfigure
This is a new reconfiguration command which behaves like full-reconfigure
but only for a single tenant.  This can be useful after connection issues
with code hosting systems, or potentially with Zuul cache bugs.

Because this is the first command-socket command with an argument, some
command-socket infrastructure changes are necessary.  Additionally, this
includes some minor changes to make the services more consistent around
socket commands.

Change-Id: Ib695ab8e7ae54790a0a0e4ac04fdad96d60ee0c9
2022-02-08 14:14:17 -08:00

142 lines
4.2 KiB
Python

# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# Copyright 2016 Red Hat
# Copyright 2022 Acme Gating, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import os
import socket
import threading
from zuul.lib.queue import NamedQueue
class Command:
name = None
help = None
args = []
class Argument:
name = None
help = None
required = None
default = None
class StopCommand(Command):
name = 'stop'
help = 'Stop the running process'
class GracefulCommand(Command):
name = 'graceful'
help = 'Stop after completing existing work'
class PauseCommand(Command):
name = 'pause'
help = 'Stop accepting new work'
class UnPauseCommand(Command):
name = 'unpause'
help = 'Resume accepting new work'
class ReplCommand(Command):
name = 'repl'
help = 'Enable the REPL for debugging'
class NoReplCommand(Command):
name = 'norepl'
help = 'Disable the REPL'
class CommandSocket(object):
log = logging.getLogger("zuul.CommandSocket")
def __init__(self, path):
self.running = False
self.path = path
self.queue = NamedQueue('CommandSocketQueue')
def start(self):
self.running = True
if os.path.exists(self.path):
os.unlink(self.path)
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.bind(self.path)
self.socket.listen(1)
self.socket_thread = threading.Thread(target=self._socketListener)
self.socket_thread.daemon = True
self.socket_thread.start()
def stop(self):
# First, wake up our listener thread with a connection and
# tell it to stop running.
self.running = False
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(self.path)
s.sendall(b'_stop\n')
# The command '_stop' will be ignored by our listener, so
# directly inject it into the queue so that consumers of this
# class which are waiting in .get() are awakened. They can
# either handle '_stop' or just ignore the unknown command and
# then check to see if they should continue to run before
# re-entering their loop.
self.queue.put(('_stop', []))
self.socket_thread.join()
def _socketListener(self):
while self.running:
try:
s, addr = self.socket.accept()
self.log.debug("Accepted socket connection %s" % (s,))
buf = b''
while True:
buf += s.recv(1)
if buf[-1:] == b'\n':
break
buf = buf.strip()
self.log.debug("Received %s from socket" % (buf,))
s.close()
buf = buf.decode('utf8')
parts = buf.split(' ', 1)
# Because we use '_stop' internally to wake up a
# waiting thread, don't allow it to actually be
# injected externally.
args = parts[1:]
if args:
args = json.loads(args[0])
if parts[0] != '_stop':
self.queue.put((parts[0], args))
except Exception:
self.log.exception("Exception in socket handler")
# Unlink socket file within the thread so join works and we don't
# leak the socket file.
self.socket.close()
if os.path.exists(self.path):
os.unlink(self.path)
def get(self):
if not self.running:
raise Exception("CommandSocket.get called while stopped")
return self.queue.get()