Skip to content

Commit 3300820

Browse files
authored
SIGHUP handler (#154)
* Implement signal handler * flake8 * Fix the keep-alive issue * Release 0.13.17
1 parent 7906f48 commit 3300820

7 files changed

Lines changed: 101 additions & 51 deletions

File tree

docs/Changelog.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v0.13.17 - 2021-10-25
4+
5+
- handle SUGHUP to reload configuration from disk and restart
6+
37
## v0.13.16 - 2021-10-08
48

59
- add Windows Installer to release assets
@@ -181,8 +185,6 @@
181185
- allow enabling multiple tags + allow response to trigger tag up/down => state machine for complex scenarios
182186

183187
## Other
184-
- Have SIGHUP handler
185-
186188
- Nicer logging of requests, with special option to enable it.
187189
- Nicer formatted error pages for known errors, explaining the problem
188190

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ enabled in startup time, e.g. `mockintosh --enable-tags first,second`
169169

170170
Using `--sample-config` will cause Mockintosh to write the example configuration file into specified location.
171171

172+
_Note: sending SIGHUP to Mockintosh's process will cause it to re-read configuration file and restart the server._
173+
172174
### OpenAPI Specification to Mockintosh Config Conversion (_experimental_)
173175

174176
_Note: This feature is experimental. One-to-one transpilation of OAS documents is not guaranteed._

mockintosh/__init__.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def run(
9090
tags: list = [],
9191
load_override: Union[dict, None] = None
9292
):
93-
queue, _ = start_render_queue()
93+
queue, render_thread = start_render_queue()
9494

9595
if address: # pragma: no cover
9696
logging.info('Bind address: %s', address)
@@ -111,8 +111,22 @@ def run(
111111
logging.exception('Mock server loading error:')
112112
with _nostderr():
113113
raise
114+
115+
prev_handler = signal.getsignal(signal.SIGHUP)
116+
do_restart = [False] # mutable
117+
118+
def sighup_handler(num, frame):
119+
logging.info("Received SIGHUP")
120+
http_server.stop()
121+
render_thread.kill()
122+
signal.signal(signal.SIGHUP, prev_handler)
123+
do_restart[0] = True
124+
125+
signal.signal(signal.SIGHUP, sighup_handler)
114126
http_server.run()
115127

128+
return do_restart[0]
129+
116130

117131
def _gracefully_exit(num, frame):
118132
atexit._run_exitfuncs()
@@ -271,15 +285,9 @@ def initiate(argv=None):
271285
logging.info("%s v%s is starting...", PROGRAM.capitalize(), __version__)
272286

273287
if not cov_no_run: # pragma: no cover
274-
run(
275-
source,
276-
debug=debug_mode,
277-
interceptors=interceptors,
278-
address=address,
279-
services_list=services_list,
280-
tags=tags,
281-
load_override=load_override
282-
)
288+
while run(source, debug=debug_mode, interceptors=interceptors, address=address,
289+
services_list=services_list, tags=tags, load_override=load_override):
290+
logging.info("Restarting...")
283291

284292

285293
def demo_run():

mockintosh/management.py

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import threading
1616
from typing import (
1717
Union,
18-
Tuple
18+
Tuple, Optional, Awaitable
1919
)
2020
from collections import OrderedDict
2121
from urllib.parse import parse_qs, unquote
2222

2323
import yaml
24+
import yaml.scanner
25+
import yaml.parser
2426
from yaml.representer import Representer
2527
import jsonschema
2628
import tornado.web
@@ -29,13 +31,8 @@
2931

3032
import mockintosh
3133
from mockintosh.constants import PROGRAM
32-
from mockintosh.config import (
33-
ConfigService,
34-
ConfigExternalFilePath
35-
)
36-
from mockintosh.services.http import (
37-
HttpService
38-
)
34+
from mockintosh.config import ConfigExternalFilePath
35+
from mockintosh.services.http import HttpService
3936
from mockintosh.builders import ConfigRootBuilder
4037
from mockintosh.handlers import GenericHandler
4138
from mockintosh.helpers import _safe_path_split, _b64encode, _urlsplit
@@ -46,14 +43,8 @@
4643
AsyncProducerDatasetLoopEnd,
4744
InternalResourcePathCheckError
4845
)
49-
from mockintosh.services.asynchronous import (
50-
AsyncService,
51-
AsyncActor,
52-
AsyncProducer,
53-
AsyncConsumer,
54-
AsyncConsumerGroup
55-
)
56-
from mockintosh.services.asynchronous._looping import run_loops as async_run_loops
46+
from mockintosh.services.asynchronous import AsyncService, AsyncProducer, AsyncConsumer
47+
from mockintosh.services.asynchronous._looping import run_loops as async_run_loops, stop_loops
5748
from mockintosh.replicas import Request, Response
5849

5950
POST_CONFIG_RESTRICTED_FIELDS = ('port', 'hostname', 'ssl', 'sslCertFile', 'sslKeyFile')
@@ -109,7 +100,6 @@ def _reset_iterators(app):
109100

110101

111102
class ManagementBaseHandler(tornado.web.RequestHandler):
112-
113103
def write(self, chunk: Union[str, bytes, dict]) -> None:
114104
if self._finished: # pragma: no cover
115105
raise RuntimeError("Cannot write() after finish()")
@@ -131,6 +121,9 @@ def _log(self) -> None:
131121
if logging.DEBUG >= logging.root.level:
132122
self.application.log_request(self)
133123

124+
def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
125+
pass
126+
134127

135128
class ManagementRootHandler(ManagementBaseHandler):
136129

@@ -166,21 +159,10 @@ async def post(self):
166159
if not self.check_restricted_fields(service, i):
167160
return
168161

169-
for actor in AsyncActor.actors:
170-
actor.stop = True
171-
172-
for consumer_group in AsyncConsumerGroup.groups:
173-
consumer_group.stop = True
162+
stop_loops()
163+
self.http_server.clear_lists()
174164

175165
definition.stats.services = []
176-
AsyncService.services = []
177-
AsyncActor.actors = []
178-
AsyncProducer.producers = []
179-
AsyncConsumer.consumers = []
180-
AsyncConsumerGroup.groups = []
181-
HttpService.services = []
182-
ConfigService.services = []
183-
ConfigExternalFilePath.files = []
184166
definition.services, definition.config_root = definition.analyze(data)
185167

186168
for service in HttpService.services:
@@ -298,7 +280,7 @@ async def get(self):
298280
self.write(self.logs.json())
299281

300282
async def post(self):
301-
enabled = not self.get_body_argument('enable', default=True) in ('false', 'False', '0')
283+
enabled = not self.get_body_argument('enable', default='True') in ('false', 'False', '0')
302284
for service in self.logs.services:
303285
service.enabled = enabled
304286
self.set_status(204)

mockintosh/res/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.13.16
1+
0.13.17

mockintosh/servers.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import tornado.web
2323
from tornado.routing import Rule, RuleRouter, HostMatches
2424

25+
from mockintosh.config import ConfigService, ConfigExternalFilePath
2526
from mockintosh.definition import Definition
2627
from mockintosh.exceptions import CertificateLoadingError
2728
from mockintosh.handlers import GenericHandler
@@ -49,7 +50,7 @@
4950
ManagementServiceTagHandler,
5051
UnhandledData
5152
)
52-
from mockintosh.services.asynchronous._looping import run_loops as async_run_loops
53+
from mockintosh.services.asynchronous._looping import run_loops as async_run_loops, stop_loops as async_stop_loops
5354
from mockintosh.services.http import (
5455
HttpService,
5556
HttpPath,
@@ -75,8 +76,16 @@ def get_server(
7576
def serve(self):
7677
raise NotImplementedError
7778

79+
@abstractmethod
80+
def stop(self):
81+
raise NotImplementedError
82+
7883

7984
class TornadoImpl(Impl):
85+
def __init__(self) -> None:
86+
super().__init__()
87+
self.ioloop = None
88+
self.servers = []
8089

8190
def get_server(
8291
self,
@@ -89,14 +98,32 @@ def get_server(
8998
else:
9099
server = tornado.web.HTTPServer(router)
91100

101+
self.servers.append(server)
92102
return server
93103

94104
def serve(self) -> None:
105+
self.ioloop = tornado.ioloop.IOLoop.current()
106+
logging.debug("Starting ioloop: %s", self.ioloop)
95107
try:
96-
tornado.ioloop.IOLoop.current().start()
108+
self.ioloop.start()
97109
except KeyboardInterrupt:
98110
logging.debug("Shutdown: %s", traceback.format_exc())
99111

112+
logging.debug("IOLoop has completed")
113+
114+
def stop(self):
115+
logging.debug("Stopping servers...")
116+
for server in self.servers:
117+
logging.debug("Stopping: %s", server)
118+
self.ioloop.add_callback(server.close_all_connections)
119+
server.stop()
120+
121+
logging.debug("Stopping IOLoop...")
122+
logging.debug("%s", self.ioloop)
123+
self.ioloop.add_callback_from_signal(self.ioloop.stop)
124+
125+
logging.debug("TornadoImpl is stopped")
126+
100127

101128
class _Listener:
102129
def __init__(self, hostname: Union[str, None], port: int, address: Union[str, None]):
@@ -191,19 +218,20 @@ def load_service(self, service: HttpService, rules: list, ssl: bool, ssl_options
191218
http_path_list, management_root = self.prepare_app(service)
192219
app = self.make_app(service, http_path_list, self.globals, debug=self.debug, management_root=management_root)
193220
self._apps.apps[service.internal_http_service_id] = app
221+
address_str = self.address if self.address else 'localhost'
194222
self._apps.listeners[service.internal_http_service_id] = _Listener(
195223
service.hostname,
196224
service.port,
197-
self.address if self.address else 'localhost'
225+
address_str
198226
)
199227

200228
if service.hostname is None:
201229
server = self.impl.get_server(app, ssl, ssl_options)
230+
logging.debug('Will listen: %s:%d', address_str, service.port)
202231
server.listen(service.port, address=self.address)
203-
logging.debug('Will listen port number: %d', service.port)
204232
self.services_log.append('Serving at %s://%s:%s%s' % (
205233
protocol,
206-
self.address if self.address else 'localhost',
234+
address_str,
207235
service.port,
208236
' the mock for %r' % service.get_name_or_empty()
209237
))
@@ -250,8 +278,8 @@ def load(self) -> None:
250278
if rules:
251279
router = RuleRouter(rules)
252280
server = self.impl.get_server(router, ssl, ssl_options)
281+
logging.debug('Listening on port: %s:%d', self.address, service.port)
253282
server.listen(services[0].port, address=self.address)
254-
logging.debug('Will listen port number: %d', service.port)
255283

256284
self.load_management_api()
257285

@@ -538,10 +566,24 @@ def load_management_api(self) -> None:
538566
)
539567
)
540568
])
569+
logging.debug("Listening on port %s:%s", self.address, config_management.port)
541570
server = self.impl.get_server(app, ssl, ssl_options)
542571
server.listen(config_management.port, address=self.address)
543572
self.services_log.append('Serving management UI+API at %s://%s:%s' % (
544573
protocol,
545574
self.address if self.address else 'localhost',
546575
config_management.port
547576
))
577+
578+
def clear_lists(self):
579+
HttpService.services = []
580+
ConfigService.services = []
581+
ConfigExternalFilePath.files = []
582+
583+
def stop(self):
584+
logging.info("Stopping server...")
585+
self.impl.stop()
586+
logging.debug("Stoppping async actor threads")
587+
async_stop_loops()
588+
self.clear_lists()
589+
logging.debug("Done shutdown")

mockintosh/services/asynchronous/_looping.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
import threading
1111

12-
from mockintosh.services.asynchronous import AsyncService, AsyncConsumerGroup
12+
from mockintosh.services.asynchronous import AsyncService, AsyncConsumerGroup, AsyncActor, AsyncProducer, AsyncConsumer
1313
from mockintosh.services.asynchronous.kafka import KafkaConsumerGroup # noqa: F401
1414
from mockintosh.services.asynchronous.amqp import AmqpConsumerGroup # noqa: F401
1515
from mockintosh.services.asynchronous.redis import RedisConsumerGroup # noqa: F401
@@ -49,3 +49,17 @@ def run_loops():
4949
t = threading.Thread(target=consumer_group.consume, args=(), kwargs={})
5050
t.daemon = True
5151
t.start()
52+
53+
54+
def stop_loops():
55+
for actor in AsyncActor.actors:
56+
actor.stop = True
57+
58+
for consumer_group in AsyncConsumerGroup.groups:
59+
consumer_group.stop = True
60+
61+
AsyncService.services = []
62+
AsyncActor.actors = []
63+
AsyncProducer.producers = []
64+
AsyncConsumer.consumers = []
65+
AsyncConsumerGroup.groups = []

0 commit comments

Comments
 (0)