Skip to content

Commit 06194e9

Browse files
committed
test: change way api_server timings are determined
Previously, when checking that api server requests complete in under 700ms, we measured this client-side, which included whatever latency was added by python/the network stack. Now we instead directly utilize the timing information firecracker provides in its logs in our integration tests (a log message for this was previously present, and has been upgraded to info level so that we can access it in the integration tests). Signed-off-by: Patrick Roy <[email protected]>
1 parent 9d7080a commit 06194e9

File tree

8 files changed

+72
-83
lines changed

8 files changed

+72
-83
lines changed

tests/framework/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def build_from_snapshot(
214214
)
215215
vm.jailer.daemonize = daemonize
216216
vm.spawn(log_level="Error", use_ramdisk=use_ramdisk)
217-
vm.api_session.untime()
217+
vm.time_api_requests = False
218218

219219
metrics_file_path = os.path.join(vm.path, "metrics.log")
220220
metrics_fifo = log_tools.Fifo(metrics_file_path)

tests/framework/decorators.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

tests/framework/http.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import requests
66
from requests_unixsocket import DEFAULT_SCHEME, UnixAdapter
77

8-
from framework import decorators
9-
108

119
class Session(requests.Session):
1210
"""Wrapper over requests_unixsocket.Session limiting the call duration.
@@ -75,30 +73,3 @@ def is_status_payload_too_large(response: int):
7573
self.is_status_bad_request = is_status_bad_request
7674
self.is_status_not_found = is_status_not_found
7775
self.is_status_payload_too_large = is_status_payload_too_large
78-
79-
@decorators.timed_request
80-
def get(self, url, **kwargs):
81-
"""Wrap the GET call with duration limit."""
82-
# pylint: disable=method-hidden
83-
# The `untime` method overrides this, and pylint disapproves.
84-
return super().get(url, **kwargs)
85-
86-
@decorators.timed_request
87-
def patch(self, url, data=None, **kwargs):
88-
"""Wrap the PATCH call with duration limit."""
89-
# pylint: disable=method-hidden
90-
# The `untime` method overrides this, and pylint disapproves.
91-
return super().patch(url, data=data, **kwargs)
92-
93-
@decorators.timed_request
94-
def put(self, url, data=None, **kwargs):
95-
"""Wrap the PUT call with duration limit."""
96-
# pylint: disable=method-hidden
97-
# The `untime` method overrides this, and pylint disapproves.
98-
return super().put(url, data=data, **kwargs)
99-
100-
def untime(self):
101-
"""Restore the HTTP methods to their un-timed selves."""
102-
self.get = super().get
103-
self.patch = super().patch
104-
self.put = super().put

tests/framework/microvm.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import json
1212
import logging
1313
import os
14+
import re
1415
import select
1516
import shutil
1617
import time
1718
import uuid
1819
import weakref
20+
from collections import namedtuple
1921
from functools import cached_property
2022
from pathlib import Path
2123
from threading import Lock
@@ -28,7 +30,7 @@
2830
import host_tools.memory as mem_tools
2931
import host_tools.network as net_tools
3032
from framework import utils
31-
from framework.defs import FC_PID_FILE_NAME
33+
from framework.defs import FC_PID_FILE_NAME, MAX_API_CALL_DURATION_MS
3234
from framework.http import Session
3335
from framework.jailer import JailerContext
3436
from framework.resources import (
@@ -149,6 +151,8 @@ def __init__(
149151
self._screen_pid = None
150152
self._screen_log = None
151153

154+
self.time_api_requests = True
155+
152156
# Initalize memory monitor
153157
self.memory_monitor = None
154158

@@ -205,6 +209,9 @@ def kill(self):
205209
self.expect_kill_by_signal = True
206210
utils.run_cmd("kill -9 {} || true".format(self.screen_pid))
207211

212+
if self.time_api_requests:
213+
self._validate_api_response_times()
214+
208215
# Check if Firecracker was launched by the jailer in a new pid ns.
209216
fc_pid_in_new_ns = self.pid_in_new_ns
210217

@@ -219,6 +226,59 @@ def kill(self):
219226
self.memory_monitor.join(timeout=1)
220227
self.memory_monitor.check_samples()
221228

229+
def _validate_api_response_times(self):
230+
"""
231+
Parses the firecracker logs for information regarding api server request processing times, and asserts they
232+
are within acceptable bounds.
233+
"""
234+
# Log messages are either
235+
# 2023-06-16T07:45:41.767987318 [fc44b23e-ce47-4635-9549-5779a6bd9cee:fc_api] The API server received a Get request on "/mmds".
236+
# or
237+
# 2023-06-16T07:47:31.204704732 [2f2427c7-e4de-4226-90e6-e3556402be84:fc_api] The API server received a Put request on "/actions" with body "{\"action_type\": \"InstanceStart\"}".
238+
api_request_regex = re.compile(
239+
r"\] The API server received a (?P<method>\w+) request on \"(?P<url>(/(\w|-)*)+)\"( with body (?P<body>.*))?\."
240+
)
241+
api_request_times_regex = re.compile(
242+
r"\] Total previous API call duration: (?P<execution_time>\d+) us.$"
243+
)
244+
245+
# Note: Processing of api requests is synchronous, so these messages cannot be torn by concurrency effects
246+
log_lines = self.log_data.split("\n")
247+
248+
ApiCall = namedtuple("ApiCall", "method url body")
249+
250+
current_call = None
251+
252+
for log_line in log_lines:
253+
match = api_request_regex.search(log_line)
254+
255+
if match:
256+
if current_call is not None:
257+
raise Exception(
258+
f"API call duration log entry for {current_call.method} {current_call.url} with body {current_call.body} is missing!"
259+
)
260+
261+
current_call = ApiCall(
262+
match.group("method"), match.group("url"), match.group("body")
263+
)
264+
265+
match = api_request_times_regex.search(log_line)
266+
267+
if match:
268+
if current_call is None:
269+
raise Exception(
270+
"Got API call duration log entry before request entry"
271+
)
272+
273+
if current_call.url != "/snapshot/create":
274+
exec_time = float(match.group("execution_time")) / 1000.0
275+
276+
assert (
277+
exec_time <= MAX_API_CALL_DURATION_MS
278+
), f"{current_call.method} {current_call.url} API call exceeded maximum duration: {exec_time} ms. Body: {current_call.body}"
279+
280+
current_call = None
281+
222282
@property
223283
def firecracker_version(self):
224284
"""Return the version of the Firecracker executable."""
@@ -392,7 +452,7 @@ def spawn(
392452
self,
393453
create_logger=True,
394454
log_file="log_fifo",
395-
log_level="Info",
455+
log_level="Debug",
396456
use_ramdisk=False,
397457
metrics_path=None,
398458
):
@@ -765,7 +825,6 @@ def pause_to_snapshot(
765825
response = self.vm.patch(state="Paused")
766826
assert self.api_session.is_status_no_content(response.status_code)
767827

768-
self.api_session.untime()
769828
response = self.snapshot.create(
770829
mem_file_path=mem_file_path,
771830
snapshot_path=snapshot_path,

tests/framework/resources.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ def __init__(self, api_usocket_full_name, api_session):
381381

382382
def put(self, **args):
383383
"""Create a snapshot of the microvm."""
384-
self._api_session.untime()
385384
datax = self.create_json(**args)
386385
return self._api_session.put("{}".format(self._snapshot_cfg_url), json=datax)
387386

tests/integration_tests/functional/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,7 @@ def test_map_private_seccomp_regression(test_microvm_with_api):
14001400
{"http-api-max-payload-size": str(1024 * 1024 * 2)}
14011401
)
14021402
test_microvm.spawn()
1403-
test_microvm.api_session.untime()
1403+
test_microvm.time_api_request = False
14041404

14051405
response = test_microvm.mmds.get()
14061406
assert test_microvm.api_session.is_status_ok(response.status_code)

tests/integration_tests/functional/test_logging.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ def test_log_config_failure(test_microvm_with_api):
124124
show_level=True,
125125
show_log_origin=True,
126126
)
127+
# only works if log level is Debug
128+
microvm.time_api_requests = False
127129
assert microvm.api_session.is_status_bad_request(response.status_code)
128130
assert response.json()["fault_message"]
129131

@@ -146,6 +148,8 @@ def test_api_requests_logs(test_microvm_with_api):
146148
show_level=True,
147149
show_log_origin=True,
148150
)
151+
# only works if log level is Debug
152+
microvm.time_api_requests = False
149153
assert microvm.api_session.is_status_no_content(response.status_code)
150154
microvm.start_console_logger(log_fifo)
151155

@@ -206,6 +210,8 @@ def test_api_requests_logs(test_microvm_with_api):
206210
def _test_log_config(microvm, log_level="Info", show_level=True, show_origin=True):
207211
"""Exercises different scenarios for testing the logging config."""
208212
microvm.spawn(create_logger=False)
213+
# only works if log level is Debug
214+
microvm.time_api_requests = False
209215

210216
# Configure logging.
211217
log_fifo_path = os.path.join(microvm.path, "log_fifo")

tests/integration_tests/functional/test_snapshot_basic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ def test_load_snapshot_failure_handling(test_microvm_with_api):
191191
logger = logging.getLogger("snapshot_load_failure")
192192
vm = test_microvm_with_api
193193
vm.spawn(log_level="Info")
194+
# only works if log level is Debug
195+
vm.time_api_requests = False
194196

195197
# Create two empty files for snapshot state and snapshot memory
196198
chroot_path = vm.jailer.chroot_path()

0 commit comments

Comments
 (0)