jsoncomm: transparently handle huge messages via fds

The existing jsoncomm is a work of beautiy. For very big arguments
however the used `SOCK_SEQPACKET` hits the limitations of the
kernel network buffer size (see also [0]). This lead to various
workarounds in #824,#1331,#1836 where parts of the request are
encoded as part of the json method call and parts are done via
a side-channel via fd-passing.

This commit changes the code so that the fd channel is automatically
and transparently created and the workarounds are removed. A test
is added that ensures that very big messages can be passed.

[0] https://github.com/osbuild/osbuild/pull/1833
This commit is contained in:
Michael Vogt 2024-08-07 16:49:30 +02:00 committed by Achilleas Koutsou
parent d67fa48c17
commit 0abdfb9041
6 changed files with 130 additions and 87 deletions

View file

@ -17,17 +17,15 @@ osbuild is the path. The input options are just passed to the
"""
import abc
import contextlib
import hashlib
import json
import os
import tempfile
from typing import Any, Dict, Optional, Tuple
from osbuild import host
from osbuild.util.types import PathLike
from .objectstore import ObjectStore, StoreClient, StoreServer
from .objectstore import StoreClient, StoreServer
class Input:
@ -67,7 +65,7 @@ class InputManager:
self.root = root
self.inputs: Dict[str, Input] = {}
def map(self, ip: Input, store: ObjectStore) -> Tuple[str, Dict]:
def map(self, ip: Input) -> Tuple[str, Dict]:
target = os.path.join(self.root, ip.name)
os.makedirs(target)
@ -88,12 +86,8 @@ class InputManager:
}
}
with make_args_and_reply_files(store.tmp, args) as (fd_args, fd_reply):
fds = [fd_args, fd_reply]
client = self.service_manager.start(f"input/{ip.name}", ip.info.path)
_, _ = client.call_with_fds("map", {}, fds)
with os.fdopen(os.dup(fd_reply)) as f:
reply = json.loads(f.read())
client = self.service_manager.start(f"input/{ip.name}", ip.info.path)
reply = client.call("map", args)
path = reply["path"]
@ -107,15 +101,6 @@ class InputManager:
return reply
@contextlib.contextmanager
def make_args_and_reply_files(tmp, args):
with tempfile.TemporaryFile("w+", dir=tmp, encoding="utf-8") as f_args, \
tempfile.TemporaryFile("w+", dir=tmp, encoding="utf-8") as f_reply:
json.dump(args, f_args)
f_args.seek(0)
yield f_args.fileno(), f_reply.fileno()
class InputService(host.Service):
"""Input host service"""
@ -129,21 +114,14 @@ class InputService(host.Service):
def stop(self):
self.unmap()
def dispatch(self, method: str, _, fds):
def dispatch(self, method: str, args, fds):
if method == "map":
# map() sends fd[0] to read the arguments from and fd[1] to
# write the reply back. This avoids running into EMSGSIZE
with os.fdopen(fds.steal(0)) as f:
args = json.load(f)
store = StoreClient(connect_to=args["api"]["store"])
r = self.map(store,
args["origin"],
args["refs"],
args["target"],
args["options"])
with os.fdopen(fds.steal(1), "w") as f:
f.write(json.dumps(r))
f.seek(0)
return "{}", None
return r, None
raise host.ProtocolError("Unknown method")