diff --git a/.github/scripts/flatbuffers-on.py b/.github/scripts/flatbuffers-on.py new file mode 100644 index 0000000000..0787c0b043 --- /dev/null +++ b/.github/scripts/flatbuffers-on.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +"""Run the focused FlatBuffers ON gates; never accept empty or skipped tests.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import re +import shlex +import shutil +import signal +import subprocess +import sys +import tarfile +import time +import xml.etree.ElementTree as ET + +# Keep the FlatBuffers pin synchronized with MODULE.bazel and WORKSPACE. +FLATBUFFERS_VERSION = "25.2.10" +FLATBUFFERS_SHA256 = "b9c2df49707c57a48fc0923d52b8c73beb72d675f9d44b2211e4569be40a7421" +GTEST_VERSION = "1.14.0" +GTEST_SHA256 = "8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7" +TESTS = {"brpc_flatbuffers_unittest": 21, "brpc_flatbuffers_protocol_unittest": 33} + + +def list_gtests(text): + names = set() + suite = None + for line in text.splitlines(): + value = line.split("#", 1)[0].strip() + if not value: + continue + if not line[0].isspace() and value.endswith("."): + suite = value + elif line[0].isspace() and suite and re.fullmatch(r"[A-Za-z0-9_/]+", value): + name = suite + value + if any(part.startswith("DISABLED_") for part in re.split(r"[./]", name)): + raise ValueError("Disabled test in ON gate: " + name) + if name in names: + raise ValueError("Duplicate discovered test: " + name) + names.add(name) + if not names: + raise ValueError("No GoogleTest cases discovered") + return names + + +def xml_cases(path): + path = Path(path) + if not path.is_file() or path.stat().st_size > 16 * 1024 * 1024: + raise ValueError("Missing or oversized test XML: " + str(path)) + root = ET.parse(path).getroot() + for suite in root.iter(): + if suite.tag in ("testsuites", "testsuite"): + for attribute in ("failures", "errors", "disabled", "skipped"): + if int(suite.get(attribute, "0")) != 0: + raise ValueError("Nonzero " + attribute + " in " + str(path)) + cases = list(root.iter("testcase")) + if not cases: + raise ValueError("Test XML contains no cases: " + str(path)) + for case in cases: + if (case.find("failure") is not None or case.find("error") is not None or + case.find("skipped") is not None or case.get("status") == "notrun" or + case.get("result") in ("skipped", "suppressed")): + raise ValueError("Failed or unexecuted case: " + str(case.attrib)) + return root, cases + + +def verify_gtests(xml, listing, minimum): + expected = list_gtests(Path(listing).read_text()) + root, cases = xml_cases(xml) + actual = [case.get("classname", "") + "." + case.get("name", "") for case in cases] + if len(expected) < minimum or len(actual) != len(set(actual)) or set(actual) != expected: + raise ValueError("Executed cases differ from discovery or minimum: " + str(xml)) + if int(root.get("tests", "-1")) != len(actual): + raise ValueError("Incorrect XML test count: " + str(xml)) + return {"executed": len(actual), "failed": 0, "skipped": 0} + + +def verify_ctest(xml, required): + _, cases = xml_cases(xml) + names = [case.get("name", "") for case in cases] + if len(names) != len(set(names)) or not set(required) <= set(names): + raise ValueError("Required CTest cases did not execute: " + str(required)) + return {"executed": len(names), "names": names, "failed": 0, "skipped": 0} + + +def extract_archive(archive, digest, destination, prefix): + archive = Path(archive) + if hashlib.sha256(archive.read_bytes()).hexdigest() != digest: + raise ValueError("Dependency checksum mismatch: " + str(archive)) + destination = Path(destination) + if destination.exists(): + raise ValueError("Refusing to overwrite dependency directory") + with tarfile.open(archive, "r:gz") as source: + members = source.getmembers() + if len(members) > 30000 or sum(item.size for item in members) > 512 * 1024 * 1024: + raise ValueError("Dependency archive exceeds extraction limits") + paths = set() + links = {} + for member in members: + path = PurePosixPath(member.name) + if (not path.parts or path.is_absolute() or ".." in path.parts or + path.parts[0] != prefix or path in paths or + not (member.isdir() or member.isfile() or member.issym())): + raise ValueError("Unsafe dependency archive member: " + member.name) + paths.add(path) + if member.issym(): + if len(path.parts) < 2: + raise ValueError("Archive root must not be a symlink") + links[path] = member + for path in paths: + if any(parent in links for parent in path.parents): + raise ValueError("Archive member traverses a symlink: " + str(path)) + for path, member in links.items(): + target = PurePosixPath(member.linkname) + if not member.linkname or target.is_absolute(): + raise ValueError("Unsafe archive symlink: " + member.name) + parts = list(path.parent.parts) + for part in target.parts: + if part == "..": + if len(parts) <= 1: + raise ValueError("Escaping archive symlink: " + member.name) + parts.pop() + else: + parts.append(part) + if not parts or parts[0] != prefix or PurePosixPath(*parts) in links: + raise ValueError("Escaping or chained archive symlink: " + member.name) + destination.mkdir(parents=True) + # Create links only after all regular data, so extraction never writes + # through a symlink. The pinned archives need no hardlinks/link chains. + source.extractall(destination, members=[member for member in members if not member.issym()]) + for path, member in links.items(): + link = destination / str(path) + link.parent.mkdir(parents=True, exist_ok=True) + link.symlink_to(member.linkname) + return destination / prefix + + +def check_enabled(header): + if not re.search(r"^\s*#\s*define\s+BRPC_WITH_FLATBUFFERS\s+1\s*$", + Path(header).read_text(), re.MULTILINE): + raise ValueError("FlatBuffers is not enabled in " + str(header)) + + +def signal_group(process, signum): + try: + os.killpg(process.pid, signum) + return True + except ProcessLookupError: + return False + + +def stop_process(process, grace=5): + # The group may outlive its leader; waiting for the leader alone is not + # sufficient when a compiler/test child ignores SIGTERM. + if signal_group(process, signal.SIGTERM): + try: + process.wait(timeout=grace) + except subprocess.TimeoutExpired: + pass + signal_group(process, signal.SIGKILL) + process.wait() + + +class Runner: + def __init__(self, args): + self.args = args + self.source = args.source.resolve(strict=True) + self.work = args.work.resolve() + if self.work == self.source or self.source in self.work.parents: + raise ValueError("--work must be outside the source checkout") + self.work.mkdir(parents=True, exist_ok=False) + self.evidence = self.work / "evidence" + self.evidence.mkdir() + self.env = dict(os.environ) + for name in list(self.env): + if name.startswith("GTEST_"): + self.env.pop(name) + self.env.update(GTEST_FILTER="*", GTEST_REPEAT="1") + self.steps = [] + self.results = {} + self.compiler = self.env.get("CC", "cc") + self.cxx = self.env.get("CXX", "c++") + self.prefixes = [path.resolve(strict=True) for path in args.dependency_prefix] + + def save(self, status): + (self.evidence / "summary.json").write_text(json.dumps({ + "status": status, "system": self.args.build_system, + "source": str(self.source), "steps": self.steps, "tests": self.results, + }, indent=2) + "\n") + + def step(self, name, command, cwd=None, timeout=900, env=None): + command = [str(part) for part in command] + log = self.evidence / (name + ".log") + directory = Path(cwd or self.source) + (self.evidence / (name + ".command")).write_text( + "cwd=" + str(directory) + "\n" + shlex.join(command) + "\n") + print("START", name, flush=True) + start = time.monotonic() + code = 1 + with log.open("w") as output: + process = subprocess.Popen(command, cwd=directory, env=env or self.env, + stdout=output, stderr=subprocess.STDOUT, + start_new_session=True) + try: + code = process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + stop_process(process) + code = 124 + print("Step exceeded timeout", file=output) + except BaseException: + stop_process(process) + raise + finally: + self.steps.append({"name": name, "exitcode": code, + "seconds": round(time.monotonic() - start, 3)}) + (self.evidence / (name + ".exitcode")).write_text(str(code) + "\n") + self.save("running") + print("END", name, "exit=" + str(code), flush=True) + if code != 0: + raise RuntimeError(name + " failed; see " + str(log)) + return log + + def download(self, name, version, digest): + archive = self.work / (name + ".tar.gz") + self.step("download-" + name, ["curl", "--fail", "--location", "--retry", "2", + "--connect-timeout", "20", "--max-time", "180", + "https://github.com/google/" + name + "/archive/refs/tags/v" + version + ".tar.gz", + "--output", archive], timeout=600) + return extract_archive(archive, digest, self.work / (name + "-source"), name + "-" + version) + + def prepare_dependencies(self): + if self.args.flatbuffers_prefix: + self.fb = self.args.flatbuffers_prefix.resolve(strict=True) + else: + source = self.download("flatbuffers", FLATBUFFERS_VERSION, FLATBUFFERS_SHA256) + self.fb = self.work / "dependencies" + build = self.work / "flatbuffers-build" + self.step("flatbuffers-configure", ["cmake", "-S", source, "-B", build, + "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", + "-DCMAKE_INSTALL_LIBDIR=lib", "-DCMAKE_INSTALL_PREFIX=" + str(self.fb), + "-DFLATBUFFERS_BUILD_TESTS=OFF", "-DFLATBUFFERS_BUILD_FLATC=ON", + "-DFLATBUFFERS_BUILD_FLATLIB=ON", "-DFLATBUFFERS_BUILD_SHAREDLIB=OFF", + "-DFLATBUFFERS_INSTALL=ON", "-DFLATBUFFERS_LIBCXX_WITH_CLANG=OFF"]) + self.step("flatbuffers-build", ["cmake", "--build", build, "--parallel", self.args.jobs]) + self.step("flatbuffers-install", ["cmake", "--install", build]) + self.flatc = self.fb / "bin/flatc" + version = self.step("flatc-version", [self.flatc, "--version"], timeout=10).read_text().strip() + base = (self.fb / "include/flatbuffers/base.h").read_text() + numbers = [re.search(r"#define\s+FLATBUFFERS_VERSION_" + part + r"\s+(\d+)", base) + for part in ("MAJOR", "MINOR", "REVISION")] + if (version != "flatc version " + FLATBUFFERS_VERSION or + not all(numbers) or ".".join(match.group(1) for match in numbers) != FLATBUFFERS_VERSION): + raise ValueError("Use matching pinned FlatBuffers headers and compiler") + self.fb_library = self.fb / "lib/libflatbuffers.a" + if not self.fb_library.is_file(): + raise ValueError("The generator needs " + str(self.fb_library)) + self.gtest = (self.args.gtest_source.resolve(strict=True) if self.args.gtest_source else + self.download("googletest", GTEST_VERSION, GTEST_SHA256)) + self.prefixes.insert(0, self.fb) + if self.args.build_system == "make": + build = self.work / "gtest-build" + gtest_prefix = self.work / "gtest-prefix" + self.step("gtest-configure", ["cmake", "-S", self.gtest, "-B", build, + "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib", + "-DBUILD_SHARED_LIBS=OFF", "-DCMAKE_INSTALL_PREFIX=" + str(gtest_prefix)]) + self.step("gtest-build", ["cmake", "--build", build, "--parallel", self.args.jobs]) + self.step("gtest-install", ["cmake", "--install", build]) + self.prefixes.insert(0, gtest_prefix) + self.env["PATH"] = os.pathsep.join(str(prefix / "bin") for prefix in self.prefixes) + os.pathsep + self.env.get("PATH", "") + self.step("protoc-version", [self.args.protoc or shutil.which("protoc", path=self.env["PATH"]), "--version"], timeout=10) + + def common_cmake(self): + options = ["-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-DCMAKE_PREFIX_PATH=" + ";".join(str(prefix) for prefix in self.prefixes), + "-DCMAKE_C_COMPILER=" + self.compiler, "-DCMAKE_CXX_COMPILER=" + self.cxx] + for prefix in self.prefixes: + if (prefix / "include/openssl/ssl.h").is_file(): + options.append("-DOPENSSL_ROOT_DIR=" + str(prefix)) + break + if self.args.protoc: + options.append("-DProtobuf_PROTOC_EXECUTABLE=" + str(self.args.protoc.resolve(strict=True))) + return options + + def check_gtest(self, name, binary, ctest_build=None): + listing = self.step(name + "-list", [binary, "--gtest_list_tests"], timeout=30) + xml = self.evidence / (name + ".xml") + env = dict(self.env, GTEST_OUTPUT="xml:" + str(xml)) + command = (["ctest", "--test-dir", ctest_build, "--no-tests=error", "--output-on-failure", + "--timeout", "300", "-R", "^" + name + "$"] if ctest_build else [binary]) + self.step(name, command, timeout=330, env=env) + self.results[name] = verify_gtests(xml, listing, TESTS[name]) + self.save("running") + + def ctest(self, name, build, required): + xml = self.evidence / (name + ".xml") + self.step(name, ["ctest", "--test-dir", build, "--no-tests=error", "--output-on-failure", + "--timeout", "300", "--output-junit", xml], timeout=600) + self.results[name] = verify_ctest(xml, required) + self.save("running") + + def run_cmake(self): + build = self.work / "build" + self.step("configure", ["cmake", "-S", self.source, "-B", build] + self.common_cmake() + [ + "-DWITH_FLATBUFFERS=ON", "-DBUILD_UNIT_TESTS=ON", "-DBUILD_BRPC_TOOLS=OFF", + "-DBUILD_SHARED_LIBS=ON", + "-DDOWNLOAD_GTEST=OFF", "-DBRPC_SYSTEM_GTEST_SOURCE_DIR=" + str(self.gtest), + "-DFLATBUFFERS_INCLUDE_DIR=" + str(self.fb / "include"), + "-DFLATBUFFERS_FLATC_EXECUTABLE=" + str(self.flatc)]) + check_enabled(build / "output/include/butil/config.h") + self.step("build", ["cmake", "--build", build, "--target", *TESTS, "brpc-shared", + "--parallel", self.args.jobs]) + for name in TESTS: + self.check_gtest(name, build / "test" / name, build) + codegen = self.work / "codegen" + shared_library = "libbrpc.dylib" if sys.platform == "darwin" else "libbrpc.so" + self.step("codegen-configure", ["cmake", "-S", self.source / "tools/flatbuffers", "-B", codegen] + self.common_cmake() + [ + "-DBUILD_TESTING=ON", "-DBRPC_CODEGEN_BRPC_LIBRARY=" + str(build / "output/lib" / shared_library), + "-DFLATBUFFERS_INCLUDE_DIR=" + str(self.fb / "include"), + "-DFLATBUFFERS_LIBRARY=" + str(self.fb_library), "-DFLATC_EXECUTABLE=" + str(self.flatc)]) + self.step("codegen-build", ["cmake", "--build", codegen, "--parallel", self.args.jobs]) + self.ctest("codegen", codegen, {"flatbuffers_codegen_acceptance", "flatbuffers_codegen_runtime"}) + example = self.work / "example" + self.step("example-configure", ["cmake", "-S", self.source / "example/benchmark_fb", "-B", example] + self.common_cmake() + [ + "-DBUILD_TESTING=ON", "-DBRPC_ROOT=" + str(build / "output"), + "-DFLATBUFFERS_INCLUDE_DIR=" + str(self.fb / "include"), + "-DFLATC_EXECUTABLE=" + str(self.flatc), + "-DBRPC_FLATC_EXECUTABLE=" + str(codegen / "brpc_flatc")]) + self.step("example-build", ["cmake", "--build", example, "--parallel", self.args.jobs]) + self.ctest("example", example, {"benchmark_fb_smoke"}) + + def copy_source(self): + listing = self.step("source-files", ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"]) + destination = self.work / "source" + destination.mkdir() + count = 0 + for name in set(listing.read_bytes().split(b"\0")): + if not name: + continue + relative = Path(os.fsdecode(name)) + path = self.source / relative + if (relative.is_absolute() or ".." in relative.parts or path.is_symlink() or + self.source not in path.resolve().parents): + raise ValueError("Unsafe checkout path: " + str(relative)) + if not path.exists(): + continue + if not path.is_file(): + raise ValueError("Only regular source files are supported: " + str(relative)) + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + count += 1 + if not count: + raise ValueError("The source must be a nonempty Git checkout") + self.env["CCACHE_BASEDIR"] = str(destination) + return destination + + def run_make(self): + source = self.copy_source() + headers = [str(prefix / "include") for prefix in self.prefixes] + ["/usr/include"] + libs = [str(prefix / "lib") for prefix in self.prefixes] + ["/usr/lib", "/usr/lib64"] + self.step("configure", ["sh", "config_brpc.sh", "--with-flatbuffers", + "--headers=" + " ".join(headers), "--libs=" + " ".join(libs), + "--cc=" + self.compiler, "--cxx=" + self.cxx], cwd=source) + self.step("build", ["make", "-j" + str(self.args.jobs)], cwd=source) + check_enabled(source / "output/include/butil/config.h") + self.step("test-build", ["make", "-C", "test", "-j" + str(self.args.jobs), + "FLATC=" + str(self.flatc), *TESTS], cwd=source) + variable = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" + self.env[variable] = os.pathsep.join([str(source / "test")] + + [str(prefix / "lib") for prefix in self.prefixes]) + for name in TESTS: + self.check_gtest(name, source / "test" / name) + + def run_bazel(self): + source = self.copy_source() + command = ["bazel", "--batch", "--output_user_root=" + str(self.work / "bazel-state"), + "test", "--define=BRPC_WITH_FLATBUFFERS=true", "--jobs=" + str(self.args.jobs), + "--local_test_jobs=1", "--test_timeout=300", "--cache_test_results=no", + "--runs_per_test=1", "--flaky_test_attempts=1", "--test_output=errors", + "--test_env=GTEST_FILTER=*", "--test_env=GTEST_REPEAT=1"] + try: + self.step("bazel-tests", command + ["//test:" + name for name in TESTS], cwd=source, timeout=1800) + finally: + for name in TESTS: + for filename in ("test.xml", "test.log"): + path = source / "bazel-testlogs/test" / name / filename + if path.is_file(): + shutil.copy2(path, self.evidence / (name + "." + filename)) + for name in TESTS: + listing = self.step(name + "-list", [source / "bazel-bin/test" / name, "--gtest_list_tests"], cwd=source, timeout=30) + self.results[name] = verify_gtests(self.evidence / (name + ".test.xml"), listing, TESTS[name]) + self.save("running") + + def run(self): + self.step("compiler-version", [self.cxx, "--version"], timeout=10) + self.step("cmake-version", ["cmake", "--version"], timeout=10) + if self.args.build_system == "bazel": + self.step("bazel-version", ["bazel", "--version"], timeout=30) + self.run_bazel() + else: + self.prepare_dependencies() + getattr(self, "run_" + self.args.build_system)() + self.save("passed") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-system", choices=("cmake", "make", "bazel"), required=True) + parser.add_argument("--source", type=Path, default=Path(__file__).resolve().parents[2]) + parser.add_argument("--work", type=Path, required=True) + parser.add_argument("--jobs", type=int, default=2) + parser.add_argument("--flatbuffers-prefix", type=Path) + parser.add_argument("--gtest-source", type=Path) + parser.add_argument("--dependency-prefix", type=Path, action="append", default=[]) + parser.add_argument("--protoc", type=Path) + args = parser.parse_args() + if not 1 <= args.jobs <= 16: + parser.error("--jobs must be in 1..16") + runner = None + try: + runner = Runner(args) + runner.run() + except (Exception, KeyboardInterrupt) as error: + if runner: + runner.save("failed") + (runner.evidence / "failure.txt").write_text(str(error) + "\n") + print("FlatBuffers ON gate failed:", error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) + sys.exit(main()) diff --git a/.github/scripts/test_flatbuffers_on.py b/.github/scripts/test_flatbuffers_on.py new file mode 100644 index 0000000000..d34b0b8854 --- /dev/null +++ b/.github/scripts/test_flatbuffers_on.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 argparse +import base64 +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import signal +import select +import subprocess +import sys +import time +import tarfile +import tempfile +import unittest +from unittest import mock +import xml.etree.ElementTree as ET + +SCRIPT = Path(__file__).with_name("flatbuffers-on.py") +SPEC = importlib.util.spec_from_file_location("flatbuffers_on", SCRIPT) +gate = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(gate) + + +class GateTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.listing = self.root / "list.txt" + self.listing.write_text("Suite.\n First\n Second\n") + self.xml = self.root / "tests.xml" + + def report(self, names=("First", "Second"), **attributes): + root = ET.Element("testsuites", tests=str(len(names)), failures="0", errors="0", disabled="0") + root.attrib.update(attributes) + suite = ET.SubElement(root, "testsuite", name="Suite") + for name in names: + ET.SubElement(suite, "testcase", classname="Suite", name=name, + status="run", result="completed") + ET.ElementTree(root).write(self.xml) + return root + + def save(self, root): + ET.ElementTree(root).write(self.xml) + + def test_successful_report_matches_discovery(self): + self.report() + self.assertEqual(2, gate.verify_gtests(self.xml, self.listing, 2)["executed"]) + + def test_parameterized_listing(self): + self.assertEqual({"Typed/0.Works/1"}, gate.list_gtests( + "Running main from gtest\nTyped/0. # TypeParam = int\n Works/1 # GetParam = 1\n")) + + def test_empty_or_disabled_listing_rejected(self): + for text in ("", "no tests", "DISABLED_Suite.\n Test\n", "Suite.\n DISABLED_Test\n"): + with self.subTest(text=text), self.assertRaises(ValueError): + gate.list_gtests(text) + + def test_duplicate_listing_rejected(self): + with self.assertRaises(ValueError): + gate.list_gtests("Suite.\n First\n First\n") + + def test_empty_or_missing_xml_rejected(self): + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 1) + self.report(()) + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 1) + + def test_filtered_and_below_minimum_rejected(self): + self.report(("First",)) + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 1) + self.report() + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 3) + + def test_failure_error_skip_disabled_rejected(self): + for tag in ("failure", "error", "skipped"): + root = self.report() + ET.SubElement(next(root.iter("testcase")), tag) + self.save(root) + with self.subTest(tag=tag), self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 2) + for attribute in ("failures", "errors", "disabled", "skipped"): + self.report(**{attribute: "1"}) + with self.subTest(attribute=attribute), self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 2) + + def test_unexecuted_and_duplicate_cases_rejected(self): + root = self.report() + next(root.iter("testcase")).set("status", "notrun") + self.save(root) + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 2) + self.report(("First", "First")) + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 2) + + def test_incorrect_root_count_rejected(self): + self.report(tests="999") + with self.assertRaises(ValueError): + gate.verify_gtests(self.xml, self.listing, 2) + + def test_ctest_requires_runtime_and_acceptance(self): + self.report(("acceptance", "runtime")) + self.assertEqual(2, gate.verify_ctest(self.xml, {"acceptance", "runtime"})["executed"]) + self.report(("acceptance",)) + with self.assertRaises(ValueError): + gate.verify_ctest(self.xml, {"acceptance", "runtime"}) + + def archive(self, name="pkg/file", link=False): + path = self.root / "archive.tar.gz" + with tarfile.open(path, "w:gz") as archive: + info = tarfile.TarInfo(name) + if link: + info.type = tarfile.SYMTYPE + info.linkname = "/outside" + archive.addfile(info) + else: + info.size = 4 + archive.addfile(info, io.BytesIO(b"data")) + return path, hashlib.sha256(path.read_bytes()).hexdigest() + + def test_pinned_archive_extracts(self): + path, digest = self.archive() + extracted = gate.extract_archive(path, digest, self.root / "extract", "pkg") + self.assertEqual(b"data", (extracted / "file").read_bytes()) + + def test_archive_checksum_paths_and_links_rejected(self): + path, _ = self.archive() + with self.assertRaises(ValueError): + gate.extract_archive(path, "0" * 64, self.root / "extract", "pkg") + for name, link in (("../outside", False), ("pkg/../outside", False), + ("/absolute", False), ("wrong/file", False), ("pkg/link", True)): + path, digest = self.archive(name, link) + with self.subTest(name=name), self.assertRaises(ValueError): + gate.extract_archive(path, digest, self.root / "extract", "pkg") + self.assertFalse((self.root / "extract").exists()) + + def linked_archive(self, entries): + path = self.root / "linked.tar.gz" + with tarfile.open(path, "w:gz") as archive: + for name, target in entries: + info = tarfile.TarInfo(name) + if target is not None: + info.type = tarfile.SYMTYPE + info.linkname = target + archive.addfile(info) + else: + info.size = 4 + archive.addfile(info, io.BytesIO(b"data")) + return path, hashlib.sha256(path.read_bytes()).hexdigest() + + def test_internal_archive_symlinks_extract_after_data(self): + path, digest = self.linked_archive([ + ("pkg/java/src/test/java/Example", "../../../../tests/Example"), + ("pkg/ts/package.json", "../package.json"), + ("pkg/tests/Example/schema.fbs", None), ("pkg/package.json", None)]) + extracted = gate.extract_archive(path, digest, self.root / "extract", "pkg") + self.assertTrue((extracted / "java/src/test/java/Example").is_symlink()) + self.assertEqual(b"data", (extracted / "java/src/test/java/Example/schema.fbs").read_bytes()) + self.assertEqual(b"data", (extracted / "ts/package.json").read_bytes()) + + def test_escaping_chained_and_ancestor_links_rejected(self): + cases = [ + [("pkg/link", "../../outside")], [("pkg", ".")], + [("pkg/link", "next"), ("pkg/next", "file"), ("pkg/file", None)], + [("pkg/dir", "target"), ("pkg/dir/file", None)], + [("pkg/a", "."), ("pkg/link", "nested/../a/../outside")], + ] + for entries in cases: + path, digest = self.linked_archive(entries) + with self.subTest(entries=entries), self.assertRaises(ValueError): + gate.extract_archive(path, digest, self.root / "extract", "pkg") + self.assertFalse((self.root / "extract").exists()) + + def test_duplicate_archive_members_rejected(self): + path, digest = self.linked_archive([("pkg/file", None), ("pkg/file", None)]) + with self.assertRaises(ValueError): + gate.extract_archive(path, digest, self.root / "extract", "pkg") + self.assertFalse((self.root / "extract").exists()) + + def runner(self): + source = self.root / "source" + source.mkdir() + return gate.Runner(argparse.Namespace(source=source, work=self.root / "work", + build_system="cmake", dependency_prefix=[])) + + def test_work_directory_is_exclusive(self): + runner = self.runner() + with self.assertRaises(FileExistsError): + gate.Runner(runner.args) + + def test_filter_environment_is_reset(self): + with mock.patch.dict(os.environ, {"GTEST_FILTER": "Wrong.*", "GTEST_SHARD_INDEX": "2"}): + runner = self.runner() + self.assertEqual("*", runner.env["GTEST_FILTER"]) + self.assertNotIn("GTEST_SHARD_INDEX", runner.env) + + def test_work_inside_checkout_rejected(self): + source = self.root / "checkout" + source.mkdir() + args = argparse.Namespace(source=source, work=source / "build", build_system="cmake", dependency_prefix=[]) + with self.assertRaises(ValueError): + gate.Runner(args) + + def test_source_directory_link_cannot_escape_checkout(self): + runner = self.runner() + outside = self.root / "outside" + outside.mkdir() + (outside / "file").write_text("must not be copied") + (runner.source / "link").symlink_to(outside, target_is_directory=True) + self.listing.write_bytes(b"link/file\0") + with mock.patch.object(runner, "step", return_value=self.listing): + with self.assertRaises(ValueError): + runner.copy_source() + self.assertFalse((runner.work / "source/link/file").exists()) + + def test_cmake_uses_explicit_openssl_prefix(self): + runner = self.runner() + runner.args.protoc = None + self.assertFalse(any(option.startswith("-DOPENSSL_ROOT_DIR=") + for option in runner.common_cmake())) + prefix = self.root / "openssl" + (prefix / "include/openssl").mkdir(parents=True) + (prefix / "include/openssl/ssl.h").write_text("header fixture") + runner.prefixes = [self.root / "unrelated", prefix] + self.assertIn("-DOPENSSL_ROOT_DIR=" + str(prefix), runner.common_cmake()) + + def test_make_builds_supplied_gtest_in_private_prefix(self): + runner = self.runner() + prefix = self.root / "provided-fb" + (prefix / "include/flatbuffers").mkdir(parents=True) + (prefix / "include/flatbuffers/base.h").write_text( + "#define FLATBUFFERS_VERSION_MAJOR 25\n#define FLATBUFFERS_VERSION_MINOR 2\n#define FLATBUFFERS_VERSION_REVISION 10\n") + (prefix / "lib").mkdir() + (prefix / "lib/libflatbuffers.a").touch() + source = self.root / "provided-gtest" + source.mkdir() + runner.args.build_system = "make" + runner.args.flatbuffers_prefix = prefix + runner.args.gtest_source = source + runner.args.jobs = 2 + runner.args.protoc = Path(sys.executable) + version = self.root / "version.log" + version.write_text("flatc version 25.2.10\n") + with mock.patch.object(runner, "step", return_value=version) as step: + with mock.patch.object(runner, "download") as download: + runner.prepare_dependencies() + download.assert_not_called() + configure = next(call.args[1] for call in step.call_args_list + if call.args[0] == "gtest-configure") + self.assertIn(source.resolve(), configure) + self.assertIn("-DCMAKE_INSTALL_PREFIX=" + str(runner.work / "gtest-prefix"), configure) + self.assertEqual(runner.work / "gtest-prefix", runner.prefixes[0]) + self.assertFalse((prefix / "include/gtest").exists()) + + def exercise_process_group(self, leader_exits): + ready = self.root / "child.ready" + child_code = ("import signal,time,pathlib; signal.signal(signal.SIGTERM, signal.SIG_IGN); " + "pathlib.Path(" + repr(str(ready)) + ").touch(); time.sleep(60)") + leader_code = ("import subprocess,sys,time,pathlib; " + "child=subprocess.Popen([sys.executable,'-c'," + repr(child_code) + "], " + "stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL); " + "print(child.pid,flush=True); " + + ("sys.exit(0)" if leader_exits else "time.sleep(60)")) + process = subprocess.Popen([sys.executable, "-c", leader_code], + stdout=subprocess.PIPE, text=True, start_new_session=True) + try: + readable, _, _ = select.select([process.stdout], [], [], 3) + self.assertTrue(readable, "group leader did not publish child PID") + child = int(process.stdout.readline()) + deadline = time.monotonic() + 3 + while not ready.exists() and time.monotonic() < deadline: + time.sleep(0.02) + self.assertTrue(ready.exists()) + if leader_exits: + process.wait(timeout=3) + gate.stop_process(process, grace=0.1) + deadline = time.monotonic() + 3 + while True: + result = subprocess.run(["ps", "-o", "stat=", "-p", str(child)], + stdout=subprocess.PIPE, text=True, check=False) + state = result.stdout.strip() + if not state or state.startswith("Z"): + break + self.assertLess(time.monotonic(), deadline, "owned child survived cleanup") + time.sleep(0.02) + finally: + gate.signal_group(process, signal.SIGKILL) + process.wait(timeout=3) + process.stdout.close() + + def test_cleanup_after_group_leader_exits(self): + self.exercise_process_group(True) + + def test_cleanup_kills_child_ignoring_term(self): + self.exercise_process_group(False) + + def test_timeout_is_a_recorded_failure(self): + runner = self.runner() + with self.assertRaises(RuntimeError): + runner.step("timeout", [sys.executable, "-c", "import time; time.sleep(60)"], timeout=0.05) + self.assertEqual(124, json.loads((runner.evidence / "summary.json").read_text())["steps"][0]["exitcode"]) + + def test_flatbuffers_pin_matches_both_bazel_definitions(self): + root = SCRIPT.resolve().parents[2] + for filename in ("MODULE.bazel", "WORKSPACE"): + text = (root / filename).read_text() + integrity = "sha256-" + base64.b64encode(bytes.fromhex(gate.FLATBUFFERS_SHA256)).decode() + self.assertTrue(gate.FLATBUFFERS_SHA256 in text or integrity in text, + filename + ": FlatBuffers checksum differs") + self.assertTrue("v" + gate.FLATBUFFERS_VERSION + ".tar.gz" in text, + filename + ": FlatBuffers version differs") + + def test_workflow_script_paths_and_characters(self): + root = SCRIPT.resolve().parents[2] + workflow = (root / ".github/workflows/flatbuffers-on.yml").read_text() + self.assertFalse(any(ord(char) < 32 and char not in "\n\r\t" for char in workflow)) + for filename in ("flatbuffers-on.py", "test_flatbuffers_on.py"): + self.assertIn(".github/scripts/" + filename, workflow) + self.assertTrue((SCRIPT.parent / filename).is_file()) + self.assertNotIn("pull_request_target", workflow) + self.assertNotIn("continue-on-error", workflow) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/flatbuffers-on.yml b/.github/workflows/flatbuffers-on.yml new file mode 100644 index 0000000000..018c8e9a6c --- /dev/null +++ b/.github/workflows/flatbuffers-on.yml @@ -0,0 +1,86 @@ +name: FlatBuffers ON + +on: + push: + branches: [master] + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build-test: + name: FB ON (${{ matrix.os }}, ${{ matrix.system }}, ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - {os: ubuntu-22.04, system: cmake, compiler: gcc, cc: gcc, cxx: g++, cache: ccache} + - {os: ubuntu-22.04, system: cmake, compiler: clang, cc: clang, cxx: clang++, cache: ccache} + - {os: ubuntu-22.04, system: make, compiler: gcc, cc: gcc, cxx: g++, cache: ccache} + - {os: ubuntu-22.04, system: make, compiler: clang, cc: clang, cxx: clang++, cache: ccache} + - {os: ubuntu-22.04, system: bazel, compiler: gcc, cc: gcc, cxx: g++, cache: bazel} + - {os: macos-latest, system: cmake, compiler: apple-clang, cc: clang, cxx: clang++, cache: ccache} + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + FB_WORK: ${{ runner.temp }}/brpc-fb-${{ matrix.system }}-${{ matrix.compiler }} + BUILD_SYSTEM: ${{ matrix.system }} + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + + - name: Validate the gate's report checks + run: python3 .github/scripts/test_flatbuffers_on.py + + - name: Install focused Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build clang ccache pkg-config python3 curl ca-certificates git \ + libssl-dev libgflags-dev libprotobuf-dev libprotoc-dev protobuf-compiler \ + libleveldb-dev libgoogle-perftools-dev zlib1g-dev + + - name: Install focused macOS dependencies + if: runner.os == 'macOS' + run: brew install cmake ninja pkg-config openssl@3 gflags leveldb protobuf@29 abseil + + - uses: ./.github/actions/setup-build-cache + with: + kind: ${{ matrix.cache }} + cache-key: fb-on-${{ matrix.system }}-${{ matrix.compiler }} + + - name: Build and run the FB ON gates + shell: bash + run: | + args=(--build-system "$BUILD_SYSTEM" --work "$FB_WORK" --jobs 2) + if [[ "$RUNNER_OS" == macOS ]]; then + for package in protobuf@29 abseil openssl@3 gflags leveldb; do + args+=(--dependency-prefix "$(brew --prefix "$package")") + done + args+=(--protoc "$(brew --prefix protobuf@29)/bin/protoc") + fi + python3 .github/scripts/flatbuffers-on.py "${args[@]}" + + - name: Upload commands, logs and test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: flatbuffers-on-${{ matrix.os }}-${{ matrix.system }}-${{ matrix.compiler }} + path: ${{ env.FB_WORK }}/evidence/ + if-no-files-found: warn + retention-days: 14 diff --git a/.gitignore b/.gitignore index 739963a26c..eef8f3eb14 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,10 @@ /test/output build/ -# Ignore hidden files +# Ignore hidden files, but keep GitHub workflows and helpers discoverable. .* +!/.github/ +/.github/scripts/__pycache__/ *.swp # Ignore auto-generated files diff --git a/BUILD.bazel b/BUILD.bazel index db73605e6e..923c8adccd 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -20,6 +20,12 @@ licenses(["notice"]) # Apache v2 exports_files(["LICENSE"]) +config_setting( + name = "brpc_with_flatbuffers", + define_values = {"BRPC_WITH_FLATBUFFERS": "true"}, + visibility = ["//visibility:public"], +) + COPTS = [ "-fno-omit-frame-pointer", ] + select({ @@ -45,6 +51,9 @@ DEFINES = [ }) + select({ "//bazel/config:brpc_with_thrift": ["ENABLE_THRIFT_FRAMED_PROTOCOL=1"], "//conditions:default": [], + }) + select({ + ":brpc_with_flatbuffers": ["BRPC_WITH_FLATBUFFERS=1"], + "//conditions:default": ["BRPC_WITH_FLATBUFFERS=0"], }) + select({ "//bazel/config:brpc_with_thrift_legacy_version": [], "//conditions:default": ["THRIFT_STDCXX=std"], @@ -125,6 +134,14 @@ genrule( "//conditions:default": "0", }) + """ +#ifdef BRPC_WITH_FLATBUFFERS +#undef BRPC_WITH_FLATBUFFERS +#endif +#define BRPC_WITH_FLATBUFFERS """ + select({ + ":brpc_with_flatbuffers": "1", + "//conditions:default": "0", + }) + + """ #ifdef BUTIL_USE_CPU_FREQUENCY #undef BUTIL_USE_CPU_FREQUENCY #endif @@ -539,6 +556,11 @@ brpc_proto_library( visibility = ["//visibility:public"], ) +FLATBUFFERS_SRC_PATTERNS = [ + "src/brpc/flatbuffers/*.cpp", + "src/brpc/policy/flatbuffers_protocol.cpp", +] + URMA_SRC_PATTERNS = [ "src/brpc/urma/*.cpp", "src/brpc/urma/**/*.cpp", @@ -562,7 +584,7 @@ BRPC_BASE_SRCS = glob( "src/brpc/policy/thrift_protocol.cpp", "src/brpc/event_dispatcher_epoll.cpp", "src/brpc/event_dispatcher_kqueue.cpp", - ] + URMA_SRC_PATTERNS, + ] + URMA_SRC_PATTERNS + FLATBUFFERS_SRC_PATTERNS, ) cc_library( @@ -573,6 +595,9 @@ cc_library( "src/brpc/**/thrift*.cpp", ]), "//conditions:default": [], + }) + select({ + ":brpc_with_flatbuffers": glob(FLATBUFFERS_SRC_PATTERNS), + "//conditions:default": [], }) + select({ "//bazel/config:brpc_with_urma_use_real": URMA_SRCS, "//bazel/config:brpc_with_urma": URMA_SRCS + [ @@ -612,6 +637,9 @@ cc_library( "@org_apache_thrift//:thrift", ], "//conditions:default": [], + }) + select({ + ":brpc_with_flatbuffers": ["@com_github_google_flatbuffers//:runtime_cc"], + "//conditions:default": [], }), ) diff --git a/CMakeLists.txt b/CMakeLists.txt index f9e1aa8fc3..f25882d620 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ option(WITH_MESALINK "With MesaLink" OFF) option(WITH_BORINGSSL "With BoringSSL" OFF) option(WITH_DEBUG_SYMBOLS "With debug symbols" ON) option(WITH_THRIFT "With thrift framed protocol supported" OFF) +option(WITH_FLATBUFFERS "With FlatBuffers message and RPC support (headers only)" OFF) option(WITH_BTHREAD_TRACER "With bthread tracer supported" OFF) option(WITH_SNAPPY "With snappy" OFF) option(WITH_RDMA "With RDMA" OFF) @@ -82,6 +83,17 @@ if(WITH_GLOG) set(BRPC_WITH_GLOG 1) endif() +set(WITH_FLATBUFFERS_VAL "0") +if(WITH_FLATBUFFERS) + find_path(FLATBUFFERS_INCLUDE_DIR NAMES flatbuffers/flatbuffers.h) + if(NOT FLATBUFFERS_INCLUDE_DIR) + message(FATAL_ERROR + "WITH_FLATBUFFERS requires FlatBuffers headers; set FLATBUFFERS_INCLUDE_DIR.") + endif() + set(WITH_FLATBUFFERS_VAL "1") + list(APPEND BRPC_COMMON_INCLUDE_DIRS ${FLATBUFFERS_INCLUDE_DIR}) +endif() + set(WITH_CPU_FREQUENCY_VAL "0") if(WITH_CPU_FREQUENCY) set(WITH_CPU_FREQUENCY_VAL "1") @@ -177,6 +189,7 @@ endif() list(APPEND BRPC_COMMON_DEFINITIONS BRPC_WITH_GLOG=${WITH_GLOG_VAL} + BRPC_WITH_FLATBUFFERS=${WITH_FLATBUFFERS_VAL} BRPC_WITH_RDMA=${WITH_RDMA_VAL} BRPC_WITH_URMA=${WITH_URMA_VAL} BRPC_WITH_UBRING=${WITH_UBRING_VAL} @@ -670,6 +683,10 @@ file(GLOB_RECURSE BTHREAD_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/b file(GLOB_RECURSE JSON2PB_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/json2pb/*.cpp") file(GLOB_RECURSE BRPC_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/brpc/*.cpp") file(GLOB_RECURSE THRIFT_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/brpc/thrift*.cpp") +if(NOT WITH_FLATBUFFERS) + list(FILTER BRPC_SOURCES EXCLUDE REGEX "/brpc/flatbuffers/.*\\.cpp$") + list(REMOVE_ITEM BRPC_SOURCES "${PROJECT_SOURCE_DIR}/src/brpc/policy/flatbuffers_protocol.cpp") +endif() file(GLOB_RECURSE EXCLUDE_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/brpc/event_dispatcher_*.cpp") # When building with the real liburma, exclude the link-time mock so its urma_* diff --git a/MODULE.bazel b/MODULE.bazel index 97862d36f1..4f72f9b07b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -79,3 +79,17 @@ git_repository( remote = 'https://atomgit.com/openeuler/umdk.git', commit = '564ee727a55523d4351a8fb3c94292b388ebb924', # v26.06.0_CAM ) + +# runtime_cc and flatc do not need FlatBuffers' gRPC module dependency, which +# would otherwise conflict with brpc's BoringSSL version even when disabled. +# Keep the archive and checksum in sync with WORKSPACE. +flatbuffers_http_archive = use_repo_rule( + '@bazel_tools//tools/build_defs/repo:http.bzl', + 'http_archive', +) +flatbuffers_http_archive( + name = 'com_github_google_flatbuffers', + sha256 = 'b9c2df49707c57a48fc0923d52b8c73beb72d675f9d44b2211e4569be40a7421', + strip_prefix = 'flatbuffers-25.2.10', + urls = ['https://github.com/google/flatbuffers/archive/refs/tags/v25.2.10.tar.gz'], +) diff --git a/Makefile b/Makefile index 271b518ae6..936299f363 100644 --- a/Makefile +++ b/Makefile @@ -204,6 +204,9 @@ JSON2PB_SOURCES = $(foreach d,$(JSON2PB_DIRS),$(wildcard $(addprefix $(d)/*,$(SR JSON2PB_OBJS = $(addsuffix .o, $(basename $(JSON2PB_SOURCES))) BRPC_DIRS = src/brpc src/brpc/details src/brpc/builtin src/brpc/policy src/brpc/policy/mysql src/brpc/rdma +ifeq ($(WITH_FLATBUFFERS),1) +BRPC_DIRS += src/brpc/flatbuffers +endif ifeq ($(WITH_URMA),1) BRPC_DIRS += src/brpc/urma endif @@ -213,6 +216,9 @@ BRPC_SOURCES_ALL = $(foreach d,$(BRPC_DIRS),$(wildcard $(addprefix $(d)/*,$(SRCE ifeq ($(URMA_USE_MOCK),0) BRPC_SOURCES_ALL := $(filter-out src/brpc/urma/mock_urma.cpp,$(BRPC_SOURCES_ALL)) endif +ifneq ($(WITH_FLATBUFFERS),1) +BRPC_SOURCES_ALL := $(filter-out src/brpc/policy/flatbuffers_protocol.cpp,$(BRPC_SOURCES_ALL)) +endif BRPC_SOURCES = $(filter-out $(THRIFT_SOURCES) $(EXCLUDE_SOURCES), $(BRPC_SOURCES_ALL)) BRPC_PROTOS = $(filter %.proto,$(BRPC_SOURCES)) BRPC_CFAMILIES = $(filter-out %.proto %.pb.cc,$(BRPC_SOURCES)) diff --git a/README.md b/README.md index d65366fafb..3fa3ea7f08 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ You can use it to: * Read [overview](docs/en/overview.md) to know where bRPC can be used and its advantages. * Read [getting started](docs/en/getting_started.md) for building steps and play with [examples](https://github.com/apache/brpc/tree/master/example/). * Docs: + * [Enable FlatBuffers RPC and verify the example](docs/en/flatbuffers.md) * [Performance benchmark](docs/en/benchmark.md) * [bvar](docs/en/bvar.md) * [bvar_c++](docs/en/bvar_c++.md) diff --git a/README_cn.md b/README_cn.md index 2cc686bd85..b3a5946c0d 100644 --- a/README_cn.md +++ b/README_cn.md @@ -32,6 +32,7 @@ * 通过[概述](docs/cn/overview.md)了解哪里可以用bRPC及其优势。 * 阅读[编译步骤](docs/cn/getting_started.md)了解如何开始使用, 之后可以运行一下[示例程序](https://github.com/apache/brpc/tree/master/example/). * 文档: + * [启用 FlatBuffers RPC 并通过 example 验证](docs/cn/flatbuffers.md) * [性能测试](docs/cn/benchmark.md) * [bvar](docs/cn/bvar.md) * [bvar_c++](docs/cn/bvar_c++.md) diff --git a/WORKSPACE b/WORKSPACE index 22fc411b32..9778a704ba 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -150,6 +150,14 @@ http_archive( urls = ["https://github.com/google/crc32c/archive/1.1.2.tar.gz"], ) +# Optional FlatBuffers support uses runtime_cc; keep this version in sync with MODULE.bazel. +http_archive( + name = "com_github_google_flatbuffers", + integrity = "sha256-ucLfSXB8V6SPwJI9UrjHO+ty1nX51EsiEeRWm+QKdCE=", + strip_prefix = "flatbuffers-25.2.10", + urls = ["https://github.com/google/flatbuffers/archive/refs/tags/v25.2.10.tar.gz"], +) + http_archive( name = "com_github_google_glog", # 2021-05-07T23:06:39Z patch_args = ["-p1"], diff --git a/config.h.in b/config.h.in index d8de111be9..ece54504ce 100644 --- a/config.h.in +++ b/config.h.in @@ -21,6 +21,11 @@ #endif #cmakedefine BRPC_WITH_GLOG @WITH_GLOG_VAL@ +#ifdef BRPC_WITH_FLATBUFFERS +#undef BRPC_WITH_FLATBUFFERS +#endif +#define BRPC_WITH_FLATBUFFERS @WITH_FLATBUFFERS_VAL@ + #ifdef BUTIL_USE_CPU_FREQUENCY #undef BUTIL_USE_CPU_FREQUENCY #endif diff --git a/config_brpc.sh b/config_brpc.sh index 0efa4d374d..e940e486f4 100755 --- a/config_brpc.sh +++ b/config_brpc.sh @@ -54,9 +54,10 @@ else LDD=ldd fi -TEMP=`getopt -o v: --long headers:,libs:,cc:,cxx:,with-glog,with-thrift,with-rdma,with-urma,with-urma-mock,without-urma-mock,with-mesalink,with-bthread-tracer,with-debug-bthread-sche-safety,with-debug-lock,with-asan,with-riscv-zvbc,with-riscv-zbc,with-cpu-frequency,nodebugsymbols,werror -n 'config_brpc' -- "$@"` +TEMP=`getopt -o v: --long headers:,libs:,cc:,cxx:,with-glog,with-thrift,with-flatbuffers,with-rdma,with-urma,with-urma-mock,without-urma-mock,with-mesalink,with-bthread-tracer,with-debug-bthread-sche-safety,with-debug-lock,with-asan,with-riscv-zvbc,with-riscv-zbc,with-cpu-frequency,nodebugsymbols,werror -n 'config_brpc' -- "$@"` WITH_GLOG=0 WITH_THRIFT=0 +WITH_FLATBUFFERS=0 WITH_RDMA=0 WITH_URMA=0 URMA_MOCK_MODE=auto @@ -91,6 +92,7 @@ while true; do --cxx ) CXX=$2; shift 2 ;; --with-glog ) WITH_GLOG=1; shift 1 ;; --with-thrift) WITH_THRIFT=1; shift 1 ;; + --with-flatbuffers) WITH_FLATBUFFERS=1; shift 1 ;; --with-rdma) WITH_RDMA=1; shift 1 ;; --with-urma) WITH_URMA=1; shift 1 ;; --with-urma-mock) URMA_MOCK_MODE=on; shift 1 ;; @@ -479,6 +481,7 @@ append_to_output "HDRS=$($ECHO $HDRS)" append_to_output "LIBS=$($ECHO $LIBS)" append_to_output "PROTOC=$PROTOC" append_to_output "PROTOBUF_HDR=$PROTOBUF_HDR" +append_to_output "WITH_FLATBUFFERS=$WITH_FLATBUFFERS" append_to_output "CC=$CC" append_to_output "CXX=$CXX" append_to_output "GCC_VERSION=$GCC_VERSION" @@ -486,7 +489,7 @@ append_to_output "STATIC_LINKINGS=$STATIC_LINKINGS" append_to_output "DYNAMIC_LINKINGS=$DYNAMIC_LINKINGS" # CPP means C PreProcessing, not C PlusPlus -CPPFLAGS="${CPPFLAGS} -DBRPC_WITH_GLOG=$WITH_GLOG -DBRPC_DEBUG_BTHREAD_SCHE_SAFETY=$BRPC_DEBUG_BTHREAD_SCHE_SAFETY -DBRPC_DEBUG_LOCK=$BRPC_DEBUG_LOCK -DBUTIL_USE_CPU_FREQUENCY=$WITH_CPU_FREQUENCY" +CPPFLAGS="${CPPFLAGS} -DBRPC_WITH_GLOG=$WITH_GLOG -DBRPC_WITH_FLATBUFFERS=$WITH_FLATBUFFERS -DBRPC_DEBUG_BTHREAD_SCHE_SAFETY=$BRPC_DEBUG_BTHREAD_SCHE_SAFETY -DBRPC_DEBUG_LOCK=$BRPC_DEBUG_LOCK -DBUTIL_USE_CPU_FREQUENCY=$WITH_CPU_FREQUENCY" # Avoid over-optimizations of TLS variables by GCC>=4.8 # See: https://github.com/apache/brpc/issues/1693 @@ -506,6 +509,12 @@ if [ "$SYSTEM" = "Darwin" ]; then fi fi +if [ $WITH_FLATBUFFERS != 0 ]; then + FLATBUFFERS_HDR=$(find_dir_of_header_or_die flatbuffers/flatbuffers.h) || exit 1 + append_to_output_headers "$FLATBUFFERS_HDR" + print_success "Found FlatBuffers headers: $FLATBUFFERS_HDR" +fi + if [ $WITH_THRIFT != 0 ]; then THRIFT_LIB=$(find_dir_of_lib_or_die thriftnb) THRIFT_HDR=$(find_dir_of_header_or_die thrift/Thrift.h) @@ -692,6 +701,11 @@ cat << EOF > src/butil/config.h #endif #define BRPC_WITH_GLOG $WITH_GLOG +#ifdef BRPC_WITH_FLATBUFFERS +#undef BRPC_WITH_FLATBUFFERS +#endif +#define BRPC_WITH_FLATBUFFERS $WITH_FLATBUFFERS + #ifdef BUTIL_USE_CPU_FREQUENCY #undef BUTIL_USE_CPU_FREQUENCY #endif @@ -714,6 +728,7 @@ print_info "C++ std: $CXXFLAGS" print_info "System: $SYSTEM" if [ $WITH_GLOG -ne 0 ]; then print_info "With glog: yes"; fi if [ $WITH_THRIFT -ne 0 ]; then print_info "With thrift: yes"; fi +if [ $WITH_FLATBUFFERS -ne 0 ]; then print_info "With FlatBuffers: yes (headers only)"; fi if [ $WITH_RDMA -ne 0 ]; then print_info "With RDMA: yes"; fi if [ $WITH_URMA -ne 0 ]; then print_info "With URMA: yes"; fi if [ $WITH_MESALINK -ne 0 ]; then print_info "With MesaLink: yes"; fi diff --git a/docs/cn/flatbuffers.md b/docs/cn/flatbuffers.md new file mode 100644 index 0000000000..343c29ded2 --- /dev/null +++ b/docs/cn/flatbuffers.md @@ -0,0 +1,334 @@ +# 启用 FlatBuffers 消息与 RPC + +[English version](../en/flatbuffers.md) + +bRPC 提供基于 IOBuf 的 FlatBuffers 消息、构造器、服务描述及 `fb_rpc` 协议。 +支持默认关闭,需先编译启用该功能的 bRPC,再让客户端和服务端使用同一套头文件、 +生成配置和运行库。完整示例位于 [example/benchmark_fb](../../example/benchmark_fb/README.md)。 + +## 1. 启用选项与依赖 + +| 构建方式 | 启用选项 | 可供独立 example 使用的运行库前缀 | +| --- | --- | --- | +| CMake | `-DWITH_FLATBUFFERS=ON` | `/output` | +| Make | `config_brpc.sh --with-flatbuffers` | `/output` | +| Bazel | build/test 命令均传 `--define=BRPC_WITH_FLATBUFFERS=true` | 原始 Bazel 输出不是 example 所需的安装前缀 | + +**不能只给应用增加 `-DBRPC_WITH_FLATBUFFERS=1`。** 该功能会改变 Channel、Controller、 +Server 的 ABI;ON 头文件与 OFF 运行库混用不受支持。`butil/config.h` 中的宏始终为 0 或 1, +应用应使用 `#if BRPC_WITH_FLATBUFFERS`,而不是 `#ifdef`。 + +先按[入门指南](getting_started.md)准备 C++ 工具链、Protobuf 编译器及开发库、gflags、 +LevelDB、OpenSSL、zlib。FlatBuffers RPC **并不消除 Protobuf 依赖**。 + +| 组件 | FlatBuffers 或测试依赖 | +| --- | --- | +| bRPC 消息及 RPC 运行库 | FlatBuffers 头文件,不链接 `libflatbuffers` | +| 官方 schema 代码生成 | 与头文件版本一致的 `flatc` | +| bRPC 服务代码生成 | `brpc_flatc`,其构建另需匹配的官方头文件和 `libflatbuffers` | +| example 烟测 | Python 3,不需要 GoogleTest | +| 库单元测试 | GoogleTest 及项目原有测试依赖 | + +本仓库的 Bazel 和 ON gate 固定使用 **FlatBuffers 25.2.10**。复现该流程时,应使用同版本的 +头文件、官方 `flatc` 和用于构建 `brpc_flatc` 的库。不要删除生成头文件中的版本断言。 + +三个容易混淆的参数: + +- 根项目 CMake 的库测试:`FLATBUFFERS_FLATC_EXECUTABLE` 指向官方 `flatc`。 +- 生成器验收及 example:`FLATC_EXECUTABLE` 指向官方 `flatc`。 +- example:`BRPC_FLATC_EXECUTABLE` 指向 bRPC 的 `brpc_flatc`,不能填成官方 `flatc`。 + +各阶段还必须使用兼容的同一套 Protobuf。当 CMake 检测到 `Protobuf_VERSION > 4.21` 时, +需要 C++17 及匹配的 Abseil 依赖;生成器验收和 example 需要 Protobuf 的 CMake config 包 +导出传递依赖。运行库与 example 最低要求 CMake 3.16;下文 CTest 命令使用 3.17+ 的 `--no-tests=error` +防止空测试误报成功。只有 3.16 时,可直接执行 `smoke.py`。以下按 Linux/macOS 的 +Unix Makefiles 或 Ninja 单配置生成器编写。 + +## 2. 用 CMake 构建启用后的运行库 + +从 **可写的 bRPC checkout** 开始。按需提前设置有效的 `CC`、`CXX`。 +下面的 example 绑定代码与构建产物在 `WORK` 中,但根项目配置仍会写入 +**源码目录的 `src/butil/config.h`**。只读源码挂载会失败;同一 checkout 即使使用不同的 +build 目录,也不能并发配置 ON/OFF 或混用不同构建系统。需要隔离时使用独立可写副本。 + +将下列变量替换为已有安装前缀,即包含 `include/`、`lib/` 或 `lib64/` 的目录;多个变量 +可以指向同一前缀。macOS 上需保持编译器、SDK、架构一致,并显式指定实际 OpenSSL 前缀, +不要假定 Apple Silicon 环境中存在 `/usr/local/opt/openssl`。 + +```sh +REPO="$PWD" +DEPS=/absolute/path/to/dependency-prefix +FB=/absolute/path/to/flatbuffers-prefix +OPENSSL=/absolute/path/to/openssl-prefix +WORK="$(mktemp -d "${TMPDIR:-/tmp}/brpc-benchmark-fb.XXXXXX")" +JOBS=2 +printf 'WORK=%s\n' "$WORK" +``` + +若尚未安装 FlatBuffers,可先从匹配版本的官方源码构建。已有完整安装时跳过此段。 +`FB` 必须是可写的安装前缀,不是源码目录: + +```sh +FB_SOURCE=/absolute/path/to/flatbuffers-25.2.10-source +cmake -S "$FB_SOURCE" -B "$WORK/flatbuffers" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_INSTALL_PREFIX="$FB" -DCMAKE_INSTALL_LIBDIR=lib \ + -DFLATBUFFERS_BUILD_TESTS=OFF -DFLATBUFFERS_BUILD_FLATC=ON \ + -DFLATBUFFERS_BUILD_FLATLIB=ON -DFLATBUFFERS_BUILD_SHAREDLIB=OFF \ + -DFLATBUFFERS_INSTALL=ON -DFLATBUFFERS_LIBCXX_WITH_CLANG=OFF +cmake --build "$WORK/flatbuffers" --parallel "$JOBS" +cmake --install "$WORK/flatbuffers" +"$FB/bin/flatc" --version +``` + +构建静态运行库,不启用库单元测试,因此这一阶段不需要 GoogleTest: + +```sh +cmake -S "$REPO" -B "$WORK/runtime" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DWITH_FLATBUFFERS=ON -DBUILD_SHARED_LIBS=OFF \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DBUILD_UNIT_TESTS=OFF -DDOWNLOAD_GTEST=OFF -DBUILD_BRPC_TOOLS=OFF +cmake --build "$WORK/runtime" --parallel "$JOBS" +grep -E '^#define BRPC_WITH_FLATBUFFERS +1$' "$WORK/runtime/output/include/butil/config.h" +``` + +最后一条检查必须输出 `#define BRPC_WITH_FLATBUFFERS 1`。公共接口位于 +`brpc/flatbuffers/message.h` 和 `brpc/flatbuffers/service.h`。 +上面的 policy 参数是兼容 CMake 4 下某些旧依赖策略的可选设置,不是启用 FlatBuffers 的 +开关,也不能替代编译器和依赖版本要求。 + +## 3. 通过 example 完成端到端验证 + +该示例是**有界功能验证,不是性能对比**。[echo.fbs](../../example/benchmark_fb/echo.fbs) +声明 `BenchmarkService.Echo`,显式 wire method ID 为 7。 +[server.cpp](../../example/benchmark_fb/server.cpp) 使用 `AddFlatBuffersService` 注册服务; +[client.cpp](../../example/benchmark_fb/client.cpp) 使用生成的 `BenchmarkService::Stub` +及 `fb_rpc` channel。 + +### 构建生成器和示例 + +在前一节同一个 shell 中继续,沿用 `REPO`、`DEPS`、`FB`、`OPENSSL`、`WORK`、`JOBS`: + +```sh +cmake -S "$REPO/tools/flatbuffers" -B "$WORK/codegen" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=OFF \ + -DCMAKE_PREFIX_PATH="$FB;$DEPS" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" +cmake --build "$WORK/codegen" --parallel "$JOBS" + +cmake -S "$REPO/example/benchmark_fb" -B "$WORK/example" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=ON \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DBRPC_ROOT="$WORK/runtime/output" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DFLATC_EXECUTABLE="$FB/bin/flatc" \ + -DBRPC_FLATC_EXECUTABLE="$WORK/codegen/brpc_flatc" +cmake --build "$WORK/example" --parallel "$JOBS" +(cd "$WORK/example" && ctest -V --no-tests=error --output-on-failure -R '^benchmark_fb_smoke$') +``` + +构建时会执行两个生成器,文件位于 `WORK/example/generated/`: + +- 官方 `flatc`:`echo_generated.h`,定义 table 类型。 +- `brpc_flatc`:`echo.brpc.fb.h`、`echo.brpc.fb.cpp`,定义服务及 stub。 + +不要提交这些生成文件,也不要在更换版本后沿用旧头文件。生成器若选错库,可在其 CMake +配置中显式传入 `FLATBUFFERS_LIBRARY`。导入 schema、稳定方法 ID 等规则见 +[生成器说明](../../tools/flatbuffers/README.md)。 + +### 烟测的成功标准 + +必须看到名为 `benchmark_fb_smoke` 的 **1 项 CTest 通过**、命令退出码为 0,并输出: + +```text +benchmark_fb smoke passed: 13 verified replies, 2 schema rejections, clean shutdown +``` + +这是 **15 次 RPC:13 次正常响应、2 次预期 schema 拒绝**,不是 15 项 CTest。 +验证内容包括二进制字节、空字符串与缺失字符串、附件、并发、single/pooled/short 连接、 +拒绝后的正常调用恢复,以及服务端的干净退出。 + +脚本自行启动 `127.0.0.1:0` 服务端并读取系统分配的端口,不依赖固定端口或外部服务。 +编排期限为 35 秒,CTest 外层超时为 50 秒;正常清理应完成 Stop/Join,若必须 SIGKILL +才退出则烟测失败。`No tests were found` 不算通过。也可直接运行相同校验: + +```sh +python3 "$REPO/example/benchmark_fb/smoke.py" \ + --server "$WORK/example/benchmark_fb_server" \ + --client "$WORK/example/benchmark_fb_client" +``` + +### 手工分别运行 server/client + +两个进程必须位于**同一主机或容器**,该 example 刻意拒绝非 loopback 地址。 +在构建终端启动服务端: + +```sh +"$WORK/example/benchmark_fb_server" --listen_addr=127.0.0.1:0 --duration_s=300 +``` + +等待 `BRPC_FB_READY 127.0.0.1:`。另开终端时,必须重新设置 `WORK` 为构建时打印的 +绝对路径;shell 变量不会自动共享。将 `PORT_FROM_READY` 换为实际数字端口: + +```sh +WORK=/absolute/path/printed/by/the/build +SERVER=127.0.0.1:PORT_FROM_READY +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=16 --thread_num=2 --request_size=8193 --attachment_size=257 +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 \ + --request_size=0 --omit_message=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 --corrupt_request=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 +``` + +四条 client 命令均应退出 0,JSON 结果依次为: + +```text +{"completed":16,"successes":16,"expected_rejections":0,"failures":0} +{"completed":2,"successes":2,"expected_rejections":0,"failures":0} +{"completed":2,"successes":0,"expected_rejections":2,"failures":0} +{"completed":2,"successes":2,"expected_rejections":0,"failures":0} +``` + +`failures` 是 0/1 错误标志,不是失败 RPC 数量。破坏请求的命令只有在服务端返回 schema +拒绝错误码 -1,且响应和附件为空时才成功;连接失败、超时不能算作预期拒绝。 +最后一条正常请求验证同一服务端在拒绝后仍可工作。 + +服务端会在 300 秒后退出,也可用 Ctrl-C 提前停止。客户端的 `request_count` 为所有线程的 +总请求数,不是每线程数量;`request_size` 为字符串字节数,不是整个 frame 大小。 +字符串和附件含确定性的二进制数据,包括零字节。 + +| 参数 | 范围或含义 | +| --- | --- | +| `request_count` / `thread_num` | 1..1000 / 1..16 | +| `request_size` / `attachment_size` | 各为 0..1048576 字节 | +| `timeout_ms` / `deadline_ms` | 每 RPC 1..5000 毫秒 / 客户端总期限 1..120000 毫秒 | +| `connection_type` | `single`、`pooled`、`short` | +| `omit_message=true` | 不设置字符串,与空字符串不同 | +| `corrupt_request=true` | 破坏根偏移并要求 schema 拒绝 | +| 服务端 `duration_s` / `max_concurrency` | 1..3600 秒 / 1..64 | + +### 复用已有运行库或改为动态链接 + +已有兼容的 FB ON 运行库时,无需重编它:将 example 配置中的 `BRPC_ROOT` 替换为其 +`output/` 或安装前缀;`BRPC_FLATC_EXECUTABLE` 指向匹配的生成器,其他步骤不变。 +example 会读取该前缀真实的 `include/butil/config.h`,不会用替代头文件假装启用功能。 + +默认链接 `libbrpc.a`。`LINK_SO=ON` 只选择已经存在的共享库,不会替你构建运行库。 +在上面的完整构建之后,可按以下顺序切换并验证: + +```sh +cmake -S "$REPO" -B "$WORK/runtime" -DBUILD_SHARED_LIBS=ON +cmake --build "$WORK/runtime" --target brpc-shared --parallel "$JOBS" +cmake -S "$REPO/example/benchmark_fb" -B "$WORK/example" -DLINK_SO=ON +cmake --build "$WORK/example" --parallel "$JOBS" +(cd "$WORK/example" && ctest -V --no-tests=error --output-on-failure -R '^benchmark_fb_smoke$') +``` + +这几条重配置命令依赖此前已填写的 CMake cache;全新 build 目录仍需传完整参数。 +定制运行库若增加可选依赖,可通过 `BRPC_EXTRA_LIBRARIES` 补充链接库。 + +## 4. Make、Bazel 和完整回归 + +### Make + +在独立可写 checkout 中配置,沿用前述依赖变量: + +```sh +sh config_brpc.sh --with-flatbuffers \ + --headers="$FB/include $DEPS/include $OPENSSL/include" \ + --libs="$DEPS/lib $OPENSSL/lib" --cc="${CC:-cc}" --cxx="${CXX:-c++}" +make -j"$JOBS" +``` + +按平台替换为实际 `lib64` 或库目录。产出的 `output/` 可以作为 example 的 `BRPC_ROOT`。 +Make 单测通过 `FLATC="$FB/bin/flatc"` 选择官方生成器,但还需要已安装的 GoogleTest 库和 +gperftools,不是只提供 GoogleTest 源码即可;执行时也需能够加载 `test/libbrpc.dbg.*`。 +下方 ON runner 会构建 GoogleTest、设置测试库路径并校验报告;gperftools 等系统测试依赖 +仍需预先安装。 + +### Bazel + +```sh +bazel build --define=BRPC_WITH_FLATBUFFERS=true //:brpc +bazel test --define=BRPC_WITH_FLATBUFFERS=true --cache_test_results=no \ + //test:brpc_flatbuffers_unittest //test:brpc_flatbuffers_protocol_unittest +``` + +这些目标验证库,不会自动执行独立 example。不要将原始 `bazel-bin` 当成 `BRPC_ROOT`; +example 需要 CMake/Make output 或安装前缀的 include/lib 布局。 + +### 库单测与 ON gate + +单独配置库单测时,设置 `BUILD_UNIT_TESTS=ON`、匹配的 `FLATBUFFERS_FLATC_EXECUTABLE`; +若 `DOWNLOAD_GTEST=OFF`,还需指定 `BRPC_SYSTEM_GTEST_SOURCE_DIR`。 +构建并运行 `brpc_flatbuffers_unittest`、`brpc_flatbuffers_protocol_unittest`。 +仅打开库的 FlatBuffers 选项,不会自动执行测试。 + +要一次验证两组库单测、两项 codegen 验收及 example,可使用 +[ON runner](../../.github/scripts/flatbuffers-on.py)。完整 CMake 门禁需要 **CMake/CTest 3.21+** +以生成 JUnit XML 报告(`--output-junit`)。**`--work` 目录必须尚不存在**: + +```sh +python3 "$REPO/.github/scripts/flatbuffers-on.py" \ + --build-system cmake --source "$REPO" --work "$WORK/on-gate" --jobs "$JOBS" \ + --flatbuffers-prefix "$FB" \ + --dependency-prefix "$DEPS" --dependency-prefix "$OPENSSL" +``` + +该门禁要求 FlatBuffers 25.2.10。不传 `--flatbuffers-prefix` 时会下载、校验并构建该版本。 +GoogleTest 默认下载并校验固定的 1.14.0,也可用 `--gtest-source` 复用源码。 +平台常规依赖仍需预先安装。Make/Bazel 模式验证两组库测试,只有 CMake 模式额外构建 +生成器和 example。 + +检查 `WORK/on-gate/evidence/summary.json`:`status` 应为 `passed`,应包含所有预期测试, +执行数量非零,失败和跳过均为零。各步骤命令、日志及 XML 即使失败也会保留; +空测试、过滤后少跑或跳过不会被当作成功。 + +[CI 工作流](../../.github/workflows/flatbuffers-on.yml)覆盖 Linux CMake/Make 的 GCC、Clang, +Linux Bazel GCC,以及 macOS CMake。本地 gate 通过,不代表 GitHub 托管矩阵或另一套依赖 +组合已经通过。 + +## 5. 常见问题 + +| 现象 | 检查与处理 | +| --- | --- | +| `BRPC_ROOT is not FlatBuffers-enabled` 或缺少 FB 符号 | 用正确选项重编运行库和使用方,检查实际 output 的配置头;不要强制宏或混用 ON 头文件与 OFF 库。 | +| 找不到 `flatbuffers/idl.h`、`libflatbuffers` | 生成器需要完整官方开发安装,单有运行时头文件不够;明确指定 include 和 library 路径。 | +| 头文件与 `flatc` 版本不匹配 | 检查 `flatbuffers/base.h` 和 `flatc --version`,统一版本,在新目录重新生成代码,不删除版本断言。 | +| Protobuf/Abseil 缺头文件或链接符号 | 统一 Protobuf,提供其 config 包和 Abseil 前缀,避免系统与私有安装混用。 | +| macOS 找不到 OpenSSL | 设置实际 `OPENSSL_ROOT_DIR` 和 `CMAKE_PREFIX_PATH`,保持编译器、SDK、架构一致。 | +| 无法写 `src/butil/config.h.tmp` | 源码 checkout 必须可写;用独立副本隔离配置,不只隔离 build 目录。 | +| CMake 4 报旧依赖策略不兼容 | 对相关依赖使用适当的 policy compatibility 设置或升级依赖;不能借此降低 C++ 和依赖版本要求。 | +| `No tests were found` | 检查 example 的 build 目录、`BUILD_TESTING=ON` 和已构建的程序,或直接运行 `smoke.py`。 | +| `LINK_SO=ON` 找不到库或动态加载失败 | 先用 `BUILD_SHARED_LIBS=ON` 构建 `brpc-shared`,再检查共享库及传递依赖的运行时搜索路径。 | +| 连接拒绝或超时 | 等待 readiness,用当前端口,确保同一主机/容器且服务端未超过生命周期;不能计作 schema 拒绝成功。 | + +## 6. 接入应用时的边界 + +- 用官方 `flatc --cpp` 生成 table,再用 `brpc_flatc` 生成服务。每个 RPC 方法必须有稳定的 + 非负 int32 `(id: N)`;不要用声明顺序替代 ID,也不要将已删除 ID 分配给不同方法。 +- 业务 schema 使用独立 namespace。尤其 `flatc 2.0.x` 生成未全限定的 `flatbuffers::` 名称, + 不要把业务 schema 放在 `brpc` 下并依赖 include 顺序规避遮蔽。 +- `MessageBuilder` 完成 `Finish` 后调用 `ReleaseMessage()`;消息拥有 IOBuf 存储引用。 + 序列化共享的消息不是 copy-on-write,不要在其他调用仍读取时修改其内容。 +- frame 长度检查不等于 schema 验证。收到消息后先 `Verify()`,再访问 root; + 生成服务会在分派前检查请求,手写服务也应如此。RPC 成功不能替代响应 schema 验证。 +- Channel 使用 `fb_rpc`;服务通过 `AddFlatBuffersService` 注册。管理服务需在停止状态下 + 进行;成功实现必须按约定恰好执行一次 completion。借用的服务、描述符、请求及回调 + 所需对象应保持到相应生命周期结束。 +- 支持同步/异步、重试、backup request、三类连接及附件;不支持鉴权、压缩、校验和、 + streaming、HTTP/JSON 映射、SelectiveChannel/ParallelChannel 和 RPC-dump 回放。 + 本 example 不是公网或生产部署配置。 +- 协议 magic 是 `FRPC`,不是 `BRPC`;不要假定可与历史实验版本的 wire format 互通。 + +完整消息所有权、描述符及线格式约束参见[英文参考](../en/flatbuffers.md#message-construction-and-ownership)。 diff --git a/docs/en/flatbuffers.md b/docs/en/flatbuffers.md new file mode 100644 index 0000000000..f722e9cd5b --- /dev/null +++ b/docs/en/flatbuffers.md @@ -0,0 +1,443 @@ +# FlatBuffers messages and RPC + +[中文版](../cn/flatbuffers.md) + +bRPC provides optional IOBuf-backed FlatBuffers messages, builders, service +descriptors, and the `fb_rpc` transport. The implementation builds on +[apache/brpc#3196](https://github.com/apache/brpc/pull/3196) and +[apache/brpc#3197](https://github.com/apache/brpc/pull/3197), while preserving +Protocol's existing protobuf callback signatures. + +## Enable FlatBuffers + +FlatBuffers is **OFF by default**. Enable it when building bRPC, then build the +client/server against that same library and generated configuration header. +Adding `-DBRPC_WITH_FLATBUFFERS=1` to an application is not a substitute: the +feature changes the Channel, Controller and Server ABI. + +| Build system | Enable option | Exported runtime prefix | +| --- | --- | --- | +| CMake | `-DWITH_FLATBUFFERS=ON` | `/output` | +| Make | `config_brpc.sh --with-flatbuffers` | `/output` | +| Bazel | `--define=BRPC_WITH_FLATBUFFERS=true` on build/test commands | Bazel outputs, not an install prefix for the standalone example | + +### Dependencies and versions + +Prepare the normal bRPC dependencies described in [Getting started](getting_started.md): +a C++ toolchain, Protobuf compiler/development libraries, gflags, LevelDB, +OpenSSL and zlib. FlatBuffers RPC does **not** remove the Protobuf dependency. + +| Component | Additional requirement | +| --- | --- | +| bRPC message/RPC runtime | FlatBuffers headers; no `libflatbuffers` linkage | +| Official schema generation | `flatc` matching the runtime headers | +| bRPC service generation | `brpc_flatc`, built with matching official headers and `libflatbuffers` | +| Example smoke | Python 3; no GoogleTest requirement | +| Library unit tests | GoogleTest and the project's test dependencies | + +The repository's Bazel/ON gate pins FlatBuffers **25.2.10**. Use a complete, +matching installation to reproduce it; never remove the generated header's +version assertions. CMake runtime tests use `FLATBUFFERS_FLATC_EXECUTABLE`, +whereas generator acceptance and the example use `FLATC_EXECUTABLE`. +`BRPC_FLATC_EXECUTABLE` always means the bRPC generator, not official `flatc`. + +Use the same Protobuf installation across all builds. When CMake detects +`Protobuf_VERSION > 4.21`, C++17 and the corresponding Abseil dependencies are +required; generator/example builds need Protobuf's CMake config package to +export those dependencies. The runtime/example path requires CMake 3.16+; the CTest commands +below use CMake/CTest 3.17+ for `--no-tests=error`. With 3.16, run the Python smoke +directly. Use a single-configuration generator such as Unix Makefiles or Ninja. + +### CMake runtime build + +Run from a **writable checkout**, with a working compiler selected through +`CC`/`CXX` if necessary. Example-generated bindings and binaries stay in `WORK`, +but root bRPC configuration also writes **`src/butil/config.h` in the checkout**. +Do not configure ON/OFF or different build systems concurrently in the same +checkout, even with separate build directories; use independent writable copies. + +Replace the prefixes below with your installations (`include/`, `lib/` or +`lib64/`). A prefix may be reused for several dependencies. On macOS, select a +consistent compiler/SDK/architecture and the actual OpenSSL prefix; do not assume +that `/usr/local/opt/openssl` exists on Apple Silicon. + +```sh +REPO="$PWD" +DEPS=/absolute/path/to/dependency-prefix +FB=/absolute/path/to/flatbuffers-prefix +OPENSSL=/absolute/path/to/openssl-prefix +WORK="$(mktemp -d "${TMPDIR:-/tmp}/brpc-benchmark-fb.XXXXXX")" +JOBS=2 +printf 'WORK=%s\n' "$WORK" +``` + +If FlatBuffers is not installed, build a matching official source checkout first +(skip this block when `FB` already contains the required headers, compiler and +library). `FB` must be a writable installation prefix, not the source directory: + +```sh +FB_SOURCE=/absolute/path/to/flatbuffers-25.2.10-source +cmake -S "$FB_SOURCE" -B "$WORK/flatbuffers" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_INSTALL_PREFIX="$FB" -DCMAKE_INSTALL_LIBDIR=lib \ + -DFLATBUFFERS_BUILD_TESTS=OFF -DFLATBUFFERS_BUILD_FLATC=ON \ + -DFLATBUFFERS_BUILD_FLATLIB=ON -DFLATBUFFERS_BUILD_SHAREDLIB=OFF \ + -DFLATBUFFERS_INSTALL=ON -DFLATBUFFERS_LIBCXX_WITH_CLANG=OFF +cmake --build "$WORK/flatbuffers" --parallel "$JOBS" +cmake --install "$WORK/flatbuffers" +"$FB/bin/flatc" --version +``` + +Build the runtime without the unit-test dependencies: + +```sh +cmake -S "$REPO" -B "$WORK/runtime" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DWITH_FLATBUFFERS=ON -DBUILD_SHARED_LIBS=OFF \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DBUILD_UNIT_TESTS=OFF -DDOWNLOAD_GTEST=OFF -DBUILD_BRPC_TOOLS=OFF +cmake --build "$WORK/runtime" --parallel "$JOBS" +grep -E '^#define BRPC_WITH_FLATBUFFERS +1$' "$WORK/runtime/output/include/butil/config.h" +``` + +The final check must print `#define BRPC_WITH_FLATBUFFERS 1`. Public headers live +in `brpc/flatbuffers/`: `message.h` for messages/builders and `service.h` for +services/descriptors. The configuration macro is always 0 or 1; use `#if`, not +`#ifdef`. The policy argument above is an optional compatibility setting for +older dependencies under CMake 4, not a FlatBuffers switch or a replacement for +required dependency versions. + +### Make and Bazel alternatives + +In a separate writable checkout, using the same dependency prefixes: + +```sh +sh config_brpc.sh --with-flatbuffers \ + --headers="$FB/include $DEPS/include $OPENSSL/include" \ + --libs="$DEPS/lib $OPENSSL/lib" --cc="${CC:-cc}" --cxx="${CXX:-c++}" +make -j"$JOBS" +``` + +Use `lib64` or the platform's library directory where appropriate. The resulting +`output/` can replace `WORK/runtime/output` in the example command below. Make +unit tests take `FLATC="$FB/bin/flatc"` and require installed GoogleTest libraries +and gperftools, not just GoogleTest sources; their `test/libbrpc.dbg.*` must also +be loadable. The ON runner below builds GoogleTest, sets the test library path, +and validates reports; system test dependencies such as gperftools must already +be installed. + +Bazel supplies its pinned dependencies; pass the feature flag to both commands: + +```sh +bazel build --define=BRPC_WITH_FLATBUFFERS=true //:brpc +bazel test --define=BRPC_WITH_FLATBUFFERS=true --cache_test_results=no \ + //test:brpc_flatbuffers_unittest //test:brpc_flatbuffers_protocol_unittest +``` + +These Bazel targets verify the library, not the standalone example. Do not use a +raw `bazel-bin` directory as `BRPC_ROOT`; the example requires the include/lib +layout of a CMake/Make output or installed prefix. + +## Verify with the client/server example + +[example/benchmark_fb](../../example/benchmark_fb/README.md) is a **bounded +functional example**, not a performance benchmark. Its [schema](../../example/benchmark_fb/echo.fbs) +uses `BenchmarkService.Echo` with explicit wire ID 7. The +[server](../../example/benchmark_fb/server.cpp) calls `AddFlatBuffersService`; +the [client](../../example/benchmark_fb/client.cpp) uses the generated +`BenchmarkService::Stub` and `fb_rpc` channel. + +### Generate, build and smoke-test + +Continue in the same shell with the variables and runtime from above. An +existing compatible FB ON runtime may be used instead by changing `BRPC_ROOT`. +Build the bRPC generator, then the example: + +```sh +cmake -S "$REPO/tools/flatbuffers" -B "$WORK/codegen" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=OFF \ + -DCMAKE_PREFIX_PATH="$FB;$DEPS" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" +cmake --build "$WORK/codegen" --parallel "$JOBS" + +cmake -S "$REPO/example/benchmark_fb" -B "$WORK/example" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=ON \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DBRPC_ROOT="$WORK/runtime/output" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DFLATC_EXECUTABLE="$FB/bin/flatc" \ + -DBRPC_FLATC_EXECUTABLE="$WORK/codegen/brpc_flatc" +cmake --build "$WORK/example" --parallel "$JOBS" +(cd "$WORK/example" && ctest -V --no-tests=error --output-on-failure -R '^benchmark_fb_smoke$') +``` + +The example build runs **both** generators. `flatc` produces `echo_generated.h`; +`brpc_flatc` produces `echo.brpc.fb.h/.cpp`, all under `WORK/example/generated/`. +Do not commit them or reuse stale generated headers after changing versions. +If library discovery is ambiguous, pass `FLATBUFFERS_LIBRARY` explicitly when +configuring `brpc_flatc`. See the [generator guide](../../tools/flatbuffers/README.md) +for included schemas, explicit IDs and generator acceptance tests. + +Success means **one named CTest passed**, exit status 0, and this output: + +```text +benchmark_fb smoke passed: 13 verified replies, 2 schema rejections, clean shutdown +``` + +That is 15 RPCs, not 15 CTests. The smoke verifies binary bytes, empty/absent +strings, attachments, concurrency, single/pooled/short connections, schema +rejection and recovery on the same server. It starts its own loopback server on +an ephemeral port and reaps it; requiring SIGKILL is a failure. The orchestration +has a 35-second deadline and CTest a 50-second timeout. `No tests were found` +is not a pass. The same checks can be run without CTest: + +```sh +python3 "$REPO/example/benchmark_fb/smoke.py" \ + --server "$WORK/example/benchmark_fb_server" \ + --client "$WORK/example/benchmark_fb_client" +``` + +The default example links `libbrpc.a`. For `LINK_SO=ON`, first build the runtime +with `BUILD_SHARED_LIBS=ON` and the `brpc-shared` target, then reconfigure/rebuild +the example. `LINK_SO` alone cannot create a shared runtime. The +[example README](../../example/benchmark_fb/README.md) includes that sequence. + +### Run the programs separately + +Both processes must run on the **same host/container**: this example accepts +only `127.0.0.1`. In the build terminal: + +```sh +"$WORK/example/benchmark_fb_server" --listen_addr=127.0.0.1:0 --duration_s=300 +``` + +Wait for `BRPC_FB_READY 127.0.0.1:`. In another terminal, set `WORK` again +(shell variables are not shared), replace the port, and run: + +```sh +WORK=/absolute/path/printed/by/the/build +SERVER=127.0.0.1:PORT_FROM_READY +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=16 --thread_num=2 --request_size=8193 --attachment_size=257 +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 --request_size=0 --omit_message=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 --corrupt_request=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 +``` + +All four commands must exit 0. Their `(completed, successes, +expected_rejections, failures)` results are respectively `(16,16,0,0)`, +`(2,2,0,0)`, `(2,0,2,0)` and `(2,2,0,0)`. `failures` is a 0/1 flag, not an RPC +count. A corrupt request counts as an expected rejection only for the generated +server's schema error (-1) with an empty response/attachment; timeouts and +connection failures do not count. The last command verifies recovery. Stop the +server with Ctrl-C or let its 300-second lifetime expire. See the example README +for all bounded client/server parameters. + +## Additional tests and the ON gate + +For library unit tests, configure with `BUILD_UNIT_TESTS=ON`, +`FLATBUFFERS_FLATC_EXECUTABLE` pointing to the matching official compiler, and +`BRPC_SYSTEM_GTEST_SOURCE_DIR` pointing to GoogleTest sources when +`DOWNLOAD_GTEST=OFF`. Build and run `brpc_flatbuffers_unittest` and +`brpc_flatbuffers_protocol_unittest`; enabling the library alone does not run them. + +For the complete CMake path (both library suites, two codegen tests, and the +example smoke), the shared [ON runner](../../.github/scripts/flatbuffers-on.py) +can use the prefixes above. This full CMake gate requires **CMake/CTest 3.21+** +for JUnit XML output (`--output-junit`). **Its work directory must not already +exist**: + +```sh +python3 "$REPO/.github/scripts/flatbuffers-on.py" \ + --build-system cmake --source "$REPO" --work "$WORK/on-gate" --jobs "$JOBS" \ + --flatbuffers-prefix "$FB" \ + --dependency-prefix "$DEPS" --dependency-prefix "$OPENSSL" +``` + +This gate requires FlatBuffers 25.2.10. Omit `--flatbuffers-prefix` to let it +download/checksum/build that version; GoogleTest defaults to a checksum-pinned +1.14.0 download, or can be supplied with `--gtest-source`. Normal platform +dependencies must already be installed. Make/Bazel modes validate the two +library suites; only CMake mode also builds the generator and example. Inspect +`WORK/on-gate/evidence/summary.json` for `status: passed`, all required test names, +nonzero executed counts and zero failed/skipped cases. Commands, logs and XML +reports are retained even on failure. Empty/filtered/skipped runs are rejected. + +The [workflow](../../.github/workflows/flatbuffers-on.yml) runs Linux CMake/Make +with GCC and Clang, Linux Bazel with GCC, and macOS CMake. A local gate pass does +not imply the hosted GitHub matrix or a different dependency combination passed. + +## Troubleshooting + +| Symptom | Check / action | +| --- | --- | +| `BRPC_ROOT is not FlatBuffers-enabled` or missing FB symbols | Rebuild bRPC with the enable option, then rebuild all consumers. Check the actual output `include/butil/config.h`; do not force the macro or mix an ON header with an OFF library. | +| Missing `flatbuffers/idl.h` or `libflatbuffers` | The service generator needs the full official development installation, not only the runtime headers. Set `FLATBUFFERS_INCLUDE_DIR` and, if needed, `FLATBUFFERS_LIBRARY`. | +| Header/compiler version mismatch | Check `flatc --version`, select matching headers/compiler/library, and regenerate bindings in a fresh build directory. Do not delete upstream version checks. | +| Missing Protobuf/Abseil headers or link symbols | Use one compatible Protobuf installation throughout and expose its CMake config and Abseil prefixes. Do not mix system and private headers/libraries. | +| OpenSSL not found on macOS | Set `OPENSSL_ROOT_DIR` and include the actual installed prefix in `CMAKE_PREFIX_PATH`; check compiler/SDK/architecture consistency. | +| Cannot write `src/butil/config.h.tmp` | Root configuration needs a writable checkout. Use a private source copy; separate build directories do not isolate concurrent source configuration. | +| Old dependency policy error with CMake 4 | Try the appropriate `CMAKE_POLICY_VERSION_MINIMUM` compatibility setting for that dependency, or update it; this does not change the required C++ or dependency versions. | +| `No tests were found` | Use the example build directory, configure `BUILD_TESTING=ON`, build the executables, then run the named test or `smoke.py` directly. | +| `LINK_SO=ON` cannot find a library or the loader fails | Build `brpc-shared` with runtime `BUILD_SHARED_LIBS=ON` first. Check the shared library's dependencies and runtime search paths. | +| Connection refused/timeout | Wait for the readiness line, use its current port on the same host/container, and check the server's finite lifetime. These are not successful schema rejections. | + +Flatc 2.0.x emits unqualified `flatbuffers::` names. Keep business schemas outside +the `brpc` namespace (for example `myapp.rpc`); do not depend on include order to +avoid shadowing. Flatc 25.2.10 emits fully-qualified names. + +## Message construction and ownership + +Use upstream `flatc --cpp` to generate the schema's `*_generated.h`. Pass a +`brpc::flatbuffers::MessageBuilder` to the generated `Create...` functions, +call `Finish(root)`, and finally call `ReleaseMessage()`. + +* `ReleaseMessage()` does not copy payload bytes. The returned move-only Message + owns an IOBuf block reference and survives builder reuse or destruction. +* Message/builder moves leave the source reusable. Moving a shared-string builder + discards its optional deduplication cache; existing offsets remain valid. +* Importing an ordinary `::flatbuffers::FlatBufferBuilder` copies its payload and + scratch while preserving unfinished table state. The original allocator frees + the original storage, including owned custom allocators. No `free`/`delete[]` + guess or assumption about spare bytes before the payload is made. +* Use MessageBuilder's own move, swap, and release operations. Do not transfer it + through a base-class cast or use inherited raw-buffer release operations: a raw + FlatBuffers detached buffer would retain the address of its member allocator. +* 64 zero-initialized bytes precede a released payload. They may be shortened + with `reduce_meta_size_and_get_buf`; growing them is rejected without mutation. + Payload addresses and bytes are unchanged by shortening metadata. +* Serialization accepts a const Message and retains its storage in the output + IOBuf. The buffer is shared, not copy-on-write: do not mutate payload/metadata + while another reader or serialized buffer is using it. +* Allocation sizes are checked before narrowing to SingleIOBuf's uint32_t size. + Allocation failure is fatal, including release builds, independently of + bRPC's `crash_on_fatal_log` setting. These paths explicitly abort rather than + relying on `CHECK`/`LOG(FATAL)`. Upstream `vector_downward` cannot safely + continue with a null allocation result. + +`ParseFbFromIOBUF` checks sizes/framing and retains independent ownership. It +shares a contiguous input when the payload address is 64-byte aligned; fragmented +or insufficiently aligned input is copied into aligned storage. A builder's +allocation is 64-byte aligned, but the final payload need only have the alignment +required by its schema, so not every local message qualifies for receive-side +zero-copy. Alignments above 64 bytes are not supported. + +**Framing is not schema verification.** Call `msg.Verify()` before +`GetRoot()` or `GetMutableRoot()` on received data. Optional +FlatBuffers strings/vectors can still be null in a valid message. Failed framing +checks leave the prior message intact. + +## Service IDs and generation + +`BrpcDescriptorTable` contains a namespace, service name, whitespace-separated +method names, and explicit method IDs. IDs must be unique nonnegative int32 values. +An empty ID list assigns ordinal IDs to manually constructed descriptors; +generated services require explicit IDs: + +```fbs +rpc_service BenchmarkService { + First(Request):Response (id: 2); + Second(Request):Response (id: 5); +} +``` + +* `descriptor.method(position)` enumerates methods in declaration order. +* `method.index()` is the stable wire ID, not its array position. +* `descriptor.FindMethodByIndex(id)` looks up sparse wire IDs. A transport must + use this lookup rather than indexing a dense array with the wire ID. +* Never recycle a removed method ID for a different method. Removing or reordering + declarations leaves surviving explicit IDs unchanged. +* Namespace `a.b` and `a.b.` normalize to the same service name; empty namespace + means global scope. Method full names include their service. The service hash + uses the canonical full name and MurmurHash3 seed 1. Keep service names stable + when persisting or transmitting these IDs. +* Descriptors cannot be reinitialized after success. They own methods with RAII; + generated accessors use function-local static initialization for thread safety. + +The companion generator in `tools/flatbuffers/` uses the upstream parser to +produce service bindings. It is independently built; only that optional tool +needs `libflatbuffers`. See its [README](../../tools/flatbuffers/README.md) for +commands and limitations. Generated dispatch verifies requests, rejects +unknown/foreign methods, and runs non-null completion callbacks on failure, +including unimplemented methods. Successful implementations own completion and +must run their callback exactly once. + +## Network RPC + +Build both the library and its users with the same `BRPC_WITH_FLATBUFFERS` +setting; enabling the feature changes the Channel, Controller and Server ABI. +Register generated services with `server.AddFlatBuffersService(&service, +SERVER_DOESNT_OWN_SERVICE)`. Use `ChannelOptions::protocol = "fb_rpc"` and pass +that `brpc::Channel` to a generated stub. The stub calls `Channel::FBCallMethod`; +`Controller::flatbuffers_method()` returns the typed descriptor, while the +protobuf `Controller::method()` remains null. Manually supplied descriptors +must outlive their calls, including retries and backup requests. + +Service registration is separate from protobuf's AddService and ListServices +APIs. Add/remove/clear require a stopped (READY) server, not a server still +STOPPING; management operations must be externally serialized. Ownership +transfers only after successful registration. `RemoveFlatBuffersService` +deletes an owned service, while Stop/Join keep registrations for restart. +`GetFlatBuffersServiceCount()` reports this separate registry. Duplicate service +IDs (including hash collisions) and conflicting full protobuf/FlatBuffers method +names are rejected. String-based `MaxConcurrencyOf` supports FlatBuffers +methods, as do server-wide and default method concurrency limits. + +The transport supports synchronous/asynchronous calls, retries, backup requests, +single/pooled/short connections and request/response attachments. It allocates a +small independent header for each send and shares payload storage; it never +rewrites a const request's metadata prefix. Thus retries and concurrent calls +can safely share an immutable request. The application must still keep each +response and Controller alive until completion. + +Framing validates lengths, not application schemas. Generated services verify +requests before dispatch. Handwritten services must do the same. Callers must +verify received response schemas before using root accessors; an RPC succeeding +does not replace `response.Verify()`. + +Authentication, compression, checksums and streaming are not supported and are +rejected rather than silently ignored. FlatBuffers services cannot be accessed +through the internal, builtin-only port. SelectiveChannel/ParallelChannel, +HTTP/JSON mapping and RPC-dump replay are not provided by this transport. +Log IDs, user fields and distributed-tracing metadata are not transmitted. + +For a complete generated client/server, follow +[Verify with the client/server example](#verify-with-the-clientserver-example) +above. The example demonstrates these APIs without making a performance claim. + +### FRPC framing and compatibility + +A frame is `[12-byte header][metadata][message][attachment]`. The header contains +`FRPC`, a big-endian uint32 body size, and a big-endian uint32 metadata size; body +size excludes the 12-byte header but includes all three following sections. +The metadata prefix is explicitly little-endian, matching the original FRPC +experiment on little-endian machines without relying on packed structs: + +* Request: uint32 service ID, int32 method ID, int32 message size, int32 attachment + size, uint64 correlation ID (24 bytes). +* Response: int32 error code, int32 message size, int32 attachment size, uint64 + correlation ID (20 bytes). + +Readers require the whole known prefix, then skip unknown trailing metadata +using the advertised metadata size. Future optional fields must be appended; +do not reorder, resize or repurpose existing fields. A shorter prefix, negative +size, inconsistent payload length or excessive body is rejected. Error replies +have a nonzero error code and no payload. Method IDs are sparse wire IDs, never +array positions: adding, deleting or reordering declarations preserves surviving +IDs only when explicit IDs are retained. This does not make legacy ordinal-ID +schemas compatible, and removed IDs must never be reused. + +The magic is `FRPC`, not `BRPC`. This format does not promise compatibility with +older unpublished variants that used different magic, service hashes or native +big-endian metadata. The library's global Protocol hook signatures remain +unchanged, so existing protocol callback implementations need no adaptation. +Private numeric protocol IDs must not overlap newly assigned builtin IDs; +`PROTOCOL_FLATBUFFERS_RPC` uses ID 30. diff --git a/example/benchmark_fb/CMakeLists.txt b/example/benchmark_fb/CMakeLists.txt new file mode 100644 index 0000000000..8eb080c878 --- /dev/null +++ b/example/benchmark_fb/CMakeLists.txt @@ -0,0 +1,168 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +cmake_minimum_required(VERSION 3.16...3.28) +project(benchmark_fb LANGUAGES CXX) + +set(BRPC_ROOT "" CACHE PATH "FlatBuffers-enabled brpc output or install prefix") +set(BRPC_EXTRA_LIBRARIES "" CACHE STRING "Extra dependencies of this libbrpc, as a CMake list") +option(LINK_SO "Link the shared brpc library instead of libbrpc.a" OFF) +if(NOT BRPC_ROOT) + message(FATAL_ERROR "Set BRPC_ROOT to an existing WITH_FLATBUFFERS=ON output/install prefix") +endif() +get_filename_component(BRPC_ROOT "${BRPC_ROOT}" ABSOLUTE) +set(BRPC_INCLUDE_DIR "${BRPC_ROOT}/include") +if(NOT EXISTS "${BRPC_INCLUDE_DIR}/brpc/channel.h" OR + NOT EXISTS "${BRPC_INCLUDE_DIR}/butil/config.h") + message(FATAL_ERROR "BRPC_ROOT must contain include/brpc/channel.h and include/butil/config.h") +endif() +file(STRINGS "${BRPC_INCLUDE_DIR}/butil/config.h" _fb_enabled + REGEX "^#[ \t]*define[ \t]+BRPC_WITH_FLATBUFFERS[ \t]+1([ \t]|$)") +if(NOT _fb_enabled) + message(FATAL_ERROR + "BRPC_ROOT is not FlatBuffers-enabled. Rebuild brpc with WITH_FLATBUFFERS=ON; do not force BRPC_WITH_FLATBUFFERS with -D.") +endif() +# Re-resolve when BRPC_ROOT or LINK_SO changes; never select another installed brpc. +unset(_BRPC_LIBRARY CACHE) +if(LINK_SO) + set(_brpc_names "${CMAKE_SHARED_LIBRARY_PREFIX}brpc${CMAKE_SHARED_LIBRARY_SUFFIX}") +else() + set(_brpc_names libbrpc.a) +endif() +find_library(_BRPC_LIBRARY NAMES ${_brpc_names} + PATHS "${BRPC_ROOT}/lib" "${BRPC_ROOT}/lib64" NO_DEFAULT_PATH) +if(NOT _BRPC_LIBRARY) + message(FATAL_ERROR "The requested brpc library was not found under BRPC_ROOT/lib or lib64") +endif() + +find_path(FLATBUFFERS_INCLUDE_DIR NAMES flatbuffers/flatbuffers.h) +find_program(FLATC_EXECUTABLE NAMES flatc) +find_program(BRPC_FLATC_EXECUTABLE NAMES brpc_flatc HINTS "${BRPC_ROOT}/bin") +if(NOT FLATBUFFERS_INCLUDE_DIR OR NOT FLATC_EXECUTABLE OR NOT BRPC_FLATC_EXECUTABLE) + message(FATAL_ERROR "Set FLATBUFFERS_INCLUDE_DIR, FLATC_EXECUTABLE and BRPC_FLATC_EXECUTABLE") +endif() +if(NOT EXISTS "${FLATBUFFERS_INCLUDE_DIR}/flatbuffers/base.h") + message(FATAL_ERROR "FLATBUFFERS_INCLUDE_DIR must contain flatbuffers/base.h") +endif() +foreach(_part MAJOR MINOR REVISION) + file(STRINGS "${FLATBUFFERS_INCLUDE_DIR}/flatbuffers/base.h" _version_line + REGEX "^#define[ \t]+FLATBUFFERS_VERSION_${_part}[ \t]+[0-9]+") + if(NOT _version_line MATCHES "FLATBUFFERS_VERSION_${_part}[ \t]+([0-9]+)") + message(FATAL_ERROR "Cannot read FlatBuffers ${_part} version from base.h") + endif() + set(_fb_${_part} "${CMAKE_MATCH_1}") +endforeach() +set(_headers_version "${_fb_MAJOR}.${_fb_MINOR}.${_fb_REVISION}") +execute_process(COMMAND "${FLATC_EXECUTABLE}" --version + RESULT_VARIABLE _flatc_result OUTPUT_VARIABLE _flatc_version + ERROR_VARIABLE _flatc_error OUTPUT_STRIP_TRAILING_WHITESPACE TIMEOUT 5) +if(NOT _flatc_result STREQUAL "0" OR + NOT _flatc_version MATCHES "flatc version ([0-9]+\\.[0-9]+\\.[0-9]+)") + message(FATAL_ERROR "Cannot execute official flatc --version: ${_flatc_error}") +endif() +set(_compiler_version "${CMAKE_MATCH_1}") +if(NOT _compiler_version VERSION_EQUAL _headers_version) + message(FATAL_ERROR + "FlatBuffers headers ${_headers_version} do not match flatc ${_compiler_version}") +endif() +execute_process(COMMAND "${BRPC_FLATC_EXECUTABLE}" --help + RESULT_VARIABLE _brpc_flatc_result OUTPUT_QUIET ERROR_VARIABLE _brpc_flatc_error TIMEOUT 5) +if(NOT _brpc_flatc_result STREQUAL "0") + message(FATAL_ERROR "Cannot execute BRPC_FLATC_EXECUTABLE: ${_brpc_flatc_error}") +endif() + +find_package(Threads REQUIRED) +find_package(Protobuf CONFIG QUIET) +set(_protobuf_config ${Protobuf_FOUND}) +if(NOT Protobuf_FOUND) + find_package(Protobuf REQUIRED) +endif() +find_package(OpenSSL REQUIRED) +find_package(ZLIB REQUIRED) +find_path(GFLAGS_INCLUDE_DIR NAMES gflags/gflags.h) +find_library(GFLAGS_LIBRARY NAMES gflags) +find_library(LEVELDB_LIBRARY NAMES leveldb) +if(NOT GFLAGS_INCLUDE_DIR OR NOT GFLAGS_LIBRARY OR NOT LEVELDB_LIBRARY) + message(FATAL_ERROR "Install gflags and leveldb, or set CMAKE_PREFIX_PATH to the runtime dependency prefix") +endif() +set(_cxx_standard 14) +if(Protobuf_VERSION VERSION_GREATER 4.21) + set(_cxx_standard 17) + if(NOT _protobuf_config) + message(FATAL_ERROR "Protobuf 5+ needs its CMake config package to export transitive Abseil dependencies") + endif() +endif() + +add_library(benchmark_fb_dependencies INTERFACE) +# Keep the actual runtime configuration ahead of dependency include directories. +target_include_directories(benchmark_fb_dependencies INTERFACE + "${BRPC_INCLUDE_DIR}" "${FLATBUFFERS_INCLUDE_DIR}" "${GFLAGS_INCLUDE_DIR}") +target_compile_features(benchmark_fb_dependencies INTERFACE cxx_std_${_cxx_standard}) +target_link_libraries(benchmark_fb_dependencies INTERFACE + "${_BRPC_LIBRARY}" protobuf::libprotobuf "${GFLAGS_LIBRARY}" "${LEVELDB_LIBRARY}" + OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB Threads::Threads ${CMAKE_DL_LIBS} + ${BRPC_EXTRA_LIBRARIES}) +file(STRINGS "${BRPC_INCLUDE_DIR}/butil/config.h" _glog_enabled + REGEX "^#[ \t]*define[ \t]+BRPC_WITH_GLOG[ \t]+1([ \t]|$)") +if(_glog_enabled) + find_package(glog CONFIG REQUIRED) + target_link_libraries(benchmark_fb_dependencies INTERFACE glog::glog) +endif() +if(APPLE) + target_link_libraries(benchmark_fb_dependencies INTERFACE + "-framework CoreFoundation" "-framework CoreGraphics" "-framework CoreData" + "-framework CoreText" "-framework Security" "-framework Foundation") + target_link_options(benchmark_fb_dependencies INTERFACE + "LINKER:-U,_MallocExtension_ReleaseFreeMemory" "LINKER:-U,_ProfilerStart" + "LINKER:-U,_ProfilerStop" "LINKER:-U,__Z13GetStackTracePPvii" + "LINKER:-U,_mallctl" "LINKER:-U,_malloc_stats_print") +endif() + +set(_generated "${CMAKE_CURRENT_BINARY_DIR}/generated") +set(_schema "${CMAKE_CURRENT_SOURCE_DIR}/echo.fbs") +set(_generated_files "${_generated}/echo_generated.h" + "${_generated}/echo.brpc.fb.h" "${_generated}/echo.brpc.fb.cpp") +add_custom_command(OUTPUT ${_generated_files} + COMMAND "${CMAKE_COMMAND}" -E make_directory "${_generated}" + COMMAND "${FLATC_EXECUTABLE}" --cpp -o "${_generated}" "${_schema}" + COMMAND "${BRPC_FLATC_EXECUTABLE}" -o "${_generated}" "${_schema}" + DEPENDS "${_schema}" "${FLATC_EXECUTABLE}" "${BRPC_FLATC_EXECUTABLE}" + VERBATIM) +add_custom_target(benchmark_fb_codegen DEPENDS ${_generated_files}) +add_library(benchmark_fb_schema STATIC "${_generated}/echo.brpc.fb.cpp") +add_dependencies(benchmark_fb_schema benchmark_fb_codegen) +target_include_directories(benchmark_fb_schema PUBLIC "${_generated}") +target_link_libraries(benchmark_fb_schema PUBLIC benchmark_fb_dependencies) +foreach(_program client server) + add_executable(benchmark_fb_${_program} "${_program}.cpp") + add_dependencies(benchmark_fb_${_program} benchmark_fb_codegen) + target_link_libraries(benchmark_fb_${_program} PRIVATE benchmark_fb_schema) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(benchmark_fb_${_program} PRIVATE -Wall -Wextra) + endif() +endforeach() + +include(CTest) +if(BUILD_TESTING) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + add_test(NAME benchmark_fb_smoke + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/smoke.py" + --server "$" + --client "$") + set_tests_properties(benchmark_fb_smoke PROPERTIES TIMEOUT 50 LABELS "flatbuffers;smoke") +endif() +message(STATUS "benchmark_fb: brpc=${_BRPC_LIBRARY}, FlatBuffers=${_headers_version}") diff --git a/example/benchmark_fb/README.md b/example/benchmark_fb/README.md new file mode 100644 index 0000000000..9578c2b732 --- /dev/null +++ b/example/benchmark_fb/README.md @@ -0,0 +1,254 @@ +# FlatBuffers RPC example + +[English enablement guide](../../docs/en/flatbuffers.md) | +[中文启用与验证指南](../../docs/cn/flatbuffers.md) + +This directory contains independent client and server programs using the +`fb_rpc` protocol, generated FlatBuffers services, and +`brpc::flatbuffers::Message`. The directory name is retained for continuity; +this is a **bounded functional example**, not a performance comparison. + +The schema declares `BenchmarkService.Echo` with stable wire method ID **7**. +The generated stub calls `Channel::FBCallMethod`; the server registers the +generated service with `Server::AddFlatBuffersService`. + +## Prerequisites + +Use an existing local C++ toolchain and matching development installations of +Protobuf, gflags, leveldb, OpenSSL, zlib, and FlatBuffers. The runtime is not +Protobuf-free: its controller/closure interfaces and other bRPC components still +need Protobuf. The projects require CMake 3.16 or newer; the CTest commands below +use CMake/CTest 3.17+ for `--no-tests=error`. With 3.16, run `smoke.py` directly. +Use a single-configuration generator (Unix Makefiles or Ninja) and Python 3 for +the smoke test. The commands below target +Linux and macOS and do not download dependencies. + +The runtime needs FlatBuffers headers, but building `brpc_flatc` also needs the +official `libflatbuffers`. The official `flatc` generates table types; it cannot +replace `brpc_flatc`, which generates bRPC services. The repository's ON gate pins +FlatBuffers 25.2.10; use a matching complete installation to reproduce that gate. + +Use the **same FlatBuffers release** for the runtime headers, official `flatc`, +and the headers/library used to build `brpc_flatc`. Use the same Protobuf +installation and compatible compiler/ABI for the runtime and this example. +When CMake detects `Protobuf_VERSION > 4.21`, the example requires C++17 and a +Protobuf CMake config package exporting its transitive Abseil dependencies. +An additional dependency prefix can be appended to `CMAKE_PREFIX_PATH` with a +semicolon. + +## Build runtime, generator, then example + +Run from a **writable** brpc checkout. Replace `DEPS`, `FB`, and `OPENSSL` with +local installation prefixes containing `include/` and `lib/` (or `lib64/`); they +may be the same directory. Set `CC`/`CXX` to a working compiler before configuring +any build. On macOS, use a consistent compiler, SDK and architecture, and pass +the actual OpenSSL prefix rather than assuming an Intel Homebrew path. + +Example bindings and build products stay under `WORK`. However, the root brpc +configuration also generates `src/butil/config.h` in the checkout. A read-only +source mount will fail, and separate build directories alone do not isolate +concurrent ON/OFF configurations. Use separate writable checkouts for those. + +The policy argument is an optional compatibility setting for older dependencies +that report unsupported policies with CMake 4. It does not replace the required +compiler/dependency versions or enable FlatBuffers by itself. + +```sh +REPO="$PWD" +DEPS=/absolute/path/to/dependency-prefix +FB=/absolute/path/to/flatbuffers-prefix +OPENSSL=/absolute/path/to/openssl-prefix +WORK="$(mktemp -d "${TMPDIR:-/tmp}/brpc-benchmark-fb.XXXXXX")" +JOBS=2 +printf 'WORK=%s\n' "$WORK" + +cmake -S "$REPO" -B "$WORK/runtime" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DWITH_FLATBUFFERS=ON -DBUILD_SHARED_LIBS=OFF \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DBUILD_UNIT_TESTS=OFF -DDOWNLOAD_GTEST=OFF -DBUILD_BRPC_TOOLS=OFF +cmake --build "$WORK/runtime" --parallel "$JOBS" + +cmake -S "$REPO/tools/flatbuffers" -B "$WORK/codegen" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=OFF \ + -DCMAKE_PREFIX_PATH="$FB;$DEPS" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" +cmake --build "$WORK/codegen" --parallel "$JOBS" + +cmake -S "$REPO/example/benchmark_fb" -B "$WORK/example" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_TESTING=ON \ + -DCMAKE_PREFIX_PATH="$DEPS;$FB;$OPENSSL" \ + -DOPENSSL_ROOT_DIR="$OPENSSL" \ + -DBRPC_ROOT="$WORK/runtime/output" \ + -DFLATBUFFERS_INCLUDE_DIR="$FB/include" \ + -DFLATC_EXECUTABLE="$FB/bin/flatc" \ + -DBRPC_FLATC_EXECUTABLE="$WORK/codegen/brpc_flatc" +cmake --build "$WORK/example" --parallel "$JOBS" +(cd "$WORK/example" && ctest -V --no-tests=error --output-on-failure -R '^benchmark_fb_smoke$') +``` + +Success must include the named `benchmark_fb_smoke` test, exit status 0, and: + +```text +benchmark_fb smoke passed: 13 verified replies, 2 schema rejections, clean shutdown +``` + +`No tests were found` is not a successful verification. Configure with +`-DBUILD_TESTING=ON`, build the example, and run CTest from the example build +folder, not the brpc runtime folder. + +`BRPC_ROOT` can instead point to an already-built FB ON `output/` directory or +an installed prefix containing `include/` and `lib/` (or `lib64/`). The example +reads that prefix's real `include/butil/config.h` and rejects an FB OFF build. +It never generates a substitute configuration header or defines +`BRPC_WITH_FLATBUFFERS` to pretend the library supports this feature. + +The official `flatc` version must match `FLATBUFFERS_INCLUDE_DIR`; configuration +fails early on a mismatch. `BRPC_FLATC_EXECUTABLE` is the brpc generator, **not** +the official `flatc`. Both generators run during the example build and write +only to `build-directory/generated/`. Do not commit `*_generated.h` or +`*.brpc.fb.h` / `*.brpc.fb.cpp`. + +The default links `libbrpc.a`. `-DLINK_SO=ON` selects an **already built** shared +library; it does not create one. After the commands above, a shared-library +variant can be built and checked with: + +```sh +cmake -S "$REPO" -B "$WORK/runtime" -DBUILD_SHARED_LIBS=ON +cmake --build "$WORK/runtime" --target brpc-shared --parallel "$JOBS" +cmake -S "$REPO/example/benchmark_fb" -B "$WORK/example" -DLINK_SO=ON +cmake --build "$WORK/example" --parallel "$JOBS" +(cd "$WORK/example" && ctest -V --no-tests=error --output-on-failure -R '^benchmark_fb_smoke$') +``` + +These reconfigure commands reuse the previously populated CMake caches. For a +fresh build directory, pass the complete configuration options again. Optional +runtime features may require extra dependencies via +`-DBRPC_EXTRA_LIBRARIES='library1;library2'`. Use `CMAKE_CXX_COMPILER` consistently +across all three builds when a non-default compiler is required. An FB ON header +and an FB OFF binary mixed into one prefix are not a supported installation; +rebuild or reinstall that prefix rather than overriding feature macros. + +## Reuse an existing FB ON runtime + +Skip the runtime build when a compatible library already exists. Set `REPO`, +`DEPS`, `FB`, `OPENSSL`, `WORK`, and `JOBS` as above; build `brpc_flatc` if needed. +In the example configure command, replace `-DBRPC_ROOT="$WORK/runtime/output"` +with your existing output/install prefix and set `BRPC_FLATC_EXECUTABLE` to the +matching generator. All remaining configure, build and smoke steps are the same. +A Make build exports `output/`; a raw Bazel build directory is not an installed +prefix with the layout expected by `BRPC_ROOT`. + +## Run separately + +Run the server and client on the **same host/container**. This example rejects +non-loopback addresses. In the build terminal: + +```sh +"$WORK/example/benchmark_fb_server" --listen_addr=127.0.0.1:0 --duration_s=300 +``` + +It prints a flushed `BRPC_FB_READY 127.0.0.1:` line only after +`Server::Start` succeeds and termination handlers are installed. Use that actual +endpoint in another terminal. Shell variables are not shared between terminals: +set `WORK` to the absolute path printed by the build and replace `PORT_FROM_READY` +with the numeric port. The server exits after 300 seconds, or earlier on Ctrl-C. + +```sh +WORK=/absolute/path/printed/by/the/build +SERVER=127.0.0.1:PORT_FROM_READY +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=16 --thread_num=2 --request_size=8193 --attachment_size=257 +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 --request_size=0 --omit_message=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 --corrupt_request=true +"$WORK/example/benchmark_fb_client" --server="$SERVER" \ + --request_count=2 --thread_num=1 +``` + +The first command must exit 0 and print: + +```json +{"completed":16,"successes":16,"expected_rejections":0,"failures":0} +``` + +The corrupt-request command must also exit 0, but with a different result: + +```json +{"completed":2,"successes":0,"expected_rejections":2,"failures":0} +``` + +The final normal command verifies recovery against the same server and must +report `completed=2`, `successes=2`, `expected_rejections=0`, `failures=0`. + +The client always verifies the response schema **before** reading it, then +compares the request ID, every string byte, optional-string presence, attachment +size, and attachment bytes. Both empty and absent strings are supported. +`request_size` means string bytes, not total serialized frame size. Payloads and +attachments contain deterministic binary bytes, including zero bytes. + +`--corrupt_request=true` damages the FlatBuffers root offset but leaves FRPC +framing valid. Exit status 0 in this mode means every request received the +expected generated-service schema rejection (`RpcController::SetFailed(string)`, +error code -1). A connection error, timeout, accepted request, or unexpected +response is a failure, not a successful negative test. JSON output distinguishes +`successes` from `expected_rejections`; `failures` is a 0/1 failure flag, not a +failed-RPC count. It contains no throughput estimate. + +### Bounded controls + +| Client option | Default | Allowed | +| --- | --- | --- | +| `server` | required | `127.0.0.1:` | +| `request_count` | 16 | 1..1000 total, not per thread | +| `thread_num` | 2 | 1..16 synchronous callers | +| `request_size` | 64 | 0..1048576 string bytes | +| `attachment_size` | 0 | 0..1048576 bytes | +| `timeout_ms` | 1500 | 1..5000 per RPC | +| `deadline_ms` | 30000 | 1..120000 for the client run | +| `connection_type` | `single` | `single`, `pooled`, `short` | +| `omit_message` | false | Preserve absent rather than empty string | +| `corrupt_request` | false | Require schema rejection | + +The server defaults to `127.0.0.1:0`, `duration_s=60` (1..3600), and +`max_concurrency=32` (1..64). It stops earlier on SIGINT/SIGTERM and calls +`Stop`/`Join`. Both programs deliberately accept only IPv4 loopback endpoints. +Builtin services are disabled. FRPC does not provide authentication, +compression, checksums, or streaming; none is enabled by this example. It is not +a production/public-network deployment configuration. + +## Smoke test behavior + +CTest invokes the standard-library-only `smoke.py`. It: + +1. Starts its own server on `127.0.0.1:0` and reads the readiness endpoint. +2. Verifies finite RPCs with binary attachments, empty and absent strings, and + single/pooled/short connections. +3. Requires two malformed-schema requests to be rejected. +4. Verifies subsequent normal requests against the same server. +5. Terminates and reaps only the server process it started. + +There are 13 verified replies and 2 expected rejections. Readiness and client +subprocesses have deadlines, the orchestration deadline is 35 seconds, and +CTest has a 50-second outer timeout. Shutdown allows 3 seconds before killing +and reaping a stuck server; requiring that fallback fails the smoke. No fixed +port or external server is needed, and the owned server is reaped before return. +Generated code remains only in the build directory; the smoke does not modify +source files. + +For OFF-prefix rejection, version mismatch, missing libraries, empty CTest +runs, or connection failures, see the enablement guide's +[troubleshooting table](../../docs/en/flatbuffers.md#troubleshooting). + +The same test can be invoked without CTest: + +```sh +python3 "$REPO/example/benchmark_fb/smoke.py" \ + --server "$WORK/example/benchmark_fb_server" \ + --client "$WORK/example/benchmark_fb_client" +``` diff --git a/example/benchmark_fb/client.cpp b/example/benchmark_fb/client.cpp new file mode 100644 index 0000000000..4900d366c9 --- /dev/null +++ b/example/benchmark_fb/client.cpp @@ -0,0 +1,219 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "butil/config.h" +#include "butil/endpoint.h" +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "echo.brpc.fb.h" + +#if !BRPC_WITH_FLATBUFFERS +#error "benchmark_fb requires a FlatBuffers-enabled brpc library" +#endif + +DEFINE_string(server, "", "Required IPv4 loopback endpoint printed by benchmark_fb_server"); +DEFINE_string(connection_type, "single", "single, pooled, or short"); +DEFINE_int32(request_count, 16, "Total requests across all threads, 1..1000"); +DEFINE_int32(thread_num, 2, "Number of concurrent synchronous callers, 1..16"); +DEFINE_int32(request_size, 64, "Bytes in the optional request string, 0..1048576"); +DEFINE_int32(attachment_size, 0, "Bytes in the echoed attachment, 0..1048576"); +DEFINE_int32(timeout_ms, 1500, "Per-RPC timeout, 1..5000 milliseconds"); +DEFINE_int32(deadline_ms, 30000, "Overall client deadline, 1..120000 milliseconds"); +DEFINE_bool(omit_message, false, "Omit the optional string rather than sending an empty string"); +DEFINE_bool(corrupt_request, false, "Send an invalid root offset and require schema rejection"); + +namespace { +using brpc::flatbuffers::Message; +using brpc::flatbuffers::MessageBuilder; + +std::string MakeBytes(size_t size, unsigned multiplier) { + std::string bytes(size, '\0'); + for (size_t i = 0; i < size; ++i) { + bytes[i] = static_cast((i * multiplier) % 251); + } + return bytes; +} + +Message MakeRequest(uint64_t request_id, const std::string& text) { + MessageBuilder builder; + const auto message = FLAGS_omit_message ? + ::flatbuffers::Offset<::flatbuffers::String>() : builder.CreateString(text); + builder.Finish(benchmark_fb::CreateRequest(builder, request_id, message)); + Message request = builder.ReleaseMessage(); + if (FLAGS_corrupt_request) { + // Damage only the schema, not FRPC framing, so the server's generated + // dispatcher must reject the request before invoking the service. + memset(request.mutable_data(), 0xff, sizeof(::flatbuffers::uoffset_t)); + } + return request; +} + +bool CheckResponse(const Message& response, uint64_t request_id, + const std::string& text, const std::string& attachment, + const brpc::Controller& cntl) { + if (!response.Verify()) { + return false; + } + const auto* output = response.GetRoot(); + if (output->request_id() != request_id || + output->attachment_size() != attachment.size() || + cntl.response_attachment().to_string() != attachment) { + return false; + } + if (FLAGS_omit_message) { + return output->message() == nullptr; + } + return output->message() != nullptr && output->message()->str() == text; +} + +bool ValidFlags(butil::EndPoint* endpoint) { + return FLAGS_server.compare(0, 10, "127.0.0.1:") == 0 && + butil::str2endpoint(FLAGS_server.c_str(), endpoint) == 0 && endpoint->port > 0 && + (FLAGS_connection_type == "single" || FLAGS_connection_type == "pooled" || + FLAGS_connection_type == "short") && + FLAGS_request_count >= 1 && FLAGS_request_count <= 1000 && + FLAGS_thread_num >= 1 && FLAGS_thread_num <= 16 && + FLAGS_request_size >= 0 && FLAGS_request_size <= 1024 * 1024 && + FLAGS_attachment_size >= 0 && FLAGS_attachment_size <= 1024 * 1024 && + FLAGS_timeout_ms >= 1 && FLAGS_timeout_ms <= 5000 && + FLAGS_deadline_ms >= 1 && FLAGS_deadline_ms <= 120000; +} + +int RunClient(const butil::EndPoint& endpoint) { + brpc::Channel channel; + brpc::ChannelOptions options; + options.protocol = "fb_rpc"; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms; + options.connect_timeout_ms = std::min(FLAGS_timeout_ms, 1000); + options.max_retry = 0; + if (channel.Init(endpoint, &options) != 0) { + std::cerr << "Cannot initialize FlatBuffers channel\n"; + return 1; + } + // Generated stub methods route through Channel::FBCallMethod. + benchmark_fb::BenchmarkService::Stub stub(&channel); + const std::string text = MakeBytes(FLAGS_omit_message ? 0 : FLAGS_request_size, 31); + const std::string attachment = MakeBytes(FLAGS_attachment_size, 17); + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(FLAGS_deadline_ms); + std::atomic next{0}; + std::atomic successes{0}; + std::atomic expected_rejections{0}; + std::atomic failed{false}; + std::mutex error_mutex; + auto fail = [&](const std::string& message) { + if (!failed.exchange(true)) { + std::lock_guard lock(error_mutex); + std::cerr << message << '\n'; + } + }; + auto worker = [&] { + try { + while (!failed.load()) { + const int index = next.fetch_add(1); + if (index >= FLAGS_request_count) { + break; + } + const int64_t remaining = std::chrono::duration_cast< + std::chrono::milliseconds>(deadline - + std::chrono::steady_clock::now()).count(); + if (remaining <= 0) { + fail("Overall client deadline exceeded"); + break; + } + const uint64_t request_id = static_cast(index) + 1; + const Message request = MakeRequest(request_id, text); + Message response; + brpc::Controller cntl; + cntl.set_timeout_ms(std::min(FLAGS_timeout_ms, remaining)); + cntl.request_attachment().append(attachment); + stub.Echo(&cntl, &request, &response, nullptr); + if (FLAGS_corrupt_request) { + // The generated generic RpcController::SetFailed(string) + // maps to -1. Timeouts and connection errors do not count. + if (!cntl.Failed() || cntl.ErrorCode() != -1 || + response.size() != 0 || !cntl.response_attachment().empty()) { + fail("Expected server schema rejection; error=" + cntl.ErrorText()); + break; + } + ++expected_rejections; + } else if (cntl.Failed()) { + fail("RPC failed: " + cntl.ErrorText()); + break; + } else if (!CheckResponse(response, request_id, text, attachment, cntl)) { + fail("Response schema, fields, optional string or attachment mismatch"); + break; + } else { + ++successes; + } + } + } catch (const std::exception& error) { + fail(std::string("Client worker failed: ") + error.what()); + } + }; + std::vector workers; + workers.reserve(FLAGS_thread_num); + try { + for (int i = 0; i < FLAGS_thread_num; ++i) { + workers.emplace_back(worker); + } + } catch (const std::exception& error) { + fail(std::string("Cannot start client worker: ") + error.what()); + } + for (auto& thread : workers) { + thread.join(); + } + const int completed = successes.load() + expected_rejections.load(); + if (completed != FLAGS_request_count) { + fail("Not all requested RPCs completed"); + } + std::cout << "{\"completed\":" << completed + << ",\"successes\":" << successes.load() + << ",\"expected_rejections\":" << expected_rejections.load() + << ",\"failures\":" << (failed.load() ? 1 : 0) << "}\n"; + return failed.load() ? 1 : 0; +} +} // namespace + +int main(int argc, char* argv[]) { + GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); + butil::EndPoint endpoint; + if (!ValidFlags(&endpoint)) { + std::cerr << "Invalid flags; --server=127.0.0.1:PORT is required. " + "See --help and README.md for bounded parameter ranges.\n"; + return 2; + } + try { + return RunClient(endpoint); + } catch (const std::exception& error) { + std::cerr << "Client failed: " << error.what() << '\n'; + return 1; + } +} diff --git a/example/benchmark_fb/echo.fbs b/example/benchmark_fb/echo.fbs new file mode 100644 index 0000000000..f7a40eae2b --- /dev/null +++ b/example/benchmark_fb/echo.fbs @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +namespace benchmark_fb; + +table Request { + request_id:ulong; + message:string; +} + +table Response { + request_id:ulong; + message:string; + attachment_size:ulong; +} + +rpc_service BenchmarkService { + Echo(Request):Response (id: 7); +} + +root_type Request; diff --git a/example/benchmark_fb/server.cpp b/example/benchmark_fb/server.cpp new file mode 100644 index 0000000000..f8fa9244ef --- /dev/null +++ b/example/benchmark_fb/server.cpp @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include +#include +#include +#include +#include "butil/config.h" +#include "butil/endpoint.h" +#include "brpc/closure_guard.h" +#include "brpc/controller.h" +#include "brpc/errno.pb.h" +#include "brpc/server.h" +#include "echo.brpc.fb.h" + +#if !BRPC_WITH_FLATBUFFERS +#error "benchmark_fb requires a FlatBuffers-enabled brpc library" +#endif + +DEFINE_string(listen_addr, "127.0.0.1:0", "Loopback IPv4 address; port 0 selects a free port"); +DEFINE_int32(duration_s, 60, "Exit after 1..3600 seconds, or earlier on SIGINT/SIGTERM"); +DEFINE_int32(max_concurrency, 32, "Maximum concurrent requests, 1..64"); + +namespace { +const size_t kMaxBytes = 1024 * 1024; + +class BenchmarkServiceImpl : public benchmark_fb::BenchmarkService { +public: + void Echo(google::protobuf::RpcController* controller, + const brpc::flatbuffers::Message* request, + brpc::flatbuffers::Message* response, + google::protobuf::Closure* done) override { + brpc::ClosureGuard done_guard(done); + if (!controller) { + return; + } + auto* cntl = static_cast(controller); + // The generated dispatcher also verifies the schema. Keep this guard + // for callers invoking the typed service method directly. + if (!request || !response || !request->Verify()) { + cntl->SetFailed(brpc::EREQUEST, "Invalid FlatBuffers Request"); + return; + } + const auto* input = request->GetRoot(); + if ((input->message() && input->message()->size() > kMaxBytes) || + cntl->request_attachment().size() > kMaxBytes) { + cntl->SetFailed(brpc::EREQUEST, "Example payload limit is 1 MiB per field"); + return; + } + brpc::flatbuffers::MessageBuilder builder; + // Preserve absent versus empty strings, including embedded zero bytes. + const auto text = input->message() ? + builder.CreateString(input->message()->str()) : + ::flatbuffers::Offset<::flatbuffers::String>(); + builder.Finish(benchmark_fb::CreateResponse( + builder, input->request_id(), text, cntl->request_attachment().size())); + *response = builder.ReleaseMessage(); + cntl->response_attachment().append(cntl->request_attachment()); + } +}; +} // namespace + +int main(int argc, char* argv[]) { + GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); + butil::EndPoint endpoint; + if (FLAGS_listen_addr.compare(0, 10, "127.0.0.1:") != 0 || + butil::str2endpoint(FLAGS_listen_addr.c_str(), &endpoint) != 0 || + FLAGS_duration_s < 1 || FLAGS_duration_s > 3600 || + FLAGS_max_concurrency < 1 || FLAGS_max_concurrency > 64) { + std::cerr << "Use --listen_addr=127.0.0.1:PORT, --duration_s=1..3600 " + "and --max_concurrency=1..64\n"; + return 2; + } + // brpc defaults SIGTERM to immediate termination. This example always + // enables graceful shutdown, including the smoke's owned-process cleanup. + if (GFLAGS_NAMESPACE::SetCommandLineOption( + "graceful_quit_on_sigterm", "true").empty()) { + std::cerr << "Cannot enable graceful SIGTERM handling\n"; + return 1; + } + BenchmarkServiceImpl service; + brpc::Server server; + if (server.AddFlatBuffersService(&service, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) { + std::cerr << "Cannot register FlatBuffers service\n"; + return 1; + } + brpc::ServerOptions options; + options.has_builtin_services = false; + options.enabled_protocols = "fb_rpc"; + options.max_concurrency = FLAGS_max_concurrency; + if (server.Start(endpoint, &options) != 0) { + std::cerr << "Cannot start FlatBuffers server\n"; + return 1; + } + // Install brpc's termination handlers before publishing readiness. + brpc::IsAskedToQuit(); + std::cout << "BRPC_FB_READY " << server.listen_address() << std::endl; + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::seconds(FLAGS_duration_s); + while (!brpc::IsAskedToQuit() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + const int stop_result = server.Stop(0); + const int join_result = server.Join(); + return stop_result == 0 && join_result == 0 ? 0 : 1; +} diff --git a/example/benchmark_fb/smoke.py b/example/benchmark_fb/smoke.py new file mode 100644 index 0000000000..5b9c621cfc --- /dev/null +++ b/example/benchmark_fb/smoke.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +"""Bounded functional smoke: no network dependency and no throughput measurement.""" + +import argparse +import json +import os +from pathlib import Path +import re +import selectors +import signal +import subprocess +import sys +import tempfile +import time + + +def remaining(deadline): + seconds = deadline - time.monotonic() + if seconds <= 0: + raise RuntimeError("smoke deadline exceeded") + return seconds + + +def wait_ready(process, deadline): + buffer = b"" + os.set_blocking(process.stdout.fileno(), False) + with selectors.DefaultSelector() as selector: + selector.register(process.stdout, selectors.EVENT_READ) + while True: + if process.poll() is not None: + raise RuntimeError("server exited before readiness") + events = selector.select(min(0.2, remaining(deadline))) + if not events: + continue + chunk = os.read(process.stdout.fileno(), 4096) + if not chunk: + raise RuntimeError("server closed stdout before readiness") + buffer += chunk + if len(buffer) > 65536: + raise RuntimeError("unexpectedly large server readiness output") + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + match = re.fullmatch(rb"BRPC_FB_READY (127\.0\.0\.1):([0-9]{1,5})", line) + if match: + port = int(match.group(2)) + if not 1 <= port <= 65535: + raise RuntimeError("server published an invalid port") + return "127.0.0.1:" + str(port) + + +def run_client(binary, endpoint, deadline, *, count, size, attachment, + threads=1, omit=False, corrupt=False, connection="single"): + command = [ + str(binary), "--server=" + endpoint, + "--request_count=" + str(count), "--thread_num=" + str(threads), + "--request_size=" + str(size), "--attachment_size=" + str(attachment), + "--connection_type=" + connection, + "--omit_message=" + str(omit).lower(), + "--corrupt_request=" + str(corrupt).lower(), + "--timeout_ms=1000", "--deadline_ms=5000", + ] + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, timeout=min(8.0, remaining(deadline)), check=False) + if result.returncode != 0: + raise RuntimeError("client failed ({}):\n{}\n{}".format( + result.returncode, result.stdout, result.stderr)) + try: + report = json.loads(result.stdout) + except (ValueError, TypeError) as error: + raise RuntimeError("invalid client summary: " + result.stdout) from error + expected = {"completed": count, "successes": 0 if corrupt else count, + "expected_rejections": count if corrupt else 0, "failures": 0} + if report != expected: + raise RuntimeError("unexpected client result: " + repr(report)) + + +def stop_owned_server(process): + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=3) + raise RuntimeError("server required SIGKILL instead of a clean Stop/Join") + if process.returncode != 0: + raise RuntimeError("server exited with status " + str(process.returncode)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--server", required=True, help="benchmark_fb_server executable") + parser.add_argument("--client", required=True, help="benchmark_fb_client executable") + args = parser.parse_args() + process = None + failure = None + deadline = time.monotonic() + 35 + with tempfile.TemporaryFile(mode="w+b") as server_log: + try: + server = Path(args.server).resolve(strict=True) + client = Path(args.client).resolve(strict=True) + process = subprocess.Popen( + [str(server), "--listen_addr=127.0.0.1:0", "--duration_s=45", + "--max_concurrency=8"], + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=server_log, + start_new_session=True, + ) + endpoint = wait_ready(process, min(deadline, time.monotonic() + 10)) + run_client(client, endpoint, deadline, + count=6, size=8193, attachment=257, threads=2) + run_client(client, endpoint, deadline, + count=2, size=0, attachment=31, connection="pooled") + run_client(client, endpoint, deadline, + count=2, size=0, attachment=0, omit=True, connection="short") + run_client(client, endpoint, deadline, + count=2, size=17, attachment=5, corrupt=True) + # Keep the same server alive after the malformed requests. + run_client(client, endpoint, deadline, count=3, size=33, attachment=9) + if process.poll() is not None: + raise RuntimeError("server exited during the smoke") + except (Exception, KeyboardInterrupt) as error: + failure = str(error) or type(error).__name__ + finally: + if process is not None: + try: + stop_owned_server(process) + except Exception as error: + failure = failure or str(error) + if process.stdout is not None: + process.stdout.close() + if failure: + server_log.seek(0) + log = server_log.read(65536).decode("utf-8", errors="replace") + sys.stderr.write("benchmark_fb smoke failed: " + failure + "\n" + log) + if failure: + return 1 + print("benchmark_fb smoke passed: 13 verified replies, 2 schema rejections, clean shutdown") + return 0 + + +def handle_termination(signum, frame): + raise KeyboardInterrupt() + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, handle_termination) + sys.exit(main()) diff --git a/src/brpc/channel.cpp b/src/brpc/channel.cpp index de0a1bde49..ee5aa64175 100644 --- a/src/brpc/channel.cpp +++ b/src/brpc/channel.cpp @@ -40,6 +40,9 @@ #include "brpc/transport_factory.h" #include "brpc/details/controller_private_accessor.h" #include "brpc/details/ssl_helper.h" +#if BRPC_WITH_FLATBUFFERS +#include "brpc/flatbuffers/message.h" +#endif namespace brpc { @@ -484,6 +487,29 @@ void Channel::CallMethod(const google::protobuf::MethodDescriptor* method, const google::protobuf::Message* request, google::protobuf::Message* response, google::protobuf::Closure* done) { +#if BRPC_WITH_FLATBUFFERS + static_cast(controller_base)->_flatbuffers_method = nullptr; +#endif + CallMethodInternal(method, controller_base, request, response, done, false); +} + +#if BRPC_WITH_FLATBUFFERS +void Channel::FBCallMethod(const flatbuffers::MethodDescriptor* method, + google::protobuf::RpcController* controller_base, + const flatbuffers::Message* request, + flatbuffers::Message* response, + google::protobuf::Closure* done) { + static_cast(controller_base)->_flatbuffers_method = method; + CallMethodInternal(nullptr, controller_base, request, response, done, true); +} +#endif + +void Channel::CallMethodInternal( + const google::protobuf::MethodDescriptor* method, + google::protobuf::RpcController* controller_base, + const google::protobuf::Message* request, + google::protobuf::Message* response, + google::protobuf::Closure* done, bool is_fb) { const int64_t start_send_real_us = butil::gettimeofday_us(); Controller* cntl = static_cast(controller_base); cntl->OnRPCBegin(start_send_real_us); @@ -538,12 +564,17 @@ void Channel::CallMethod(const google::protobuf::MethodDescriptor* method, return; } cntl->set_used_by_rpc(); + const bool is_fb_protocol = (_options.protocol == PROTOCOL_FLATBUFFERS_RPC); if (cntl->_sender == nullptr && IsTraceable(Span::tls_parent().get())) { const int64_t start_send_us = butil::cpuwide_time_us(); std::string method_name; - if (_get_method_name) { + if (_get_method_name && is_fb == is_fb_protocol) { method_name = butil::EnsureString(_get_method_name(method, cntl)); +#if BRPC_WITH_FLATBUFFERS + } else if (is_fb && cntl->_flatbuffers_method) { + method_name = cntl->_flatbuffers_method->full_name(); +#endif } else if (method) { method_name = butil::EnsureString(method->full_name()); } else { @@ -591,6 +622,33 @@ void Channel::CallMethod(const google::protobuf::MethodDescriptor* method, // Share the lb with controller. cntl->_lb = _lb; + const char* entry_error = nullptr; + if (is_fb != is_fb_protocol) { + entry_error = is_fb ? "FlatBuffers calls require the fb_rpc protocol" : + "The fb_rpc protocol requires FBCallMethod"; + } +#if BRPC_WITH_FLATBUFFERS + else if (is_fb) { + if (!cntl->_flatbuffers_method || !request || !response) { + entry_error = "FlatBuffers method, request and response " + "must not be null"; + } else if (_options.auth != nullptr) { + // An authenticated shared socket may skip the packer's auth check. + entry_error = "The fb_rpc protocol does not support authentication"; + } else if (!cntl->_request_streams.empty() || + !cntl->_response_streams.empty()) { + entry_error = "The fb_rpc protocol does not support streams"; + } + } +#endif + if (entry_error) { + // A custom retry policy must not pack an unserialized request. Keep + // the normal completion path; callbacks may delete the controller. + cntl->set_max_retry(0); + cntl->SetFailed(EINVAL, "%s", entry_error); + return cntl->HandleSendFailed(); + } + // Ensure that serialize_request is done before pack_request in all // possible executions, including: // HandleSendFailed => OnVersionedRPCReturned => IssueRPC(pack_request) diff --git a/src/brpc/channel.h b/src/brpc/channel.h index a778a1bd2a..ce490e203f 100644 --- a/src/brpc/channel.h +++ b/src/brpc/channel.h @@ -23,6 +23,7 @@ // on internal structures, use opaque pointers instead. #include // std::ostream +#include "butil/config.h" #include "bthread/errno.h" // Redefine errno #include "butil/intrusive_ptr.hpp" // butil::intrusive_ptr #include "butil/ptr_container.h" @@ -38,6 +39,9 @@ #include "brpc/naming_service_filter.h" #include "brpc/health_check_option.h" #include "brpc/socket_mode.h" +#if BRPC_WITH_FLATBUFFERS +#include "brpc/flatbuffers/service.h" +#endif namespace brpc { @@ -176,7 +180,11 @@ struct ChannelOptions { // channel.Init("bns://rdev.matrix.all", "rr", nullptr/*default options*/); // MyService_Stub stub(&channel); // stub.MyMethod(&controller, &request, &response, nullptr); -class Channel : public ChannelBase { +class Channel : public ChannelBase +#if BRPC_WITH_FLATBUFFERS + , public flatbuffers::RpcChannel +#endif +{ friend class Controller; friend class SelectiveChannel; public: @@ -228,6 +236,15 @@ friend class SelectiveChannel; google::protobuf::Message* response, google::protobuf::Closure* done); +#if BRPC_WITH_FLATBUFFERS + // The descriptor is borrowed and must remain valid until the RPC finishes. + void FBCallMethod(const flatbuffers::MethodDescriptor* method, + google::protobuf::RpcController* controller, + const flatbuffers::Message* request, + flatbuffers::Message* response, + google::protobuf::Closure* done) override; +#endif + // Get current options. const ChannelOptions& options() const { return _options; } @@ -238,6 +255,13 @@ friend class SelectiveChannel; int CheckHealth(); +private: + void CallMethodInternal(const google::protobuf::MethodDescriptor* method, + google::protobuf::RpcController* controller, + const google::protobuf::Message* request, + google::protobuf::Message* response, + google::protobuf::Closure* done, bool is_fb); + protected: bool SingleServer() const { return _lb.get() == nullptr; } diff --git a/src/brpc/controller.cpp b/src/brpc/controller.cpp index 4fff9fd2f4..ce5c8e933d 100644 --- a/src/brpc/controller.cpp +++ b/src/brpc/controller.cpp @@ -510,6 +510,9 @@ void Controller::ResetPods() { _accessed = nullptr; _pack_request = nullptr; _method = nullptr; +#if BRPC_WITH_FLATBUFFERS + _flatbuffers_method = nullptr; +#endif _auth = nullptr; _idl_names = idl_single_req_single_res; _idl_result = IDL_VOID_RESULT; diff --git a/src/brpc/controller.h b/src/brpc/controller.h index c05dbb75a7..25ebcb0999 100644 --- a/src/brpc/controller.h +++ b/src/brpc/controller.h @@ -26,6 +26,7 @@ #include // Users often need gflags #include #include +#include "butil/config.h" #include "butil/intrusive_ptr.hpp" // butil::intrusive_ptr #include "bthread/errno.h" // Redefine errno #include "butil/endpoint.h" // butil::EndPoint @@ -64,6 +65,11 @@ struct x509_st; } namespace brpc { +#if BRPC_WITH_FLATBUFFERS +namespace flatbuffers { +class MethodDescriptor; +} +#endif class Span; class Server; class SharedLoadBalancer; @@ -336,6 +342,12 @@ friend void policy::ProcessThriftRequest(InputMessageBase*); ConnectionType connection_type() const { return _connection_type; } // Get the called method. May-be nullptr for non-pb services. const google::protobuf::MethodDescriptor* method() const { return _method; } +#if BRPC_WITH_FLATBUFFERS + // Borrowed descriptor. The caller must keep it alive until the RPC ends. + const flatbuffers::MethodDescriptor* flatbuffers_method() const { + return _flatbuffers_method; + } +#endif // Get the controllers for accessing sub channels in combo channels. // Ordinary channel: @@ -961,6 +973,9 @@ friend void policy::ProcessThriftRequest(InputMessageBase*); // Fields will be used when making requests Protocol::PackRequest _pack_request; const google::protobuf::MethodDescriptor* _method; +#if BRPC_WITH_FLATBUFFERS + const flatbuffers::MethodDescriptor* _flatbuffers_method; +#endif const Authenticator* _auth; butil::IOBuf _request_buf; IdlNames _idl_names; diff --git a/src/brpc/details/controller_private_accessor.h b/src/brpc/details/controller_private_accessor.h index 1aad5b2b4e..792e4e9057 100644 --- a/src/brpc/details/controller_private_accessor.h +++ b/src/brpc/details/controller_private_accessor.h @@ -129,6 +129,11 @@ class ControllerPrivateAccessor { void set_method(const google::protobuf::MethodDescriptor* method) { _cntl->_method = method; } +#if BRPC_WITH_FLATBUFFERS + void set_flatbuffers_method(const flatbuffers::MethodDescriptor* method) + { _cntl->_flatbuffers_method = method; } +#endif + void set_readable_progressive_attachment(ReadableProgressiveAttachment* s) { _cntl->_rpa.reset(s); } diff --git a/src/brpc/details/server_private_accessor.h b/src/brpc/details/server_private_accessor.h index 0e6e4fbba8..90448a2db5 100644 --- a/src/brpc/details/server_private_accessor.h +++ b/src/brpc/details/server_private_accessor.h @@ -76,6 +76,15 @@ class ServerPrivateAccessor { return _server->FindMethodPropertyByNameAndIndex(service_name, method_index); } +#if BRPC_WITH_FLATBUFFERS + const Server::FlatBuffersMethodProperty* + FindFlatBuffersMethodPropertyByIndex(uint32_t service_index, + int32_t method_index) const { + return _server->FindFlatBuffersMethodPropertyByIndex( + service_index, method_index); + } +#endif + const Server::ServiceProperty* FindServicePropertyByFullName(const butil::StringPiece& fullname) const { return _server->FindServicePropertyByFullName(fullname); diff --git a/src/brpc/flatbuffers/message.cpp b/src/brpc/flatbuffers/message.cpp new file mode 100644 index 0000000000..c9bc775707 --- /dev/null +++ b/src/brpc/flatbuffers/message.cpp @@ -0,0 +1,304 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "brpc/flatbuffers/message.h" + +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include "butil/logging.h" + +namespace brpc { +namespace flatbuffers { +namespace { + +void CheckOrAbort(bool condition, const char* error) { + if (BAIDU_UNLIKELY(!condition)) { + // brpc's crash_on_fatal_log flag is false by default. A CHECK alone + // cannot enforce FlatBuffers' non-null allocator contract. + LOG(ERROR) << error; + std::abort(); + } +} + +uint8_t* AlignPayload(uint8_t* data) { + const uintptr_t address = reinterpret_cast(data); + const size_t padding = (kBufferAlignment - address % kBufferAlignment) % + kBufferAlignment; + return data + padding; +} + +} // namespace + +SlabAllocator& SlabAllocator::operator=(SlabAllocator&& other) noexcept { + if (this != &other) { + SlabAllocator tmp(std::move(other)); + swap(tmp); + } + return *this; +} + +void SlabAllocator::swap(SlabAllocator& other) noexcept { + _iobuf.swap(other._iobuf); + std::swap(_data, other._data); + std::swap(_capacity, other._capacity); +} + +uint8_t* SlabAllocator::allocate(size_t size) { + CheckOrAbort(size > 0 && size < FLATBUFFERS_MAX_BUFFER_SIZE, + "Invalid FlatBuffers allocation size"); + CheckOrAbort(_data == nullptr, "Allocator already has a live buffer"); + butil::SingleIOBuf storage; + const uint32_t allocation_size = static_cast( + size + kDefaultMetaSize + kBufferAlignment - 1); + uint8_t* raw = static_cast(storage.allocate(allocation_size)); + CheckOrAbort(raw != nullptr, "Fail to allocate FlatBuffers storage"); + _data = AlignPayload(raw + kDefaultMetaSize); + _capacity = size; + _iobuf.swap(storage); + return _data; +} + +void SlabAllocator::deallocate(uint8_t* p, size_t /*size*/) { + if (!p) { + return; + } + CheckOrAbort(p == _data, "Invalid FlatBuffers deallocation pointer"); + _iobuf.reset(); + _data = nullptr; + _capacity = 0; +} + +uint8_t* SlabAllocator::reallocate_downward( + uint8_t* old_p, size_t old_size, size_t new_size, + size_t in_use_back, size_t in_use_front) { + CheckOrAbort(old_p != nullptr && old_p == _data && old_size == _capacity, + "Invalid FlatBuffers reallocation buffer"); + CheckOrAbort(new_size > old_size, "FlatBuffers reallocation must grow"); + CheckOrAbort(in_use_back <= old_size && in_use_front <= old_size - in_use_back, + "Invalid FlatBuffers scratch or payload size"); + // Keep the old allocation alive until BOTH data and scratch are copied. + SlabAllocator replacement; + uint8_t* data = replacement.allocate(new_size); + if (in_use_back) { + memcpy(data + new_size - in_use_back, + old_p + old_size - in_use_back, in_use_back); + } + if (in_use_front) { + memcpy(data, old_p, in_use_front); + } + swap(replacement); + return _data; +} + +Message::Message(const butil::IOBuf::BlockRef& ref, uint32_t meta_size, + uint32_t msg_size) + : _iobuf(ref), _meta_size(meta_size), _msg_size(msg_size) {} + +Message& Message::operator=(Message&& other) noexcept { + if (this != &other) { + Clear(); + Swap(other); + } + return *this; +} + +void Message::Swap(Message& other) noexcept { + _iobuf.swap(other._iobuf); + std::swap(_meta_size, other._meta_size); + std::swap(_msg_size, other._msg_size); +} + +void Message::MergeFrom(const Message& /*other*/) { + CheckOrAbort(false, "FlatBuffers Message is move-only; use move assignment"); +} + +void Message::Clear() { + _iobuf.reset(); + _meta_size = 0; + _msg_size = 0; +} + +const uint8_t* Message::data() const { + const uint8_t* raw = static_cast(_iobuf.get_begin()); + return raw ? raw + _meta_size : nullptr; +} + +bool Message::parse_msg_from_iobuf(const butil::IOBuf& buf, size_t msg_size, + size_t meta_size) { + const size_t total = buf.size(); + // Subtraction avoids overflow, and the limits cover SingleIOBuf's uint32_t + // sizes, including allocation overhead, before any narrowing conversion. + if (msg_size == 0 || total >= FLATBUFFERS_MAX_BUFFER_SIZE || + meta_size > total || msg_size != total - meta_size) { + return false; + } + butil::SingleIOBuf storage; + const butil::StringPiece first = buf.backing_block(0); + if (first.size() == total && + reinterpret_cast(first.data() + meta_size) % + kBufferAlignment == 0) { + if (!storage.assign(buf, static_cast(total))) { + return false; + } + } else { + uint8_t* raw = static_cast(storage.allocate( + static_cast(total + kBufferAlignment - 1))); + if (!raw) { + return false; + } + uint8_t* begin = AlignPayload(raw + meta_size) - meta_size; + buf.copy_to(begin, total); + const butil::IOBuf::BlockRef& ref = storage.get_cur_ref(); + butil::IOBuf::BlockRef aligned_ref = { + ref.offset + static_cast(begin - raw), + static_cast(total), ref.block}; + butil::SingleIOBuf aligned(aligned_ref); + storage.swap(aligned); + } + _iobuf.swap(storage); + _meta_size = static_cast(meta_size); + _msg_size = static_cast(msg_size); + return true; +} + +bool Message::append_msg_to_iobuf(butil::IOBuf& buf) const { + if (!data() || !_msg_size) { + return false; + } + _iobuf.append_to(&buf); + return true; +} + +void* Message::reduce_meta_size_and_get_buf(uint32_t new_size) { + if (!data() || new_size > _meta_size) { + errno = EINVAL; + return nullptr; + } + if (new_size != _meta_size) { + const uint32_t offset = _meta_size - new_size; + const butil::IOBuf::BlockRef& ref = _iobuf.get_cur_ref(); + butil::IOBuf::BlockRef sub_ref = { + ref.offset + offset, ref.length - offset, ref.block}; + butil::SingleIOBuf slice(sub_ref); + _iobuf.swap(slice); + _meta_size = new_size; + } + return mutable_buf_begin(); +} + +MessageBuilder::MessageBuilder(size_t initial_size) + : ::flatbuffers::FlatBufferBuilder( + initial_size, &slab_allocator_, false, kBufferAlignment) { + CheckOrAbort(initial_size > 0 && initial_size < FLATBUFFERS_MAX_BUFFER_SIZE, + "Invalid FlatBuffers initial size"); +} + +MessageBuilder::MessageBuilder(MessageBuilder&& other) : MessageBuilder() { + Swap(other); +} + +MessageBuilder& MessageBuilder::operator=(MessageBuilder&& other) { + if (this != &other) { + MessageBuilder tmp(std::move(other)); + Swap(tmp); + } + return *this; +} + +void MessageBuilder::ClearStringPool() { + // FlatBuffers' shared-string comparator stores a vector_downward pointer. + // Discard only this optional cache when moving between vector objects. + delete string_pool; + string_pool = nullptr; +} + +void MessageBuilder::Swap(MessageBuilder& other) { + if (this == &other) { + return; + } + ClearStringPool(); + other.ClearStringPool(); + slab_allocator_.swap(other.slab_allocator_); + ::flatbuffers::FlatBufferBuilder::Swap(other); + // Each builder must continue to refer to its OWN allocator member. + buf_.swap_allocator(other.buf_); +} + +MessageBuilder::MessageBuilder(::flatbuffers::FlatBufferBuilder&& src) + : ::flatbuffers::FlatBufferBuilder(std::move(src)) { + ClearStringPool(); + CheckOrAbort(minalign_ <= kBufferAlignment, + "Unsupported FlatBuffers alignment"); + decltype(buf_) replacement( + buf_.capacity() ? buf_.capacity() : 1024, + &slab_allocator_, false, kBufferAlignment); + if (buf_.capacity()) { + replacement.push(buf_.data(), buf_.size()); + for (::flatbuffers::uoffset_t i = 0; i < buf_.scratch_size(); ++i) { + replacement.scratch_push_small(buf_.scratch_data()[i]); + } + } + // The temporary returns the original buffer to its actual allocator. + buf_.swap(replacement); +} + +MessageBuilder& MessageBuilder::operator=(::flatbuffers::FlatBufferBuilder&& src) { + if (static_cast<::flatbuffers::FlatBufferBuilder*>(this) != &src) { + MessageBuilder tmp(std::move(src)); + Swap(tmp); + } + return *this; +} + +Message MessageBuilder::ReleaseMessage() { + CheckOrAbort(finished && buf_.size() > 0, + "Finish the FlatBuffer before releasing a message"); + const uint32_t msg_size = static_cast(buf_.size()); + const uint8_t* msg_data = buf_.data(); + const butil::SingleIOBuf& storage = slab_allocator_._iobuf; + const uint8_t* raw = static_cast(storage.get_begin()); + CheckOrAbort(raw != nullptr, "Missing FlatBuffers storage"); + CheckOrAbort(msg_data >= raw + kDefaultMetaSize, + "Missing FlatBuffers metadata prefix"); + const size_t begin = msg_data - raw - kDefaultMetaSize; + CheckOrAbort(begin + kDefaultMetaSize + msg_size <= storage.get_length(), + "FlatBuffers message exceeds storage"); + const butil::IOBuf::BlockRef& ref = storage.get_cur_ref(); + const butil::IOBuf::BlockRef sub_ref = { + ref.offset + static_cast(begin), + kDefaultMetaSize + msg_size, ref.block}; + Message msg(sub_ref, kDefaultMetaSize, msg_size); + // Never expose stale scratch or allocator bytes in the reserved prefix. + memset(msg.mutable_buf_begin(), 0, kDefaultMetaSize); + Reset(); + return msg; +} + +bool ParseFbFromIOBUF(Message* msg, size_t msg_size, const butil::IOBuf& buf, + size_t meta_size) { + return msg && msg->parse_msg_from_iobuf(buf, msg_size, meta_size); +} + +bool SerializeFbToIOBUF(const Message* msg, butil::IOBuf& buf) { + return msg && msg->append_msg_to_iobuf(buf); +} + +} // namespace flatbuffers +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS diff --git a/src/brpc/flatbuffers/message.h b/src/brpc/flatbuffers/message.h new file mode 100644 index 0000000000..7304118747 --- /dev/null +++ b/src/brpc/flatbuffers/message.h @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#ifndef BRPC_FLATBUFFERS_MESSAGE_H +#define BRPC_FLATBUFFERS_MESSAGE_H + +#include "butil/config.h" + +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include +#include "butil/single_iobuf.h" +#include "brpc/nonreflectable_message.h" + +namespace brpc { +namespace flatbuffers { + +// Space preceding a payload, available to a future RPC transport. +constexpr uint32_t kDefaultMetaSize = 64; +// IOBuf slices need not start at an aligned address. The allocator corrects it. +constexpr size_t kBufferAlignment = 64; + +class MessageBuilder; + +// FlatBufferBuilder cannot recover from a null allocation. Allocation failure +// and sizes outside FlatBuffers' offset range are fatal, even in release builds. +class SlabAllocator : public ::flatbuffers::Allocator { +public: + SlabAllocator() : _data(nullptr), _capacity(0) {} + SlabAllocator(const SlabAllocator&) = delete; + SlabAllocator& operator=(const SlabAllocator&) = delete; + SlabAllocator(SlabAllocator&& other) noexcept : SlabAllocator() { + swap(other); + } + SlabAllocator& operator=(SlabAllocator&& other) noexcept; + + uint8_t* allocate(size_t size) override; + void deallocate(uint8_t* p, size_t size) override; + uint8_t* reallocate_downward(uint8_t* old_p, size_t old_size, + size_t new_size, size_t in_use_back, + size_t in_use_front) override; + void swap(SlabAllocator& other) noexcept; + +private: + butil::SingleIOBuf _iobuf; + uint8_t* _data; + size_t _capacity; + friend class MessageBuilder; +}; + +// Construct the allocator before the FlatBufferBuilder base uses it, and +// destroy it after that base has returned its buffer. +struct SlabAllocatorMember { + SlabAllocator slab_allocator_; +}; + +// A move-only message. Parsing checks framing, not the schema: Verify() +// must succeed before reading data received from an untrusted peer. +class Message : public NonreflectableMessage { +public: + Message() : _meta_size(0), _msg_size(0) {} + Message(const Message&) = delete; + Message& operator=(const Message&) = delete; + Message(Message&& other) noexcept : Message() { Swap(other); } + Message& operator=(Message&& other) noexcept; + + void MergeFrom(const Message&) override; + void Clear() override; + void Swap(Message& other) noexcept; + + const uint8_t* data() const; + void* mutable_data() { return const_cast(data()); } + void* mutable_buf_begin() { + return const_cast(_iobuf.get_begin()); + } + void* reduce_meta_size_and_get_buf(uint32_t new_size); + uint32_t get_meta_size() const { return _meta_size; } + size_t size() const { return _msg_size; } + + template + bool Verify() const { + if (!data() || size() < sizeof(::flatbuffers::uoffset_t) || + size() >= FLATBUFFERS_MAX_BUFFER_SIZE) { + return false; + } + ::flatbuffers::Verifier verifier(data(), size()); + return verifier.VerifyBuffer(nullptr); + } + + // These accessors require a schema-verified buffer. + template const T* GetRoot() const { + return data() ? ::flatbuffers::GetRoot(data()) : nullptr; + } + template T* GetMutableRoot() { + return data() ? ::flatbuffers::GetMutableRoot(mutable_data()) : nullptr; + } + + // Failure leaves the old message unchanged. A fragmented or unaligned + // payload is copied into aligned storage; aligned contiguous input is shared. + bool parse_msg_from_iobuf(const butil::IOBuf& buf, size_t msg_size, + size_t meta_size); + bool append_msg_to_iobuf(butil::IOBuf& buf) const; + +private: + Message(const butil::IOBuf::BlockRef& ref, uint32_t meta_size, + uint32_t msg_size); + butil::SingleIOBuf _iobuf; + uint32_t _meta_size; + uint32_t _msg_size; + friend class MessageBuilder; +}; + +class MessageBuilder : private SlabAllocatorMember, + public ::flatbuffers::FlatBufferBuilder { +public: + explicit MessageBuilder(size_t initial_size = 1024); + MessageBuilder(const MessageBuilder&) = delete; + MessageBuilder& operator=(const MessageBuilder&) = delete; + MessageBuilder(MessageBuilder&& other); + MessageBuilder& operator=(MessageBuilder&& other); + + // Importing a foreign builder copies its payload and scratch data, retaining + // build state. Its original allocator frees the source allocation correctly. + // In particular, no free()/delete[] guess or spare-prefix assumption is made. + explicit MessageBuilder(::flatbuffers::FlatBufferBuilder&& src); + MessageBuilder& operator=(::flatbuffers::FlatBufferBuilder&& src); + + void Swap(MessageBuilder& other); + // Requires Finish(). The returned message shares storage without copying; + // both this builder and a moved-from builder may immediately be reused. + Message ReleaseMessage(); + +private: + void ClearStringPool(); + // The inherited release methods would retain a pointer to our member + // allocator after this builder dies. Only ReleaseMessage is supported. + using ::flatbuffers::FlatBufferBuilder::Release; + using ::flatbuffers::FlatBufferBuilder::ReleaseRaw; + void ReleaseBufferPointer() = delete; + using ::flatbuffers::FlatBufferBuilder::SwapBufAllocator; +}; + +bool ParseFbFromIOBUF(Message* msg, size_t msg_size, const butil::IOBuf& buf, + size_t meta_size = 0); +bool SerializeFbToIOBUF(const Message* msg, butil::IOBuf& buf); + +} // namespace flatbuffers +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS +#endif // BRPC_FLATBUFFERS_MESSAGE_H diff --git a/src/brpc/flatbuffers/service.cpp b/src/brpc/flatbuffers/service.cpp new file mode 100644 index 0000000000..8bd1a9c2ce --- /dev/null +++ b/src/brpc/flatbuffers/service.cpp @@ -0,0 +1,158 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "brpc/flatbuffers/service.h" + +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include +#include "butil/third_party/murmurhash3/murmurhash3.h" + +namespace brpc { +namespace flatbuffers { +namespace { + +bool IsIdentifier(const std::string& name) { + if (name.empty()) { + return false; + } + for (size_t i = 0; i < name.size(); ++i) { + const char c = name[i]; + if (c != '_' && !(c >= 'a' && c <= 'z') && + !(c >= 'A' && c <= 'Z') && + !(i != 0 && c >= '0' && c <= '9')) { + return false; + } + } + return true; +} + +bool IsNamespace(const std::string& name) { + if (name.empty()) { + return true; + } + size_t begin = 0; + do { + const size_t end = name.find('.', begin); + if (!IsIdentifier(name.substr(begin, end - begin))) { + return false; + } + if (end == std::string::npos) { + return true; + } + begin = end + 1; + } while (begin < name.size()); + return false; +} + +} // namespace + +int ServiceDescriptor::init(const BrpcDescriptorTable& table) { + if (!_methods.empty()) { + errno = EALREADY; + return -1; + } + std::string prefix = table.prefix; + if (!prefix.empty() && prefix.back() == '.') { + prefix.pop_back(); + if (prefix.empty()) { + errno = EINVAL; + return -1; + } + } + if (!IsIdentifier(table.service_name) || !IsNamespace(prefix)) { + errno = EINVAL; + return -1; + } + std::istringstream input(table.method_name_list); + std::vector names; + std::set unique_names; + std::string name; + while (input >> name) { + if (!IsIdentifier(name) || !unique_names.insert(name).second) { + errno = EINVAL; + return -1; + } + names.push_back(name); + } + if (names.empty() || names.size() > static_cast( + std::numeric_limits::max()) || + (!table.method_ids.empty() && table.method_ids.size() != names.size())) { + errno = EINVAL; + return -1; + } + std::set unique_ids; + for (int id : table.method_ids) { + if (id < 0 || !unique_ids.insert(id).second) { + errno = EINVAL; + return -1; + } + } + _name = table.service_name; + _full_name = prefix.empty() ? _name : prefix + "." + _name; + butil::MurmurHash3_x86_32(_full_name.data(), _full_name.size(), 1, &_index); + _methods.reserve(names.size()); + for (size_t i = 0; i < names.size(); ++i) { + const int id = table.method_ids.empty() ? static_cast(i) + : table.method_ids[i]; + _methods.emplace_back(new MethodDescriptor(names[i], this, id)); + } + return 0; +} + +const MethodDescriptor* ServiceDescriptor::method(int position) const { + if (position < 0 || static_cast(position) >= _methods.size()) { + errno = EINVAL; + return nullptr; + } + return _methods[position].get(); +} + +const MethodDescriptor* ServiceDescriptor::FindMethodByIndex(int id) const { + for (const auto& method : _methods) { + if (method->index() == id) { + return method.get(); + } + } + errno = EINVAL; + return nullptr; +} + +MethodDescriptor::MethodDescriptor(const std::string& name, + const ServiceDescriptor* service, int index) + : _name(name), _full_name(service->full_name() + "." + name), + _service(service), _index(index) {} + +int parse_service_descriptors(const BrpcDescriptorTable& table, + ServiceDescriptor** out) { + if (!out) { + errno = EINVAL; + return -1; + } + std::unique_ptr descriptor(new ServiceDescriptor); + if (descriptor->init(table) != 0) { + return -1; + } + *out = descriptor.release(); + return 0; +} + +} // namespace flatbuffers +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS diff --git a/src/brpc/flatbuffers/service.h b/src/brpc/flatbuffers/service.h new file mode 100644 index 0000000000..4423170be9 --- /dev/null +++ b/src/brpc/flatbuffers/service.h @@ -0,0 +1,127 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#ifndef BRPC_FLATBUFFERS_SERVICE_H +#define BRPC_FLATBUFFERS_SERVICE_H + +#include "butil/config.h" + +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include + +namespace google { +namespace protobuf { +class Closure; +class RpcController; +} // namespace protobuf +} // namespace google + +namespace brpc { +namespace flatbuffers { +class Message; +class ServiceDescriptor; + +struct BrpcDescriptorTable { + // A namespace, optionally followed by one period. Empty means global scope. + std::string prefix; + std::string service_name; + // Whitespace-separated method names in declaration order. + std::string method_name_list; + // Stable wire IDs, NOT array positions. Empty retains legacy ordinal IDs. + // New schemas should specify every ID; IDs must be nonnegative and unique. + std::vector method_ids; +}; + +class MethodDescriptor { +public: + MethodDescriptor(const std::string& name, const ServiceDescriptor* service, + int index); + const std::string& name() const { return _name; } + const std::string& full_name() const { return _full_name; } + int index() const { return _index; } + const ServiceDescriptor* service() const { return _service; } + +private: + std::string _name; + std::string _full_name; + const ServiceDescriptor* _service; + int _index; +}; + +// Immutable after successful initialization. Owns all method descriptors. +class ServiceDescriptor { +public: + ServiceDescriptor() : _index(0) {} + ServiceDescriptor(const ServiceDescriptor&) = delete; + ServiceDescriptor& operator=(const ServiceDescriptor&) = delete; + int init(const BrpcDescriptorTable& table); + const std::string& name() const { return _name; } + const std::string& full_name() const { return _full_name; } + // MurmurHash3 of the canonical fully-qualified service name, seed 1. + uint32_t index() const { return _index; } + int method_count() const { return static_cast(_methods.size()); } + // Dense declaration-order lookup, for enumeration and generated stubs. + const MethodDescriptor* method(int position) const; + // Sparse stable-ID lookup, for wire dispatch. Missing IDs return nullptr. + const MethodDescriptor* FindMethodByIndex(int id) const; + +private: + std::string _name; + std::string _full_name; + uint32_t _index; + std::vector > _methods; +}; + +// On success the caller owns *out; on failure *out is unchanged. +int parse_service_descriptors(const BrpcDescriptorTable& table, + ServiceDescriptor** out); + +class RpcChannel { +public: + RpcChannel() = default; + virtual ~RpcChannel() = default; + RpcChannel(const RpcChannel&) = delete; + RpcChannel& operator=(const RpcChannel&) = delete; + virtual void FBCallMethod(const MethodDescriptor* method, + google::protobuf::RpcController* controller, + const Message* request, Message* response, + google::protobuf::Closure* done) = 0; +}; + +class Service { +public: + Service() = default; + virtual ~Service() = default; + Service(const Service&) = delete; + Service& operator=(const Service&) = delete; + enum ChannelOwnership { STUB_OWNS_CHANNEL, STUB_DOESNT_OWN_CHANNEL }; + virtual const ServiceDescriptor* GetDescriptor() = 0; + // Implementations must validate the method and request, report errors on + // controller and run a non-null done exactly once, including failure paths. + virtual void FBCallMethod(const MethodDescriptor* method, + google::protobuf::RpcController* controller, + const Message* request, Message* response, + google::protobuf::Closure* done) = 0; +}; + +} // namespace flatbuffers +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS +#endif // BRPC_FLATBUFFERS_SERVICE_H diff --git a/src/brpc/global.cpp b/src/brpc/global.cpp index 0a0837e096..c63bd79b85 100644 --- a/src/brpc/global.cpp +++ b/src/brpc/global.cpp @@ -71,6 +71,9 @@ #include "brpc/protocol.h" #include "brpc/policy/rdma_handshake_protocol.h" #include "brpc/policy/baidu_rpc_protocol.h" +#if BRPC_WITH_FLATBUFFERS +#include "brpc/policy/flatbuffers_protocol.h" +#endif #include "brpc/policy/http_rpc_protocol.h" #include "brpc/policy/http2_rpc_protocol.h" #include "brpc/policy/hulu_pbrpc_protocol.h" @@ -456,6 +459,17 @@ static void GlobalInitializeOrDieImpl() { exit(1); } +#if BRPC_WITH_FLATBUFFERS + Protocol flatbuffers_protocol = { + ParseFlatBuffersMessage, SerializeFlatBuffersRequest, PackFlatBuffersRequest, + ProcessFlatBuffersRequest, ProcessFlatBuffersResponse, + VerifyFlatBuffersRequest, nullptr, GetFlatBuffersMethodName, + CONNECTION_TYPE_ALL, "fb_rpc" }; + if (RegisterProtocol(PROTOCOL_FLATBUFFERS_RPC, flatbuffers_protocol) != 0) { + exit(1); + } +#endif + Protocol streaming_protocol = { ParseStreamingMessage, nullptr, nullptr, ProcessStreamingMessage, ProcessStreamingMessage, diff --git a/src/brpc/options.proto b/src/brpc/options.proto index 13b8b682e6..23f28c0a9e 100644 --- a/src/brpc/options.proto +++ b/src/brpc/options.proto @@ -67,6 +67,7 @@ enum ProtocolType { PROTOCOL_H2 = 27; PROTOCOL_COUCHBASE = 28; PROTOCOL_MYSQL = 29; // Client side only + PROTOCOL_FLATBUFFERS_RPC = 30; } enum CompressType { diff --git a/src/brpc/policy/flatbuffers_protocol.cpp b/src/brpc/policy/flatbuffers_protocol.cpp new file mode 100644 index 0000000000..8ea06dcebc --- /dev/null +++ b/src/brpc/policy/flatbuffers_protocol.cpp @@ -0,0 +1,459 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "brpc/policy/flatbuffers_protocol.h" + +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include +#include "butil/logging.h" +#include "brpc/controller.h" +#include "brpc/server.h" +#include "brpc/socket.h" +#include "brpc/flatbuffers/message.h" +#include "brpc/flatbuffers/service.h" +#include "brpc/policy/most_common_message.h" +#include "brpc/details/controller_private_accessor.h" +#include "brpc/details/server_private_accessor.h" +#include "brpc/details/usercode_backup_pool.h" + +extern "C" void bthread_assign_data(void* data); + +namespace brpc { +namespace policy { +namespace { + +const size_t kHeaderSize = 12; +const size_t kRequestMetaSize = 24; +const size_t kResponseMetaSize = 20; + +// The header is big-endian; the fixed metadata prefix is little-endian, as in +// the experimental FRPC format on x86/ARM. meta_size permits appended fields. +// Bytewise access also handles fragmented or unaligned input without packed +// structs, aliasing violations or host-endian dependencies. +uint32_t Load32(const unsigned char* p, bool big_endian = false) { + uint32_t result = 0; + for (size_t i = 0; i < 4; ++i) { + result |= static_cast(p[i]) << (8 * (big_endian ? 3 - i : i)); + } + return result; +} + +uint64_t Load64(const unsigned char* p) { + return Load32(p) | (static_cast(Load32(p + 4)) << 32); +} + +void Store32(unsigned char* p, uint32_t value, bool big_endian = false) { + for (size_t i = 0; i < 4; ++i) { + p[i] = static_cast(value >> (8 * (big_endian ? 3 - i : i))); + } +} + +void Store64(unsigned char* p, uint64_t value) { + Store32(p, static_cast(value)); + Store32(p + 4, static_cast(value >> 32)); +} + +bool ValidSizes(size_t message_size, size_t attachment_size, size_t meta_size) { + const size_t limit = static_cast(std::numeric_limits::max()); + return message_size <= limit && attachment_size <= limit && + meta_size <= limit && message_size <= limit - meta_size && + attachment_size <= limit - meta_size - message_size && + meta_size + message_size + attachment_size <= + static_cast(FLAGS_max_body_size); +} + +void PackHeader(unsigned char* header, size_t meta_size, size_t payload_size) { + memcpy(header, "FRPC", 4); + Store32(header + 4, static_cast(meta_size + payload_size), true); + Store32(header + 8, static_cast(meta_size), true); +} + +bool SerializePayload(const flatbuffers::Message& message, butil::IOBuf* out) { + butil::IOBuf body; + if (!message.append_msg_to_iobuf(body)) { + return false; + } + body.pop_front(message.get_meta_size()); + if (body.size() != message.size()) { + return false; + } + out->append(body); + return true; +} + +bool ValidPayloadSizes(uint32_t message_size, uint32_t attachment_size, + size_t payload_size) { + const uint32_t limit = static_cast(std::numeric_limits::max()); + return message_size <= limit && attachment_size <= limit && + attachment_size <= payload_size && + message_size == payload_size - attachment_size; +} + +// One closure owns the request, response, controller and concurrency accounting +// until the application completes, including asynchronous service methods. +class FlatBuffersCall : public google::protobuf::Closure { +public: + FlatBuffersCall(uint64_t id, int64_t received_us) + : correlation_id(id), received_us(received_us), cntl(new Controller), + service(nullptr), method(nullptr), status(nullptr) {} + + ~FlatBuffersCall() override { + { + ConcurrencyRemover remover(status, cntl.get(), received_us); + } + cntl->CallAfterRpcResp(&request, &response); + } + + void Run() override { + std::unique_ptr self(this); + ControllerPrivateAccessor accessor(cntl.get()); + Socket* socket = accessor.get_sending_socket(); + if (cntl->IsCloseConnection()) { + socket->SetFailed(); + return; + } + + butil::IOBuf payload; + size_t attachment_size = 0; + if (!cntl->Failed()) { + if (cntl->response_compress_type() != COMPRESS_TYPE_NONE || + cntl->response_checksum_type() != CHECKSUM_TYPE_NONE) { + cntl->SetFailed(ERESPONSE, "FRPC does not support compression or checksums"); + } else if (!SerializePayload(response, &payload)) { + cntl->SetFailed(ERESPONSE, "Empty or invalid FlatBuffers response"); + } else { + attachment_size = cntl->response_attachment().size(); + if (!ValidSizes(payload.size(), attachment_size, kResponseMetaSize)) { + cntl->SetFailed(ERESPONSE, "FlatBuffers response is too large"); + } + } + } + if (cntl->Failed()) { + payload.clear(); + attachment_size = 0; + } + + unsigned char header[kHeaderSize + kResponseMetaSize] = {}; + PackHeader(header, kResponseMetaSize, payload.size() + attachment_size); + Store32(header + 12, static_cast(cntl->ErrorCode())); + Store32(header + 16, static_cast(payload.size())); + Store32(header + 20, static_cast(attachment_size)); + Store64(header + 24, correlation_id); + butil::IOBuf wire; + if (wire.append(header, sizeof(header)) != 0) { + socket->SetFailed(ENOMEM, "Fail to allocate FlatBuffers response header"); + return; + } + wire.append(payload); + if (attachment_size) { + wire.append(cntl->response_attachment()); + } + Socket::WriteOptions options; + options.ignore_eovercrowded = true; + if (socket->Write(&wire, &options) != 0) { + cntl->SetFailed(errno, "Fail to write FlatBuffers response"); + } + } + + static void Invoke(void* arg) { + FlatBuffersCall* call = static_cast(arg); + call->service->FBCallMethod(call->method, call->cntl.get(), + &call->request, &call->response, call); + } + + uint64_t correlation_id; + int64_t received_us; + std::unique_ptr cntl; + flatbuffers::Message request; + flatbuffers::Message response; + flatbuffers::Service* service; + const flatbuffers::MethodDescriptor* method; + MethodStatus* status; +}; + +} // namespace + +ParseResult ParseFlatBuffersMessage(butil::IOBuf* source, Socket*, bool, + const void*) { + unsigned char header[kHeaderSize] = {}; + const size_t n = source->copy_to(header, sizeof(header)); + if (memcmp(header, "FRPC", std::min(n, size_t(4))) != 0) { + return MakeParseError(PARSE_ERROR_TRY_OTHERS); + } + if (n < sizeof(header)) { + return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); + } + const uint32_t body_size = Load32(header + 4, true); + const uint32_t meta_size = Load32(header + 8, true); + if (body_size > static_cast(FLAGS_max_body_size)) { + return MakeParseError(PARSE_ERROR_TOO_BIG_DATA); + } + if (meta_size > body_size) { + return MakeParseError(PARSE_ERROR_ABSOLUTELY_WRONG, + "FRPC metadata exceeds frame body"); + } + if (source->size() - kHeaderSize < body_size) { + return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); + } + MostCommonMessage* message = MostCommonMessage::Get(); + if (!message) { + return MakeParseError(PARSE_ERROR_NO_RESOURCE); + } + source->pop_front(kHeaderSize); + source->cutn(&message->meta, meta_size); + source->cutn(&message->payload, body_size - meta_size); + return MakeMessage(message); +} + +bool VerifyFlatBuffersRequest(const InputMessageBase* message) { + const Server* server = static_cast(message->arg()); + // No credential format has been defined for FRPC. Never silently bypass + // authentication configured for the server's other protocols. + return server->options().auth == nullptr; +} + +void ProcessFlatBuffersRequest(InputMessageBase* message_base) { + DestroyingPtr message( + static_cast(message_base)); + SocketUniquePtr socket_guard(message->ReleaseSocket()); + Socket* socket = socket_guard.get(); + const Server* server = static_cast(message->arg()); + ScopedNonServiceError non_service_error(server); + unsigned char meta[kRequestMetaSize]; + if (message->meta.copy_to(meta, sizeof(meta)) != sizeof(meta)) { + socket->SetFailed(EREQUEST, "Truncated FlatBuffers request metadata"); + return; + } + const uint32_t service_id = Load32(meta); + const uint32_t method_id = Load32(meta + 4); + const uint32_t message_size = Load32(meta + 8); + const uint32_t attachment_size = Load32(meta + 12); + std::unique_ptr call( + new FlatBuffersCall(Load64(meta + 16), message->received_us())); + Controller* cntl = call->cntl.get(); + ControllerPrivateAccessor accessor(cntl); + ServerPrivateAccessor server_accessor(server); + const bool security_mode = server->options().security_mode() && + socket->user() == server_accessor.acceptor(); + cntl->set_rpc_received_us(message->received_us()); + accessor.set_server(server) + .set_security_mode(security_mode) + .set_peer_id(socket->id()) + .set_remote_side(socket->remote_side()) + .set_local_side(socket->local_side()) + .set_auth_context(socket->auth_context()) + .set_request_protocol(PROTOCOL_FLATBUFFERS_RPC) + .set_begin_time_us(message->received_us()) + .move_in_server_receiving_sock(socket_guard); + if (server->thread_local_options().thread_local_data_factory) { + bthread_assign_data((void*)&server->thread_local_options()); + } + + do { + if (server->options().auth) { + cntl->SetFailed(ERPCAUTH, "FRPC does not support authentication"); + break; + } + if (RejectNonBuiltinAccessFromInternalPort(cntl, *server)) { + break; + } + if (!server->IsRunning()) { + cntl->SetFailed(ELOGOFF, "Server is stopping"); + break; + } + if (!server_accessor.AddConcurrency(cntl)) { + cntl->SetFailed(ELIMIT, "Reached server's max_concurrency"); + break; + } + if (FLAGS_usercode_in_pthread && TooManyUserCode()) { + cntl->SetFailed(ELIMIT, "Too many user code tasks"); + break; + } + if (!message_size || !ValidPayloadSizes(message_size, attachment_size, + message->payload.size())) { + cntl->SetFailed(EREQUEST, "Invalid FlatBuffers request sizes"); + break; + } + if (method_id > static_cast(std::numeric_limits::max())) { + cntl->SetFailed(ENOMETHOD, "Invalid FlatBuffers method ID"); + break; + } + const Server::FlatBuffersMethodProperty* property = + server_accessor.FindFlatBuffersMethodPropertyByIndex(service_id, method_id); + if (!property) { + cntl->SetFailed(ENOMETHOD, "Unknown FlatBuffers service=%u method=%u", + service_id, method_id); + break; + } + call->service = property->service; + call->method = property->method; + accessor.set_flatbuffers_method(property->method); + if (socket->is_overcrowded() && !server->options().ignore_eovercrowded && + !property->ignore_eovercrowded) { + cntl->SetFailed(EOVERCROWDED, "FlatBuffers connection is overcrowded"); + break; + } + non_service_error.release(); + if (property->status) { + call->status = property->status; + if (!call->status->OnRequested(nullptr, cntl)) { + cntl->SetFailed(ELIMIT, "Reached method's max_concurrency"); + break; + } + } + if (!server->AcceptRequest(cntl)) { + break; + } + butil::IOBuf payload; + message->payload.cutn(&payload, message_size); + if (!call->request.parse_msg_from_iobuf(payload, message_size, 0)) { + cntl->SetFailed(EREQUEST, "Fail to parse FlatBuffers request"); + break; + } + cntl->request_attachment().swap(message->payload); + message.reset(); + if (FLAGS_usercode_in_pthread) { + RunUserCode(&FlatBuffersCall::Invoke, call.release()); + } else { + FlatBuffersCall::Invoke(call.release()); + } + return; + } while (false); + call.release()->Run(); +} + +void ProcessFlatBuffersResponse(InputMessageBase* message_base) { + DestroyingPtr message( + static_cast(message_base)); + unsigned char meta[kResponseMetaSize]; + if (message->meta.copy_to(meta, sizeof(meta)) != sizeof(meta)) { + if (message->socket()) { + message->socket()->SetFailed(ERESPONSE, "Truncated FlatBuffers response metadata"); + } + return; + } + const uint32_t error_code = Load32(meta); + const uint32_t message_size = Load32(meta + 4); + const uint32_t attachment_size = Load32(meta + 8); + const bthread_id_t correlation_id = {Load64(meta + 12)}; + Controller* cntl = nullptr; + const int rc = bthread_id_lock(correlation_id, reinterpret_cast(&cntl)); + if (rc != 0) { + return; + } + ControllerPrivateAccessor accessor(cntl); + const int saved_error = cntl->ErrorCode(); + cntl->set_rpc_received_us(message->received_us()); + do { + if (!ValidPayloadSizes(message_size, attachment_size, message->payload.size())) { + cntl->SetFailed(ERESPONSE, "Invalid FlatBuffers response sizes"); + break; + } + if (error_code) { + if (message_size || attachment_size) { + cntl->SetFailed(ERESPONSE, "FlatBuffers error response has a payload"); + break; + } + cntl->SetFailed(static_cast(error_code), "FlatBuffers server error"); + break; + } + if (!message_size) { + cntl->SetFailed(ERESPONSE, "Empty FlatBuffers response"); + break; + } + butil::IOBuf payload; + message->payload.cutn(&payload, message_size); + if (cntl->response()) { + if (cntl->response()->GetDescriptor() != flatbuffers::Message::descriptor()) { + cntl->SetFailed(ERESPONSE, "Response is not a FlatBuffers Message"); + break; + } + flatbuffers::Message response; + if (!response.parse_msg_from_iobuf(payload, message_size, 0)) { + cntl->SetFailed(ERESPONSE, "Fail to parse FlatBuffers response"); + break; + } + *static_cast(cntl->response()) = std::move(response); + } + cntl->response_attachment().swap(message->payload); + } while (false); + message.reset(); + accessor.OnResponse(correlation_id, saved_error); +} + +void SerializeFlatBuffersRequest(butil::IOBuf* buf, Controller* cntl, + const google::protobuf::Message* request) { + if (!request || request->GetDescriptor() != flatbuffers::Message::descriptor()) { + cntl->SetFailed(EREQUEST, "Request is not a FlatBuffers Message"); + return; + } + if (cntl->request_compress_type() != COMPRESS_TYPE_NONE || + cntl->request_checksum_type() != CHECKSUM_TYPE_NONE) { + cntl->SetFailed(EREQUEST, "FRPC does not support compression or checksums"); + return; + } + if (!SerializePayload(*static_cast(request), buf)) { + cntl->SetFailed(EREQUEST, "Empty or invalid FlatBuffers request"); + } +} + +void PackFlatBuffersRequest(butil::IOBuf* buf, SocketMessage**, + uint64_t correlation_id, + const google::protobuf::MethodDescriptor* method, + Controller* cntl, const butil::IOBuf& request, + const Authenticator* auth) { + const flatbuffers::MethodDescriptor* fb_method = cntl->flatbuffers_method(); + if (method || !fb_method || fb_method->index() < 0) { + cntl->SetFailed(ENOMETHOD, "Use Channel::FBCallMethod with a FlatBuffers method"); + return; + } + if (auth) { + cntl->SetFailed(EREQUEST, "FRPC does not support authentication"); + return; + } + const size_t attachment_size = cntl->request_attachment().size(); + if (!request.size() || !ValidSizes(request.size(), attachment_size, kRequestMetaSize)) { + cntl->SetFailed(EREQUEST, "Invalid FlatBuffers request size"); + return; + } + unsigned char header[kHeaderSize + kRequestMetaSize] = {}; + PackHeader(header, kRequestMetaSize, request.size() + attachment_size); + Store32(header + 12, fb_method->service()->index()); + Store32(header + 16, static_cast(fb_method->index())); + Store32(header + 20, static_cast(request.size())); + Store32(header + 24, static_cast(attachment_size)); + Store64(header + 28, correlation_id); + if (buf->append(header, sizeof(header)) != 0) { + cntl->SetFailed(ENOMEM, "Fail to allocate FlatBuffers request header"); + return; + } + buf->append(request); + buf->append(cntl->request_attachment()); +} + +const std::string& GetFlatBuffersMethodName( + const google::protobuf::MethodDescriptor*, const Controller* cntl) { + static const std::string empty; + return cntl->flatbuffers_method() ? cntl->flatbuffers_method()->full_name() : empty; +} + +} // namespace policy +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS diff --git a/src/brpc/policy/flatbuffers_protocol.h b/src/brpc/policy/flatbuffers_protocol.h new file mode 100644 index 0000000000..e3e0065b6d --- /dev/null +++ b/src/brpc/policy/flatbuffers_protocol.h @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#ifndef BRPC_POLICY_FLATBUFFERS_PROTOCOL_H +#define BRPC_POLICY_FLATBUFFERS_PROTOCOL_H + +#include "butil/config.h" + +#if BRPC_WITH_FLATBUFFERS +#include "brpc/protocol.h" + +namespace brpc { +namespace policy { + +ParseResult ParseFlatBuffersMessage(butil::IOBuf* source, Socket* socket, + bool read_eof, const void* arg); +void ProcessFlatBuffersRequest(InputMessageBase* msg); +void ProcessFlatBuffersResponse(InputMessageBase* msg); +bool VerifyFlatBuffersRequest(const InputMessageBase* msg); +void SerializeFlatBuffersRequest(butil::IOBuf* buf, Controller* cntl, + const google::protobuf::Message* request); +void PackFlatBuffersRequest(butil::IOBuf* buf, SocketMessage** user_message, + uint64_t correlation_id, + const google::protobuf::MethodDescriptor* method, + Controller* cntl, const butil::IOBuf& request, + const Authenticator* auth); +const std::string& GetFlatBuffersMethodName( + const google::protobuf::MethodDescriptor* method, const Controller* cntl); + +} // namespace policy +} // namespace brpc +#endif // BRPC_WITH_FLATBUFFERS +#endif // BRPC_POLICY_FLATBUFFERS_PROTOCOL_H diff --git a/src/brpc/server.cpp b/src/brpc/server.cpp index 5a29ae236b..f3504d27a1 100644 --- a/src/brpc/server.cpp +++ b/src/brpc/server.cpp @@ -83,6 +83,10 @@ #include "brpc/rdma/rdma_helper.h" #include "brpc/baidu_master_service.h" #include "brpc/transport_factory.h" +#if BRPC_WITH_FLATBUFFERS +#include +#include "brpc/flatbuffers/service.h" +#endif inline std::ostream& operator<<(std::ostream& os, const timeval& tm) { const char old_fill = os.fill(); @@ -114,6 +118,52 @@ const char* status_str(Server::Status s) { return "UNKNOWN_STATUS"; } +#if BRPC_WITH_FLATBUFFERS +struct Server::FlatBuffersServiceMap { + struct Method { + Method(flatbuffers::Service* service, + const flatbuffers::MethodDescriptor* descriptor) + : status(new MethodStatus) + , property{service, descriptor, status.get(), false} {} + + std::unique_ptr status; + FlatBuffersMethodProperty property; + AdaptiveMaxConcurrency max_concurrency; + }; + struct Service { + Service(flatbuffers::Service* svc, + const flatbuffers::ServiceDescriptor* desc) + : service(svc) + , descriptor(desc) + , ownership(SERVER_DOESNT_OWN_SERVICE) {} + ~Service() { + methods.clear(); + if (ownership == SERVER_OWNS_SERVICE) { + delete service; + } + } + + flatbuffers::Service* service; + const flatbuffers::ServiceDescriptor* descriptor; + ServiceOwnership ownership; + std::map > methods; + }; + typedef std::map > ServiceMap; + ServiceMap services; + + Method* FindMethodByFullName(const butil::StringPiece& fullname) const { + for (const auto& service : services) { + for (const auto& method : service.second->methods) { + if (method.second->property.method->full_name() == fullname) { + return method.second.get(); + } + } + } + return nullptr; + } +}; +#endif + butil::static_atomic g_running_server_count = BUTIL_STATIC_ATOMIC_INIT(0); // Following services may have security issues and are disabled by default. @@ -361,6 +411,19 @@ void* Server::UpdateDerivedVars(void* arg) { it->second.status->Expose(mprefix); } } +#if BRPC_WITH_FLATBUFFERS + if (server->_flatbuffers_services) { + for (const auto& service : server->_flatbuffers_services->services) { + for (const auto& method : service.second->methods) { + mprefix.resize(prefix.size()); + mprefix.push_back('_'); + bvar::to_underscored_name( + &mprefix, method.second->property.method->full_name()); + method.second->status->Expose(mprefix); + } + } + } +#endif if (server->options().baidu_master_service) { server->options().baidu_master_service->Expose(prefix); } @@ -1101,6 +1164,27 @@ int Server::StartInternal(const butil::EndPoint& endpoint, it->second.max_concurrency.SetConcurrencyLimiter(cl); } } +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + for (const auto& service : _flatbuffers_services->services) { + for (const auto& entry : service.second->methods) { + auto& method = *entry.second; + const AdaptiveMaxConcurrency* amc = &method.max_concurrency; + if (amc->type() == AdaptiveMaxConcurrency::UNLIMITED()) { + amc = &_options.method_max_concurrency; + } + ConcurrencyLimiter* cl = nullptr; + if (!CreateConcurrencyLimiter(*amc, &cl)) { + LOG(ERROR) << "Fail to create ConcurrencyLimiter for " + << method.property.method->full_name(); + return -1; + } + method.status->SetConcurrencyLimiter(cl); + method.max_concurrency.SetConcurrencyLimiter(cl); + } + } + } +#endif if (0 != SetServiceMaxConcurrency(_options.nshead_service)) { return -1; } @@ -1409,6 +1493,22 @@ int Server::AddServiceInternal(google::protobuf::Service* service, // defined `option (idl_support) = true' or not. const bool is_idl_support = sd->file()->options().GetExtension(idl_support); +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + for (int i = 0; i < sd->method_count(); ++i) { + const auto* md = sd->method(i); + if (_flatbuffers_services->FindMethodByFullName(md->full_name()) || + (is_idl_support && sd->name() != sd->full_name() && + _flatbuffers_services->FindMethodByFullName( + butil::EnsureString(sd->name()) + "." + + butil::EnsureString(md->name())))) { + LOG(ERROR) << "Protobuf method conflicts with FlatBuffers method: " + << md->full_name(); + return -1; + } + } + } +#endif Tabbed* tabbed = dynamic_cast(service); for (int i = 0; i < sd->method_count(); ++i) { @@ -1639,6 +1739,126 @@ int Server::AddService(google::protobuf::Service* service, return AddServiceInternal(service, false, options); } +#if BRPC_WITH_FLATBUFFERS +int Server::AddFlatBuffersService(flatbuffers::Service* service, + ServiceOwnership ownership) { + if (service == nullptr || + (ownership != SERVER_OWNS_SERVICE && + ownership != SERVER_DOESNT_OWN_SERVICE)) { + LOG(ERROR) << "Invalid FlatBuffers service or ownership"; + return -1; + } + if (InitializeOnce() != 0 || status() != READY) { + LOG(ERROR) << "Can't add FlatBuffers service to Server[" << version() + << "] which is " << status_str(status()); + return -1; + } + const flatbuffers::ServiceDescriptor* sd = service->GetDescriptor(); + if (sd == nullptr || sd->name().empty() || sd->full_name().empty() || + sd->method_count() <= 0) { + LOG(ERROR) << "Invalid FlatBuffers service descriptor"; + return -1; + } + if (_flatbuffers_services) { + for (const auto& entry : _flatbuffers_services->services) { + if (entry.second->service == service || entry.first == sd->index() || + entry.second->descriptor->full_name() == sd->full_name()) { + LOG(ERROR) << "Duplicate FlatBuffers service or hash collision: " + << sd->full_name(); + return -1; + } + } + } + + // Keep ownership with the caller until the entire record is published. + std::unique_ptr record( + new FlatBuffersServiceMap::Service(service, sd)); + std::unordered_set method_names; + for (int i = 0; i < sd->method_count(); ++i) { + const flatbuffers::MethodDescriptor* md = sd->method(i); + if (md == nullptr || md->service() != sd || md->index() < 0 || + md->name().empty() || + md->full_name() != sd->full_name() + "." + md->name() || + sd->FindMethodByIndex(md->index()) != md || + !method_names.insert(md->name()).second || + record->methods.count(md->index()) != 0) { + LOG(ERROR) << "Invalid FlatBuffers method in " << sd->full_name(); + return -1; + } + if (_method_map.seek(md->full_name()) != nullptr) { + LOG(ERROR) << "FlatBuffers method conflicts with protobuf method: " + << md->full_name(); + return -1; + } + record->methods.emplace(md->index(), + std::unique_ptr( + new FlatBuffersServiceMap::Method(service, md))); + } + if (!_flatbuffers_services) { + _flatbuffers_services.reset(new FlatBuffersServiceMap); + } + auto inserted = _flatbuffers_services->services.emplace( + sd->index(), std::move(record)); + if (!inserted.second) { + return -1; + } + inserted.first->second->ownership = ownership; + return 0; +} + +int Server::RemoveFlatBuffersService(flatbuffers::Service* service) { + if (service == nullptr) { + LOG(ERROR) << "Parameter[service] is NULL"; + return -1; + } + if (InitializeOnce() != 0 || status() != READY) { + LOG(ERROR) << "Can't remove FlatBuffers service from Server[" << version() + << "] which is " << status_str(status()); + return -1; + } + if (_flatbuffers_services) { + for (auto it = _flatbuffers_services->services.begin(); + it != _flatbuffers_services->services.end(); ++it) { + if (it->second->service == service) { + std::unique_ptr removed( + std::move(it->second)); + _flatbuffers_services->services.erase(it); + return 0; + } + } + } + return -1; +} + +size_t Server::GetFlatBuffersServiceCount() const { + return _flatbuffers_services ? _flatbuffers_services->services.size() : 0; +} + +const Server::FlatBuffersMethodProperty* +Server::FindFlatBuffersMethodPropertyByIndex(uint32_t service_index, + int32_t method_index) const { + if (!_flatbuffers_services || method_index < 0) { + return nullptr; + } + auto service = _flatbuffers_services->services.find(service_index); + if (service == _flatbuffers_services->services.end()) { + return nullptr; + } + // Wire IDs are sparse, not declaration-order positions. + const flatbuffers::MethodDescriptor* md = + service->second->descriptor->FindMethodByIndex(method_index); + if (md == nullptr) { + return nullptr; + } + auto method = service->second->methods.find(method_index); + if (method == service->second->methods.end() || + method->second->property.method != md) { + return nullptr; + } + return &method->second->property; +} +#endif + int Server::AddBuiltinService(google::protobuf::Service* service) { ServiceOptions options; options.ownership = SERVER_OWNS_SERVICE; @@ -1758,6 +1978,11 @@ void Server::ClearServices() { << "] which is " << status_str(status()); return; } +#if BRPC_WITH_FLATBUFFERS + // Detach every FlatBuffers method before destroying owned services. + std::unique_ptr removed_flatbuffers_services; + removed_flatbuffers_services.swap(_flatbuffers_services); +#endif for (ServiceMap::const_iterator it = _fullname_service_map.begin(); it != _fullname_service_map.end(); ++it) { if (it->second.ownership == SERVER_OWNS_SERVICE) { @@ -1804,6 +2029,9 @@ void Server::GetStat(ServerStatistics* stat) const { stat->connection_count += _internal_am->ConnectionCount(); } stat->user_service_count = service_count(); +#if BRPC_WITH_FLATBUFFERS + stat->user_service_count += GetFlatBuffersServiceCount(); +#endif stat->builtin_service_count = builtin_service_count(); } @@ -1834,8 +2062,11 @@ void Server::GenerateVersionIfNeeded() { if (!_version.empty()) { return; } - int extra_count = !!_options.nshead_service + !!_options.rtmp_service + + size_t extra_count = !!_options.nshead_service + !!_options.rtmp_service + !!_options.thrift_service + !!_options.redis_service; +#if BRPC_WITH_FLATBUFFERS + extra_count += GetFlatBuffersServiceCount(); +#endif _version.reserve((extra_count + service_count()) * 20); for (ServiceMap::const_iterator it = _fullname_service_map.begin(); it != _fullname_service_map.end(); ++it) { @@ -1846,6 +2077,16 @@ void Server::GenerateVersionIfNeeded() { _version.append(butil::class_name_str(*it->second.service)); } } +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + for (const auto& service : _flatbuffers_services->services) { + if (!_version.empty()) { + _version.push_back('+'); + } + _version.append(service.second->descriptor->full_name()); + } + } +#endif if (_options.nshead_service) { if (!_version.empty()) { _version.push_back('+'); @@ -2291,6 +2532,15 @@ AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(const butil::StringPiece& full_ MethodProperty* mp = _method_map.seek(full_method_name); if (mp == nullptr) { +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName( + full_method_name); + if (method) { + return method->max_concurrency; + } + } +#endif break; } return MaxConcurrencyOf(mp); @@ -2303,7 +2553,16 @@ AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(const butil::StringPiece& full_ } int Server::MaxConcurrencyOf(const butil::StringPiece& full_method_name) const { - return MaxConcurrencyOf(_method_map.seek(full_method_name)); + const MethodProperty* mp = _method_map.seek(full_method_name); +#if BRPC_WITH_FLATBUFFERS + if (mp == nullptr && _flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName(full_method_name); + if (method) { + return method->max_concurrency; + } + } +#endif + return MaxConcurrencyOf(mp); } AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(const butil::StringPiece& full_service_name, @@ -2311,6 +2570,15 @@ AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(const butil::StringPiece& full_ MethodProperty* mp = const_cast( FindMethodPropertyByFullName(full_service_name, method_name)); if (mp == nullptr) { +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName( + full_service_name.as_string() + "." + method_name.as_string()); + if (method) { + return method->max_concurrency; + } + } +#endif LOG(ERROR) << "Fail to find method=" << full_service_name << '/' << method_name; _failed_to_set_max_concurrency_of_method = true; @@ -2321,8 +2589,18 @@ AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(const butil::StringPiece& full_ int Server::MaxConcurrencyOf(const butil::StringPiece& full_service_name, const butil::StringPiece& method_name) const { - return MaxConcurrencyOf(FindMethodPropertyByFullName( - full_service_name, method_name)); + const MethodProperty* mp = FindMethodPropertyByFullName( + full_service_name, method_name); +#if BRPC_WITH_FLATBUFFERS + if (mp == nullptr && _flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName( + full_service_name.as_string() + "." + method_name.as_string()); + if (method) { + return method->max_concurrency; + } + } +#endif + return MaxConcurrencyOf(mp); } AdaptiveMaxConcurrency& Server::MaxConcurrencyOf(google::protobuf::Service* service, @@ -2338,6 +2616,19 @@ int Server::MaxConcurrencyOf(google::protobuf::Service* service, bool& Server::IgnoreEovercrowdedOf(const butil::StringPiece& full_method_name) { MethodProperty* mp = _method_map.seek(full_method_name); if (mp == nullptr) { +#if BRPC_WITH_FLATBUFFERS + if (_flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName( + full_method_name); + if (method) { + if (status() != READY) { + LOG(WARNING) << "IgnoreEovercrowdedOf requires a stopped Server"; + return g_default_ignore_eovercrowded; + } + return method->property.ignore_eovercrowded; + } + } +#endif LOG(ERROR) << "Fail to find method=" << full_method_name; _failed_to_set_ignore_eovercrowded = true; return g_default_ignore_eovercrowded; @@ -2362,6 +2653,15 @@ bool Server::IgnoreEovercrowdedOf(const butil::StringPiece& full_method_name) co return g_default_ignore_eovercrowded; } if (mp == nullptr || mp->status == nullptr) { +#if BRPC_WITH_FLATBUFFERS + if (mp == nullptr && _flatbuffers_services) { + auto* method = _flatbuffers_services->FindMethodByFullName( + full_method_name); + if (method) { + return method->property.ignore_eovercrowded; + } + } +#endif return false; } return mp->ignore_eovercrowded; diff --git a/src/brpc/server.h b/src/brpc/server.h index 3c68641be5..c320d55772 100644 --- a/src/brpc/server.h +++ b/src/brpc/server.h @@ -26,6 +26,7 @@ #include "bthread/bthread.h" // Server may need some bthread functions, // e.g. bthread_usleep #include // google::protobuf::Service +#include "butil/config.h" #include "butil/macros.h" // DISALLOW_COPY_AND_ASSIGN #include "butil/containers/doubly_buffered_data.h" // DoublyBufferedData #include "bvar/bvar.h" @@ -49,6 +50,13 @@ namespace brpc { +#if BRPC_WITH_FLATBUFFERS +namespace flatbuffers { +class Service; +class MethodDescriptor; +} // namespace flatbuffers +#endif + class Acceptor; class MethodStatus; class NsheadService; @@ -450,6 +458,15 @@ class Server { }; typedef butil::FlatMap MethodMap; +#if BRPC_WITH_FLATBUFFERS + struct FlatBuffersMethodProperty { + flatbuffers::Service* service; + const flatbuffers::MethodDescriptor* method; + MethodStatus* status; + bool ignore_eovercrowded; + }; +#endif + struct ThreadLocalOptions { bthread_key_t tls_key; const DataFactory* thread_local_data_factory; @@ -521,6 +538,20 @@ class Server { // Returns 0 on success, -1 otherwise. int RemoveService(google::protobuf::Service* service); +#if BRPC_WITH_FLATBUFFERS + // FlatBuffers services are separate from the protobuf service maps. + // The service and its descriptor must remain valid until removal. + // Add/Remove require a stopped server (READY). Ownership transfers only + // after a successful Add; Remove deletes a server-owned service. + // Like AddService/RemoveService, these methods are not thread-safe. + // Full method names must not conflict with registered protobuf methods. + // Returns 0 on success, -1 otherwise. + int AddFlatBuffersService(flatbuffers::Service* service, + ServiceOwnership ownership); + int RemoveFlatBuffersService(flatbuffers::Service* service); + size_t GetFlatBuffersServiceCount() const; +#endif + // Remove all services from this server. // NOTE: clearing services when server is running is forbidden. void ClearServices(); @@ -612,6 +643,7 @@ class Server { // server.MaxConcurrencyOf("example.EchoService.Echo") = 10; // or server.MaxConcurrencyOf("example.EchoService", "Echo") = 10; // or server.MaxConcurrencyOf(&service, "Echo") = 10; + // The string-based forms also support registered FlatBuffers methods. // Note: These interfaces can ONLY be called before the server is started. // And you should NOT set the max_concurrency when you are going to choose // an auto concurrency limiter, eg `options.max_concurrency = "auto"`.If you @@ -703,6 +735,11 @@ friend class Controller; FindMethodPropertyByNameAndIndex(const butil::StringPiece& service_name, int method_index) const; +#if BRPC_WITH_FLATBUFFERS + const FlatBuffersMethodProperty* FindFlatBuffersMethodPropertyByIndex( + uint32_t service_index, int32_t method_index) const; +#endif + const ServiceProperty* FindServicePropertyByFullName(const butil::StringPiece& fullname) const; @@ -773,6 +810,11 @@ friend class Controller; Acceptor* _am; Acceptor* _internal_am; +#if BRPC_WITH_FLATBUFFERS + struct FlatBuffersServiceMap; + std::unique_ptr _flatbuffers_services; +#endif + // Use method->full_name() as key MethodMap _method_map; diff --git a/test/BUILD.bazel b/test/BUILD.bazel index 1aecc39987..5eec8e014c 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -229,11 +229,66 @@ root_runfiles( ], ) +FLATBUFFERS_COMPATIBILITY = select({ + "//:brpc_with_flatbuffers": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +genrule( + name = "flatbuffers_test_messages", + srcs = ["flatbuffers_message.fbs"], + outs = ["flatbuffers_message_generated.h"], + cmd = select({ + "//:brpc_with_flatbuffers": "$(location @com_github_google_flatbuffers//:flatc) --cpp -o $(@D) $(location flatbuffers_message.fbs)", + "//conditions:default": "exit 1", + }), + target_compatible_with = FLATBUFFERS_COMPATIBILITY, + tools = select({ + "//:brpc_with_flatbuffers": ["@com_github_google_flatbuffers//:flatc"], + "//conditions:default": [], + }), +) + +cc_test( + name = "brpc_flatbuffers_unittest", + srcs = [ + "brpc_flatbuffers_unittest.cpp", + ":flatbuffers_test_messages", + ], + copts = COPTS, + includes = ["."], + target_compatible_with = FLATBUFFERS_COMPATIBILITY, + deps = [ + "//:brpc", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "brpc_flatbuffers_protocol_unittest", + srcs = [ + "brpc_flatbuffers_protocol_unittest.cpp", + ":flatbuffers_test_messages", + ], + copts = COPTS, + includes = ["."], + tags = ["exclusive"], + target_compatible_with = FLATBUFFERS_COMPATIBILITY, + deps = [ + "//:brpc", + "@com_google_googletest//:gtest_main", + ], +) + generate_unittests( name = "brpc_unittests", - srcs = glob([ - "brpc_*_unittest.cpp", - ]), + srcs = glob( + ["brpc_*_unittest.cpp"], + exclude = [ + "brpc_flatbuffers_unittest.cpp", + "brpc_flatbuffers_protocol_unittest.cpp", + ], + ), deps = [ ":gperftools_helper", "//:brpc", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 71baf85df4..2b27624c27 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -268,8 +268,37 @@ foreach(BTHREAD_UT ${BTHREAD_UNITTESTS}) add_test(NAME ${BTHREAD_UT_WE} COMMAND ${BTHREAD_UT_WE}) endforeach() +# FlatBuffers tests use the release library, independently of the debug suite. +if(WITH_FLATBUFFERS) + find_program(FLATBUFFERS_FLATC_EXECUTABLE NAMES flatc) + if(NOT FLATBUFFERS_FLATC_EXECUTABLE) + message(FATAL_ERROR + "FlatBuffers tests require an installed flatc; set FLATBUFFERS_FLATC_EXECUTABLE.") + endif() + set(FLATBUFFERS_TEST_HEADER + "${CMAKE_CURRENT_BINARY_DIR}/flatbuffers_message_generated.h") + add_custom_command( + OUTPUT "${FLATBUFFERS_TEST_HEADER}" + COMMAND "${FLATBUFFERS_FLATC_EXECUTABLE}" --cpp + -o "${CMAKE_CURRENT_BINARY_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/flatbuffers_message.fbs" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/flatbuffers_message.fbs" + "${FLATBUFFERS_FLATC_EXECUTABLE}" + COMMENT "Generating FlatBuffers test messages" + VERBATIM) + foreach(FB_TEST brpc_flatbuffers_unittest brpc_flatbuffers_protocol_unittest) + add_executable(${FB_TEST} ${FB_TEST}.cpp "${FLATBUFFERS_TEST_HEADER}") + target_link_libraries(${FB_TEST} PRIVATE + brpc-static brpc_test_config gtest_main ${DYNAMIC_LIB}) + add_test(NAME ${FB_TEST} COMMAND ${FB_TEST}) + endforeach() +endif() + # brpc tests file(GLOB BRPC_UNITTESTS "brpc_*_unittest.cpp") +list(REMOVE_ITEM BRPC_UNITTESTS + "${CMAKE_CURRENT_SOURCE_DIR}/brpc_flatbuffers_unittest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/brpc_flatbuffers_protocol_unittest.cpp") foreach(BRPC_UT ${BRPC_UNITTESTS}) get_filename_component(BRPC_UT_WE ${BRPC_UT} NAME_WE) add_executable(${BRPC_UT_WE} ${BRPC_UT} $) diff --git a/test/Makefile b/test/Makefile index de4d566bcd..87f5754876 100644 --- a/test/Makefile +++ b/test/Makefile @@ -181,6 +181,9 @@ TEST_BTHREAD_SOURCES = $(wildcard bthread_*unittest.cpp) TEST_BTHREAD_OBJS = $(addsuffix .o, $(basename $(TEST_BTHREAD_SOURCES))) TEST_BRPC_SOURCES = $(wildcard brpc_*unittest.cpp) +ifneq ($(WITH_FLATBUFFERS),1) +TEST_BRPC_SOURCES := $(filter-out brpc_flatbuffers_unittest.cpp brpc_flatbuffers_protocol_unittest.cpp,$(TEST_BRPC_SOURCES)) +endif TEST_BRPC_OBJS = $(addsuffix .o, $(basename $(TEST_BRPC_SOURCES))) TEST_PROTO_SOURCES = $(wildcard *.proto) @@ -194,7 +197,7 @@ all: $(TEST_BINS) .PHONY:clean clean:clean_bins @echo "> Cleaning" - rm -rf $(TEST_BUTIL_OBJS) $(TEST_BVAR_OBJS) $(TEST_BTHREAD_OBJS) $(TEST_BRPC_OBJS) $(TEST_PROTO_OBJS) $(TEST_PROTO_SOURCES:.proto=.pb.h) $(TEST_PROTO_SOURCES:.proto=.pb.cc) + rm -rf $(TEST_BUTIL_OBJS) $(TEST_BVAR_OBJS) $(TEST_BTHREAD_OBJS) $(TEST_BRPC_OBJS) $(TEST_PROTO_OBJS) $(TEST_PROTO_SOURCES:.proto=.pb.h) $(TEST_PROTO_SOURCES:.proto=.pb.cc) flatbuffers_message_generated.h $(MAKE) -C.. clean_debug .PHONY:clean_bins @@ -235,6 +238,23 @@ else ifeq ($(SYSTEM),Darwin) $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) $^ $(GTEST_STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) endif +ifeq ($(WITH_FLATBUFFERS),1) +FLATC ?= flatc + +flatbuffers_message_generated.h:flatbuffers_message.fbs + $(FLATC) --cpp -o . $< + +brpc_flatbuffers_unittest.o brpc_flatbuffers_protocol_unittest.o:flatbuffers_message_generated.h + +brpc_flatbuffers_unittest brpc_flatbuffers_protocol_unittest: %: %.o | libbrpc.dbg.$(SOEXT) + @echo "> Linking $@" +ifeq ($(SYSTEM),Linux) + $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) -Xlinker "-(" $^ -Xlinker "-)" $(STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) +else ifeq ($(SYSTEM),Darwin) + $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) $^ $(GTEST_STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) +endif +endif + brpc_%_unittest:$(TEST_PROTO_OBJS) brpc_%_unittest.o | libbrpc.dbg.$(SOEXT) @echo "> Linking $@" ifeq ($(SYSTEM),Linux) diff --git a/test/brpc_channel_unittest.cpp b/test/brpc_channel_unittest.cpp index 27f711d483..49ab77561f 100644 --- a/test/brpc_channel_unittest.cpp +++ b/test/brpc_channel_unittest.cpp @@ -281,7 +281,9 @@ class ChannelTest : public ::testing::Test{ nullptr, ProcessRpcRequest, VerifyMyRequest, nullptr, nullptr, brpc::CONNECTION_TYPE_ALL, "baidu_std" }; - ASSERT_EQ(0, RegisterProtocol((brpc::ProtocolType)30, dummy_protocol)); + // Keep the test protocol outside the IDs assigned to builtins. + const auto dummy_type = static_cast(brpc::ProtocolType_MAX + 1); + ASSERT_EQ(0, RegisterProtocol(dummy_type, dummy_protocol)); } static void ProcessRpcRequest(brpc::InputMessageBase* msg_base) { diff --git a/test/brpc_flatbuffers_protocol_unittest.cpp b/test/brpc_flatbuffers_protocol_unittest.cpp new file mode 100644 index 0000000000..21e6749641 --- /dev/null +++ b/test/brpc_flatbuffers_protocol_unittest.cpp @@ -0,0 +1,1825 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "butil/config.h" +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "butil/endpoint.h" +#include "butil/fd_guard.h" +#include "brpc/authenticator.h" +#include "brpc/channel.h" +#include "brpc/closure_guard.h" +#include "brpc/controller.h" +#include "brpc/details/controller_private_accessor.h" +#include "brpc/details/method_status.h" +#include "brpc/details/server_private_accessor.h" +#include "brpc/errno.pb.h" +#include "brpc/flatbuffers/message.h" +#include "brpc/flatbuffers/service.h" +#include "brpc/interceptor.h" +#include "brpc/retry_policy.h" +#include "brpc/policy/flatbuffers_protocol.h" +#include "brpc/policy/most_common_message.h" +#include "brpc/server.h" +#include "brpc/stream.h" +#include "flatbuffers_message_generated.h" + +namespace { + +using brpc::flatbuffers::BrpcDescriptorTable; +using brpc::flatbuffers::Message; +using brpc::flatbuffers::MessageBuilder; +using brpc::flatbuffers::MethodDescriptor; +using brpc::flatbuffers::ServiceDescriptor; +using brpc_fbtest::Payload; + +const int kRpcTimeoutMs = 1500; +const int kWaitTimeoutMs = 5000; +const size_t kHeaderSize = 12; +const size_t kRequestMetaSize = 24; +const size_t kResponseMetaSize = 20; + +// These independent byte-level helpers intentionally do not reuse the codec. +void Put32(std::string* bytes, size_t offset, uint32_t value, + bool big_endian = false) { + for (size_t i = 0; i < 4; ++i) { + (*bytes)[offset + i] = static_cast( + value >> (8 * (big_endian ? 3 - i : i))); + } +} + +uint32_t Get32(const std::string& bytes, size_t offset, + bool big_endian = false) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast( + static_cast(bytes[offset + i])) + << (8 * (big_endian ? 3 - i : i)); + } + return value; +} + +void Put64(std::string* bytes, size_t offset, uint64_t value) { + Put32(bytes, offset, static_cast(value)); + Put32(bytes, offset + 4, static_cast(value >> 32)); +} + +uint64_t Get64(const std::string& bytes, size_t offset) { + return Get32(bytes, offset) | + (static_cast(Get32(bytes, offset + 4)) << 32); +} + +std::string Header(uint32_t body_size, uint32_t meta_size) { + std::string bytes(kHeaderSize, '\0'); + bytes.replace(0, 4, "FRPC"); + Put32(&bytes, 4, body_size, true); + Put32(&bytes, 8, meta_size, true); + return bytes; +} + +std::string Frame(const std::string& meta, const std::string& payload) { + return Header(meta.size() + payload.size(), meta.size()) + meta + payload; +} + +std::string Bytes(const Message& message) { + return std::string(reinterpret_cast(message.data()), + message.size()); +} + +Message MakePayload(size_t text_size = 16, int64_t value = 123, + size_t vector_size = 4) { + MessageBuilder builder(8); + auto text = builder.CreateString(std::string(text_size, 'x')); + std::vector numbers(vector_size); + for (size_t i = 0; i < numbers.size(); ++i) { + numbers[i] = static_cast(i) * -3; + } + auto values = builder.CreateVector(numbers); + builder.Finish(brpc_fbtest::CreatePayload(builder, value, text, values)); + return builder.ReleaseMessage(); +} + +void ExpectReply(const Message& response, const Message& request, + const MethodDescriptor* method) { + ASSERT_NE(nullptr, method); + ASSERT_TRUE(request.Verify()); + // Framing validation is not schema validation; the caller must do this. + ASSERT_TRUE(response.Verify()); + const Payload* in = request.GetRoot(); + const Payload* out = response.GetRoot(); + EXPECT_EQ(in->value() + method->index(), out->value()); + ASSERT_NE(nullptr, out->message()); + EXPECT_EQ(method->name() + ":" + in->message()->str(), + out->message()->str()); + ASSERT_NE(nullptr, out->values()); + ASSERT_EQ(in->values()->size(), out->values()->size()); + for (size_t i = 0; i < in->values()->size(); ++i) { + EXPECT_EQ(in->values()->Get(i), out->values()->Get(i)); + } +} + +std::string RequestMeta(const MethodDescriptor* method, uint32_t message_size, + uint32_t attachment_size, uint64_t correlation_id) { + std::string meta(kRequestMetaSize, '\0'); + Put32(&meta, 0, method->service()->index()); + Put32(&meta, 4, method->index()); + Put32(&meta, 8, message_size); + Put32(&meta, 12, attachment_size); + Put64(&meta, 16, correlation_id); + return meta; +} + +std::string ResponseMeta(uint32_t message_size, uint32_t attachment_size, + uint64_t correlation_id) { + std::string meta(kResponseMetaSize, '\0'); + Put32(&meta, 4, message_size); + Put32(&meta, 8, attachment_size); + Put64(&meta, 12, correlation_id); + return meta; +} + +class Completion : public google::protobuf::Closure { +public: + void Run() override { + std::lock_guard lock(_mutex); + ++_calls; + _condition.notify_all(); + } + bool Wait() { + std::unique_lock lock(_mutex); + return _condition.wait_for(lock, std::chrono::milliseconds(kWaitTimeoutMs), + [this] { return _calls != 0; }); + } + int calls() { + std::lock_guard lock(_mutex); + return _calls; + } +private: + std::mutex _mutex; + std::condition_variable _condition; + int _calls = 0; +}; + +class PayloadService : public brpc::flatbuffers::Service { +public: + explicit PayloadService( + BrpcDescriptorTable table = {"brpc_fbtest", "ProtocolService", + "Echo Alternate Removed", {7, 41, 99}}, + std::atomic* destroyed = nullptr) + : _destroyed(destroyed) { + EXPECT_EQ(0, _descriptor.init(table)); + } + ~PayloadService() override { + ReleaseHeld(); + if (_destroyed) { + ++*_destroyed; + } + } + const ServiceDescriptor* GetDescriptor() override { return &_descriptor; } + void FBCallMethod(const MethodDescriptor* method, + google::protobuf::RpcController* controller, + const Message* request, Message* response, + google::protobuf::Closure* done) override { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(controller); + ++entered; + if (cntl->is_security_mode()) { + ++security_mode_calls; + } + if (!method || _descriptor.FindMethodByIndex(method->index()) != method || + cntl->flatbuffers_method() != method) { + cntl->SetFailed(brpc::ENOMETHOD, "Wrong FlatBuffers method context"); + return; + } + if (!request || !request->Verify()) { + ++rejected; + cntl->SetFailed(brpc::EREQUEST, "Invalid Payload schema"); + return; + } + // No application field is read before Verify succeeds. + const Payload* in = request->GetRoot(); + if (!in->message() || !in->values()) { + cntl->SetFailed(brpc::EREQUEST, "Missing application fields"); + return; + } + const int failure = fail_next_code.exchange(0); + if (failure != 0) { + cntl->SetFailed(failure, "Requested one-shot application failure"); + return; + } + MessageBuilder builder(8); + auto text = builder.CreateString(method->name() + ":" + + in->message()->str()); + const std::vector numbers(in->values()->begin(), + in->values()->end()); + auto values = builder.CreateVector(numbers); + builder.Finish(brpc_fbtest::CreatePayload( + builder, in->value() + method->index(), text, values)); + *response = builder.ReleaseMessage(); + cntl->response_attachment().append(cntl->request_attachment()); + if (response_compressed.load()) { + cntl->set_response_compress_type(brpc::COMPRESS_TYPE_GZIP); + } + if (response_checksummed.load()) { + cntl->set_response_checksum_type(brpc::CHECKSUM_TYPE_CRC32C); + } + std::lock_guard lock(_mutex); + if (_hold_next) { + _hold_next = false; + _held = done_guard.release(); + _condition.notify_all(); + } + } + void HoldNext() { + std::lock_guard lock(_mutex); + _hold_next = true; + } + bool WaitHeld() { + std::unique_lock lock(_mutex); + return _condition.wait_for(lock, std::chrono::milliseconds(kWaitTimeoutMs), + [this] { return _held != nullptr; }); + } + void ReleaseHeld() { + google::protobuf::Closure* done = nullptr; + { + std::lock_guard lock(_mutex); + _hold_next = false; + std::swap(done, _held); + } + if (done) { + done->Run(); + } + } + std::atomic entered{0}; + std::atomic rejected{0}; + std::atomic security_mode_calls{0}; + std::atomic fail_next_code{0}; + std::atomic response_compressed{false}; + std::atomic response_checksummed{false}; +private: + ServiceDescriptor _descriptor; + std::atomic* _destroyed; + std::mutex _mutex; + std::condition_variable _condition; + bool _hold_next = false; + google::protobuf::Closure* _held = nullptr; +}; + +int InitChannel(brpc::Channel* channel, const butil::EndPoint& endpoint, + const char* protocol = "fb_rpc", + const char* connection_type = "single") { + brpc::ChannelOptions options; + options.protocol = protocol; + options.connection_type = connection_type; + options.timeout_ms = kRpcTimeoutMs; + options.connect_timeout_ms = kRpcTimeoutMs; + options.max_retry = 0; + return channel->Init(endpoint, &options); +} + +class FlatBuffersProtocolTest : public ::testing::Test { +protected: + void SetUp() override { + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr)); + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + } + void TearDown() override { + service.ReleaseHeld(); + server.Stop(0); + server.Join(); + } + const MethodDescriptor* method(int id = 7) { + return service.GetDescriptor()->FindMethodByIndex(id); + } + PayloadService service; + brpc::Server server; + brpc::Channel channel; +}; + +TEST(FlatBuffersFramingTest, PartialFramesAndInvalidHeadersDoNotConsumeInput) { + const std::string wire = Frame(std::string(24, 'm'), "payload"); + for (size_t size = 0; size < wire.size(); ++size) { + SCOPED_TRACE(size); + butil::IOBuf input; + input.append(wire.data(), size); + const std::string before = input.to_string(); + brpc::ParseResult result = brpc::policy::ParseFlatBuffersMessage( + &input, nullptr, false, nullptr); + EXPECT_EQ(brpc::PARSE_ERROR_NOT_ENOUGH_DATA, result.error()); + EXPECT_EQ(before, input.to_string()); + } + for (const std::string& bad : {std::string("X"), std::string("FX"), + std::string("FRX"), std::string("FRPX")}) { + butil::IOBuf input; + input.append(bad); + EXPECT_EQ(brpc::PARSE_ERROR_TRY_OTHERS, + brpc::policy::ParseFlatBuffersMessage( + &input, nullptr, false, nullptr).error()); + EXPECT_EQ(bad, input.to_string()); + } + const std::vector > malformed = { + {Header(std::numeric_limits::max(), 24), + brpc::PARSE_ERROR_TOO_BIG_DATA}, + {Header(0, 1), brpc::PARSE_ERROR_ABSOLUTELY_WRONG}, + {Header(23, 24), brpc::PARSE_ERROR_ABSOLUTELY_WRONG} + }; + for (const auto& entry : malformed) { + butil::IOBuf input; + input.append(entry.first); + EXPECT_EQ(entry.second, brpc::policy::ParseFlatBuffersMessage( + &input, nullptr, false, nullptr).error()); + EXPECT_EQ(entry.first, input.to_string()); + } +} + +TEST(FlatBuffersFramingTest, FragmentedUnalignedAndExtendedMetadata) { + const Message payload = MakePayload(8193); + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "Framing", "Echo", {41}})); + std::string meta = RequestMeta(descriptor.method(0), payload.size(), 3, + 0x1122334455667788ULL); + meta.append("\x01\x02\x03\x04\x05", 5); + const std::string wire = Frame(meta, Bytes(payload) + "att"); + for (size_t fragment_size : {1u, 7u, 13u, 4093u}) { + SCOPED_TRACE(fragment_size); + butil::IOBuf input; + for (size_t pos = 0; pos < wire.size(); pos += fragment_size) { + const size_t size = std::min(fragment_size, wire.size() - pos); + char* allocation = static_cast(malloc(size + 1)); + ASSERT_NE(nullptr, allocation); + memcpy(allocation + 1, wire.data() + pos, size); + ASSERT_EQ(0, input.append_user_data(allocation + 1, size, + [allocation](void*) { free(allocation); })); + } + ASSERT_GT(input.backing_block_num(), 1u); + input.append("next"); + brpc::ParseResult result = brpc::policy::ParseFlatBuffersMessage( + &input, nullptr, false, nullptr); + ASSERT_EQ(brpc::PARSE_OK, result.error()); + brpc::DestroyingPtr parsed( + static_cast(result.message())); + EXPECT_EQ(meta, parsed->meta.to_string()); + EXPECT_EQ(Bytes(payload) + "att", parsed->payload.to_string()); + EXPECT_EQ("next", input.to_string()); + Message decoded; + butil::IOBuf body; + parsed->payload.cutn(&body, payload.size()); + ASSERT_TRUE(decoded.parse_msg_from_iobuf(body, payload.size(), 0)); + ASSERT_TRUE(decoded.Verify()); + EXPECT_EQ(Bytes(payload), Bytes(decoded)); + } +} + +TEST(FlatBuffersFramingTest, EmptyMetadataIsSafelySeparatedForProcessorValidation) { + for (size_t meta_size : {0u, 1u, 19u, 23u, 24u}) { + butil::IOBuf input; + input.append(Frame(std::string(meta_size, '\xff'), "x")); + brpc::ParseResult result = brpc::policy::ParseFlatBuffersMessage( + &input, nullptr, false, nullptr); + ASSERT_EQ(brpc::PARSE_OK, result.error()); + brpc::DestroyingPtr parsed( + static_cast(result.message())); + EXPECT_EQ(meta_size, parsed->meta.size()); + EXPECT_EQ("x", parsed->payload.to_string()); + EXPECT_TRUE(input.empty()); + } +} + +TEST(FlatBuffersSerializationTest, RejectsNullProtobufEmptyCompressionAndChecksum) { + google::protobuf::DescriptorProto protobuf; + Message empty; + Message valid = MakePayload(); + const google::protobuf::Message* invalid[] = {nullptr, &protobuf, &empty}; + for (const auto* request : invalid) { + brpc::Controller cntl; + butil::IOBuf serialized; + brpc::policy::SerializeFlatBuffersRequest(&serialized, &cntl, request); + EXPECT_TRUE(cntl.Failed()); + EXPECT_EQ(brpc::EREQUEST, cntl.ErrorCode()); + EXPECT_TRUE(serialized.empty()); + } + for (int option = 0; option < 2; ++option) { + brpc::Controller cntl; + if (option == 0) { + cntl.set_request_compress_type(brpc::COMPRESS_TYPE_GZIP); + } else { + cntl.set_request_checksum_type(brpc::CHECKSUM_TYPE_CRC32C); + } + butil::IOBuf serialized; + brpc::policy::SerializeFlatBuffersRequest(&serialized, &cntl, &valid); + EXPECT_EQ(brpc::EREQUEST, cntl.ErrorCode()); + EXPECT_TRUE(serialized.empty()); + } +} + +TEST(FlatBuffersSerializationTest, PayloadOnlyAndIndependentRetryHeadersKeepConstInput) { + Message mutable_request = MakePayload(8192); + memset(mutable_request.mutable_buf_begin(), 0xa5, + mutable_request.get_meta_size()); + const Message request = std::move(mutable_request); + const std::string original = Bytes(request); + const size_t prefix_size = request.get_meta_size(); + const std::string prefix(reinterpret_cast(request.data()) - + prefix_size, prefix_size); + const uint8_t* data = request.data(); + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "Pack", "Echo", {41}})); + brpc::Controller cntl; + brpc::ControllerPrivateAccessor(&cntl).set_flatbuffers_method( + descriptor.method(0)); + cntl.request_attachment().append("\0attachment", 11); + butil::IOBuf serialized; + brpc::policy::SerializeFlatBuffersRequest(&serialized, &cntl, &request); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(original, serialized.to_string()); + EXPECT_EQ(request.size(), serialized.size()); + butil::IOBuf first; + brpc::policy::PackFlatBuffersRequest(&first, nullptr, + 0x0102030405060708ULL, nullptr, &cntl, serialized, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + const std::string first_snapshot = first.to_string(); + cntl.request_attachment().clear(); + cntl.request_attachment().append("second"); + butil::IOBuf second; + brpc::policy::PackFlatBuffersRequest(&second, nullptr, + 0x8877665544332211ULL, nullptr, &cntl, serialized, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + const std::string second_snapshot = second.to_string(); + ASSERT_GE(first_snapshot.size(), kHeaderSize + kRequestMetaSize); + ASSERT_GE(second_snapshot.size(), kHeaderSize + kRequestMetaSize); + EXPECT_EQ("FRPC", first_snapshot.substr(0, 4)); + EXPECT_EQ(first_snapshot.size() - 12, Get32(first_snapshot, 4, true)); + EXPECT_EQ(24u, Get32(first_snapshot, 8, true)); + EXPECT_EQ(descriptor.index(), Get32(first_snapshot, 12)); + EXPECT_EQ(41u, Get32(first_snapshot, 16)); + EXPECT_EQ(request.size(), Get32(first_snapshot, 20)); + EXPECT_EQ(11u, Get32(first_snapshot, 24)); + EXPECT_EQ(0x0102030405060708ULL, Get64(first_snapshot, 28)); + EXPECT_EQ(0x8877665544332211ULL, Get64(second_snapshot, 28)); + EXPECT_EQ(6u, Get32(second_snapshot, 24)); + EXPECT_EQ(first_snapshot, first.to_string()); + EXPECT_EQ(original, serialized.to_string()); + EXPECT_EQ(data, request.data()); + EXPECT_EQ(prefix_size, request.get_meta_size()); + EXPECT_EQ(prefix, std::string(reinterpret_cast(request.data()) - + prefix_size, prefix_size)); + EXPECT_EQ(original, Bytes(request)); + EXPECT_EQ(original, first_snapshot.substr(36, request.size())); + EXPECT_EQ(original, second_snapshot.substr(36, request.size())); +} + +class RejectingAuthenticator : public brpc::Authenticator { +public: + int GenerateCredential(std::string* credential) const override { + ++generated; + *credential = "test-credential"; + return 0; + } + int VerifyCredential(const std::string&, const butil::EndPoint&, + brpc::AuthContext*) const override { + return -1; + } + mutable std::atomic generated{0}; +}; + +TEST(FlatBuffersSerializationTest, PackRejectsMissingMethodEmptyPayloadAndAuth) { + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "PackErrors", "Echo", {7}})); + const Message request = MakePayload(); + RejectingAuthenticator auth; + for (int variant = 0; variant < 3; ++variant) { + brpc::Controller cntl; + if (variant != 0) { + brpc::ControllerPrivateAccessor(&cntl).set_flatbuffers_method( + descriptor.method(0)); + } + butil::IOBuf serialized; + if (variant != 1) { + serialized.append(Bytes(request)); + } + butil::IOBuf packed; + brpc::policy::PackFlatBuffersRequest(&packed, nullptr, 1, nullptr, + &cntl, serialized, variant == 2 ? &auth : nullptr); + EXPECT_TRUE(cntl.Failed()); + EXPECT_TRUE(packed.empty()); + } +} + +TEST_F(FlatBuffersProtocolTest, SynchronousAndAsynchronousPayloadAttachmentVariants) { + EXPECT_EQ(30, static_cast(brpc::PROTOCOL_FLATBUFFERS_RPC)); + for (size_t size : {0u, 1u, 63u, 1024u, 8193u, 65537u}) { + for (bool asynchronous : {false, true}) { + SCOPED_TRACE(size); + SCOPED_TRACE(asynchronous); + const Message request = MakePayload(size, size + 31, size % 19); + Message response; + brpc::Controller cntl; + std::string attachment(size / 3, '\0'); + for (size_t i = 0; i < attachment.size(); ++i) { + attachment[i] = static_cast(i % 251); + } + cntl.request_attachment().append(attachment); + Completion done; + channel.FBCallMethod(method(41), &cntl, &request, &response, + asynchronous ? &done : nullptr); + if (asynchronous) { + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + } + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(method(41), cntl.flatbuffers_method()); + EXPECT_EQ(brpc::PROTOCOL_FLATBUFFERS_RPC, cntl.request_protocol()); + ExpectReply(response, request, method(41)); + EXPECT_EQ(attachment, cntl.response_attachment().to_string()); + } + } +} + +TEST_F(FlatBuffersProtocolTest, ConcurrentCallsShareOneConstMessageWithoutMutation) { + Message mutable_request = MakePayload(16385, 9981, 256); + memset(mutable_request.mutable_buf_begin(), 0x3d, + mutable_request.get_meta_size()); + const Message request = std::move(mutable_request); + const std::string original = Bytes(request); + const size_t prefix_size = request.get_meta_size(); + const std::string prefix(reinterpret_cast(request.data()) - + prefix_size, prefix_size); + const MethodDescriptor* echo = method(); + const MethodDescriptor* alternate = method(41); + std::vector clients; + for (int client = 0; client < 6; ++client) { + clients.emplace_back([&, client] { + for (int call = 0; call < 8; ++call) { + brpc::Controller cntl; + Message response; + const std::string attachment = std::to_string(client) + ":" + + std::to_string(call); + cntl.request_attachment().append(attachment); + const MethodDescriptor* selected = call % 2 ? echo : alternate; + channel.FBCallMethod(selected, &cntl, &request, &response, nullptr); + EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText(); + if (!cntl.Failed()) { + ExpectReply(response, request, selected); + EXPECT_EQ(attachment, cntl.response_attachment().to_string()); + } + } + }); + } + for (auto& client : clients) { + client.join(); + } + EXPECT_EQ(48, service.entered.load()); + EXPECT_EQ(original, Bytes(request)); + EXPECT_EQ(prefix_size, request.get_meta_size()); + EXPECT_EQ(prefix, std::string(reinterpret_cast(request.data()) - + prefix_size, prefix_size)); +} + +TEST_F(FlatBuffersProtocolTest, UnknownServiceAndMethodDoNotDispatch) { + ServiceDescriptor unknown_service; + ServiceDescriptor unknown_method; + ASSERT_EQ(0, unknown_service.init( + {"brpc_fbtest", "MissingService", "Echo", {7}})); + ASSERT_EQ(0, unknown_method.init( + {"brpc_fbtest", "ProtocolService", "Missing", {12345}})); + const Message request = MakePayload(); + for (const MethodDescriptor* missing : {unknown_service.method(0), + unknown_method.method(0)}) { + Message response; + brpc::Controller cntl; + channel.FBCallMethod(missing, &cntl, &request, &response, nullptr); + EXPECT_EQ(brpc::ENOMETHOD, cntl.ErrorCode()) << cntl.ErrorText(); + EXPECT_EQ(0u, response.size()); + } + EXPECT_EQ(0, service.entered.load()); + brpc::Controller cntl; + Message response; + channel.FBCallMethod(method(), &cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ExpectReply(response, request, method()); +} + +TEST_F(FlatBuffersProtocolTest, ServiceVerifiesInvalidPayloadAndRemainsUsable) { + for (size_t size : {1u, 3u, 4u, 16u, 65u}) { + butil::IOBuf bytes; + bytes.append(std::string(size, '\xff')); + Message invalid; + ASSERT_TRUE(invalid.parse_msg_from_iobuf(bytes, size, 0)); + ASSERT_FALSE(invalid.Verify()); + brpc::Controller cntl; + Message response; + channel.FBCallMethod(method(), &cntl, &invalid, &response, nullptr); + EXPECT_EQ(brpc::EREQUEST, cntl.ErrorCode()) << cntl.ErrorText(); + EXPECT_EQ(0u, response.size()); + } + EXPECT_EQ(5, service.rejected.load()); + const Message request = MakePayload(); + Message response; + brpc::Controller cntl; + channel.FBCallMethod(method(), &cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ExpectReply(response, request, method()); + EXPECT_EQ(6, service.entered.load()); +} + +TEST_F(FlatBuffersProtocolTest, AsynchronousValidationErrorsRunCallbackOnce) { + const Message request = MakePayload(); + brpc::Channel protobuf_channel; + ASSERT_EQ(0, InitChannel(&protobuf_channel, server.listen_address(), + "baidu_std")); + for (int variant = 0; variant < 4; ++variant) { + SCOPED_TRACE(variant); + Message response; + brpc::Controller cntl; + Completion done; + brpc::Channel* selected = variant == 0 ? &protobuf_channel : &channel; + selected->FBCallMethod(variant == 1 ? nullptr : method(), &cntl, + variant == 2 ? nullptr : &request, + variant == 3 ? nullptr : &response, &done); + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + EXPECT_TRUE(cntl.Failed()); + } + EXPECT_EQ(0, service.entered.load()); +} + +TEST_F(FlatBuffersProtocolTest, RejectsProtobufOnLegacyCallbackWithoutDispatch) { + google::protobuf::DescriptorProto request; + google::protobuf::DescriptorProto response; + request.set_name("not-a-flatbuffer"); + brpc::Controller cntl; + Completion done; + channel.CallMethod(nullptr, &cntl, &request, &response, &done); + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + EXPECT_TRUE(cntl.Failed()); + EXPECT_EQ(0, service.entered.load()); +} + +TEST_F(FlatBuffersProtocolTest, UnsupportedCompressionChecksumAndStreamsFailRpc) { + const Message request = MakePayload(); + for (int variant = 0; variant < 3; ++variant) { + brpc::Controller cntl; + Message response; + brpc::StreamId stream = brpc::INVALID_STREAM_ID; + if (variant == 0) { + cntl.set_request_compress_type(brpc::COMPRESS_TYPE_GZIP); + } else if (variant == 1) { + cntl.set_request_checksum_type(brpc::CHECKSUM_TYPE_CRC32C); + } else { + ASSERT_EQ(0, brpc::StreamCreate(&stream, cntl, nullptr)); + } + Completion done; + channel.FBCallMethod(method(), &cntl, &request, &response, &done); + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + EXPECT_TRUE(cntl.Failed()); + if (stream != brpc::INVALID_STREAM_ID) { + brpc::StreamClose(stream); + } + } + EXPECT_EQ(0, service.entered.load()); + for (int variant = 0; variant < 2; ++variant) { + service.response_compressed = variant == 0; + service.response_checksummed = variant == 1; + brpc::Controller cntl; + Message response; + channel.FBCallMethod(method(), &cntl, &request, &response, nullptr); + EXPECT_EQ(brpc::ERESPONSE, cntl.ErrorCode()) << cntl.ErrorText(); + EXPECT_EQ(0u, response.size()); + } +} + +TEST_F(FlatBuffersProtocolTest, ClientAuthenticationIsRejected) { + RejectingAuthenticator auth; + brpc::ChannelOptions options; + options.protocol = "fb_rpc"; + options.auth = &auth; + options.timeout_ms = kRpcTimeoutMs; + options.max_retry = 0; + brpc::Channel authenticated; + ASSERT_EQ(0, authenticated.Init(server.listen_address(), &options)); + const Message request = MakePayload(); + brpc::Controller cntl; + Message response; + authenticated.FBCallMethod(method(), &cntl, &request, &response, nullptr); + EXPECT_TRUE(cntl.Failed()); + EXPECT_EQ(0, service.entered.load()); +} + +TEST(FlatBuffersLifecycleTest, AuthenticationOnServerCannotBeBypassed) { + PayloadService service; + RejectingAuthenticator auth; + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions options; + options.auth = &auth; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &options)); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + const Message request = MakePayload(); + Message response; + brpc::Controller cntl; + channel.FBCallMethod(service.GetDescriptor()->method(0), &cntl, &request, + &response, nullptr); + EXPECT_TRUE(cntl.Failed()); + EXPECT_EQ(0, service.entered.load()); + EXPECT_EQ(0, server.Stop(0)); + EXPECT_EQ(0, server.Join()); +} + +TEST(FlatBuffersLifecycleTest, StableIdsSurviveReorderRemovalAndRestart) { + PayloadService original; + PayloadService reordered({"brpc_fbtest", "ProtocolService", + "Alternate Echo", {41, 7}}); + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &original, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr)); + const ServiceDescriptor* client_descriptor = original.GetDescriptor(); + const Message request = MakePayload(); + for (int version = 0; version < 2; ++version) { + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + for (int id : {7, 41, 99}) { + SCOPED_TRACE(version); + SCOPED_TRACE(id); + const MethodDescriptor* method = client_descriptor->FindMethodByIndex(id); + ASSERT_NE(nullptr, method); + brpc::Controller cntl; + Message response; + channel.FBCallMethod(method, &cntl, &request, &response, nullptr); + if (version == 1 && id == 99) { + EXPECT_EQ(brpc::ENOMETHOD, cntl.ErrorCode()); + } else { + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ExpectReply(response, request, method); + } + } + ASSERT_EQ(0, server.Stop(0)); + ASSERT_EQ(0, server.Join()); + if (version == 0) { + ASSERT_EQ(0, server.RemoveFlatBuffersService(&original)); + EXPECT_EQ(0, server.GetFlatBuffersServiceCount()); + ASSERT_EQ(0, server.AddFlatBuffersService( + &reordered, brpc::SERVER_DOESNT_OWN_SERVICE)); + EXPECT_EQ(1, server.GetFlatBuffersServiceCount()); + ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr)); + } + } + EXPECT_EQ(3, original.entered.load()); + EXPECT_EQ(2, reordered.entered.load()); + server.ClearServices(); + EXPECT_EQ(0, server.GetFlatBuffersServiceCount()); +} + +class NullDescriptorService : public brpc::flatbuffers::Service { +public: + explicit NullDescriptorService(std::atomic* destroyed) + : _destroyed(destroyed) {} + ~NullDescriptorService() override { ++*_destroyed; } + const ServiceDescriptor* GetDescriptor() override { return nullptr; } + void FBCallMethod(const MethodDescriptor*, + google::protobuf::RpcController* controller, + const Message*, Message*, + google::protobuf::Closure* done) override { + brpc::ClosureGuard done_guard(done); + controller->SetFailed("Invalid service was dispatched"); + ADD_FAILURE() << "Invalid service was dispatched"; + } +private: + std::atomic* _destroyed; +}; + +TEST(FlatBuffersLifecycleTest, OwnershipFailureWrongInstanceRemoveAndClear) { + std::atomic owned_destroyed{0}; + std::atomic duplicate_destroyed{0}; + std::atomic invalid_destroyed{0}; + std::atomic borrowed_destroyed{0}; + { + PayloadService borrowed({"brpc_fbtest", "Borrowed", "Echo", {7}}, + &borrowed_destroyed); + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &borrowed, brpc::SERVER_DOESNT_OWN_SERVICE)); + std::unique_ptr owned(new PayloadService( + {"brpc_fbtest", "Owned", "Echo", {41}}, &owned_destroyed)); + ASSERT_EQ(0, server.AddFlatBuffersService( + owned.get(), brpc::SERVER_OWNS_SERVICE)); + owned.release(); + EXPECT_EQ(2, server.GetFlatBuffersServiceCount()); + for (brpc::ServiceOwnership ownership : {brpc::SERVER_OWNS_SERVICE, + brpc::SERVER_DOESNT_OWN_SERVICE}) { + std::unique_ptr duplicate(new PayloadService( + {"brpc_fbtest", "Owned", "Echo", {41}}, &duplicate_destroyed)); + const int before = duplicate_destroyed.load(); + EXPECT_NE(0, server.AddFlatBuffersService(duplicate.get(), ownership)); + EXPECT_EQ(before, duplicate_destroyed.load()); + EXPECT_NE(0, server.RemoveFlatBuffersService(duplicate.get())); + EXPECT_EQ(2, server.GetFlatBuffersServiceCount()); + std::unique_ptr invalid( + new NullDescriptorService(&invalid_destroyed)); + const int invalid_before = invalid_destroyed.load(); + EXPECT_NE(0, server.AddFlatBuffersService(invalid.get(), ownership)); + EXPECT_EQ(invalid_before, invalid_destroyed.load()); + } + EXPECT_EQ(2, duplicate_destroyed.load()); + EXPECT_EQ(2, invalid_destroyed.load()); + EXPECT_NE(0, server.AddFlatBuffersService(nullptr, brpc::SERVER_OWNS_SERVICE)); + EXPECT_NE(0, server.RemoveFlatBuffersService(nullptr)); + ASSERT_EQ(0, server.RemoveFlatBuffersService(&borrowed)); + EXPECT_EQ(0, borrowed_destroyed.load()); + EXPECT_EQ(1, server.GetFlatBuffersServiceCount()); + ASSERT_EQ(0, server.AddFlatBuffersService( + &borrowed, brpc::SERVER_DOESNT_OWN_SERVICE)); + server.ClearServices(); + EXPECT_EQ(0, server.GetFlatBuffersServiceCount()); + EXPECT_EQ(1, owned_destroyed.load()); + EXPECT_EQ(0, borrowed_destroyed.load()); + server.ClearServices(); + EXPECT_EQ(1, owned_destroyed.load()); + } + EXPECT_EQ(1, borrowed_destroyed.load()); +} + +bool WaitForServerConcurrency(const brpc::Server& server, int expected) { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(kWaitTimeoutMs); + do { + if (server.Concurrency() == expected) { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + return server.Concurrency() == expected; +} + +class RejectAllInterceptor : public brpc::Interceptor { +public: + bool Accept(const brpc::Controller* cntl, int& error_code, + std::string& error_text) const override { + ++calls; + EXPECT_EQ(brpc::PROTOCOL_FLATBUFFERS_RPC, cntl->request_protocol()); + EXPECT_NE(nullptr, cntl->flatbuffers_method()); + error_code = EACCES; + error_text = "Rejected by test interceptor"; + return false; + } + mutable std::atomic calls{0}; +}; + +TEST(FlatBuffersLifecycleTest, InterceptorRejectsBeforeServiceAndReleasesConcurrency) { + PayloadService service; + RejectAllInterceptor interceptor; + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions options; + options.interceptor = &interceptor; + options.max_concurrency = 1; + options.method_max_concurrency = 1; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &options)); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + const Message request = MakePayload(); + for (int call = 0; call < 3; ++call) { + brpc::Controller cntl; + Message response; + channel.FBCallMethod(service.GetDescriptor()->method(0), &cntl, + &request, &response, nullptr); + EXPECT_EQ(EACCES, cntl.ErrorCode()) << cntl.ErrorText(); + EXPECT_EQ(call + 1, interceptor.calls.load()); + EXPECT_EQ(0, service.entered.load()); + EXPECT_EQ(0u, response.size()); + EXPECT_TRUE(WaitForServerConcurrency(server, 0)); + } +} + +TEST(FlatBuffersLifecycleTest, PublicControllerSecurityModeMatchesServerOptions) { + for (bool security_mode : {false, true}) { + SCOPED_TRACE(security_mode); + PayloadService service; + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions options; + options.has_builtin_services = !security_mode; + ASSERT_EQ(security_mode, options.security_mode()); + ASSERT_EQ(0, server.Start("127.0.0.1:0", &options)); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + const Message request = MakePayload(); + brpc::Controller cntl; + Message response; + const MethodDescriptor* method = service.GetDescriptor()->method(0); + channel.FBCallMethod(method, &cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ExpectReply(response, request, method); + EXPECT_EQ(1, service.entered.load()); + EXPECT_EQ(security_mode ? 1 : 0, service.security_mode_calls.load()); + } +} + +int StartWithEphemeralInternalPort(brpc::Server* server, + brpc::ServerOptions* options) { + for (int attempt = 0; attempt < 10; ++attempt) { + butil::EndPoint endpoint; + if (butil::str2endpoint("127.0.0.1:0", &endpoint) != 0) { + return -1; + } + butil::fd_guard reserved(butil::tcp_listen(endpoint)); + if (reserved < 0 || butil::get_local_side(reserved, &endpoint) != 0) { + continue; + } + options->internal_port = endpoint.port; + // internal_port does not support 0. Retry if its ephemeral reservation + // is taken between releasing it and starting the two server listeners. + reserved.reset(-1); + if (server->Start("127.0.0.1:0", options) == 0) { + return 0; + } + } + return -1; +} + +TEST(FlatBuffersLifecycleTest, InternalPortRejectsOrdinaryFlatBuffersServices) { + PayloadService service; + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions options; + ASSERT_EQ(0, StartWithEphemeralInternalPort(&server, &options)); + ASSERT_TRUE(server.options().security_mode()); + const butil::EndPoint internal_endpoint(server.listen_address().ip, + options.internal_port); + brpc::Channel internal_channel; + brpc::Channel public_channel; + ASSERT_EQ(0, InitChannel(&internal_channel, internal_endpoint)); + ASSERT_EQ(0, InitChannel(&public_channel, server.listen_address())); + const Message request = MakePayload(); + const MethodDescriptor* method = service.GetDescriptor()->method(0); + brpc::Controller rejected; + Message rejected_response; + internal_channel.FBCallMethod(method, &rejected, &request, + &rejected_response, nullptr); + EXPECT_EQ(EPERM, rejected.ErrorCode()) << rejected.ErrorText(); + EXPECT_EQ(0, service.entered.load()); + EXPECT_EQ(0u, rejected_response.size()); + + brpc::Controller accepted; + Message response; + public_channel.FBCallMethod(method, &accepted, &request, &response, nullptr); + ASSERT_FALSE(accepted.Failed()) << accepted.ErrorText(); + ExpectReply(response, request, method); + EXPECT_EQ(1, service.entered.load()); + EXPECT_EQ(1, service.security_mode_calls.load()); +} + +void ExerciseMethodConcurrency(bool specific_limit) { + PayloadService service; + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &service, brpc::SERVER_DOESNT_OWN_SERVICE)); + const MethodDescriptor* echo = service.GetDescriptor()->FindMethodByIndex(7); + if (specific_limit) { + server.MaxConcurrencyOf(echo->full_name()) = 1; + } + brpc::ServerOptions options; + options.max_concurrency = 8; + options.method_max_concurrency = specific_limit ? 2 : 1; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &options)); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, server.listen_address())); + const Message request = MakePayload(); + for (int id : {7, 41}) { + SCOPED_TRACE(id); + const MethodDescriptor* method = service.GetDescriptor()->FindMethodByIndex(id); + const int limit = specific_limit && id == 41 ? 2 : 1; + const brpc::Server& const_server = server; + EXPECT_EQ(specific_limit && id == 7 ? 1 : 0, + const_server.MaxConcurrencyOf(method->full_name())); + const auto* property = brpc::ServerPrivateAccessor(&server). + FindFlatBuffersMethodPropertyByIndex(method->service()->index(), id); + ASSERT_NE(nullptr, property); + ASSERT_NE(nullptr, property->status); + EXPECT_EQ(limit, property->status->MaxConcurrency()); + const int entered_before = service.entered.load(); + service.HoldNext(); + brpc::Controller held_cntl; + held_cntl.set_timeout_ms(kWaitTimeoutMs); + Message held_response; + Completion held_done; + channel.FBCallMethod(method, &held_cntl, &request, &held_response, &held_done); + const bool held = service.WaitHeld(); + EXPECT_TRUE(held); + // No fatal assertions while a service completion is held: all exits + // below must release it before the server's destructor calls Join(). + if (held) { + EXPECT_TRUE(WaitForServerConcurrency(server, 1)); + for (int attempt = 0; attempt < 2; ++attempt) { + brpc::Controller contender; + Message contender_response; + channel.FBCallMethod(method, &contender, &request, + &contender_response, nullptr); + if (limit == 1) { + EXPECT_EQ(brpc::ELIMIT, contender.ErrorCode()) + << contender.ErrorText(); + EXPECT_EQ(entered_before + 1, service.entered.load()); + EXPECT_EQ(0u, contender_response.size()); + } else { + EXPECT_FALSE(contender.Failed()) << contender.ErrorText(); + if (!contender.Failed()) { + ExpectReply(contender_response, request, method); + } + EXPECT_EQ(entered_before + attempt + 2, service.entered.load()); + } + EXPECT_TRUE(WaitForServerConcurrency(server, 1)); + } + } + service.ReleaseHeld(); + const bool completed = held_done.Wait(); + EXPECT_TRUE(completed); + EXPECT_EQ(1, held_done.calls()); + EXPECT_FALSE(held_cntl.Failed()) << held_cntl.ErrorText(); + if (completed && !held_cntl.Failed()) { + ExpectReply(held_response, request, method); + } + EXPECT_TRUE(WaitForServerConcurrency(server, 0)); + brpc::Controller recovered; + Message recovered_response; + channel.FBCallMethod(method, &recovered, &request, &recovered_response, nullptr); + EXPECT_FALSE(recovered.Failed()) << recovered.ErrorText(); + if (!recovered.Failed()) { + ExpectReply(recovered_response, request, method); + } + EXPECT_TRUE(WaitForServerConcurrency(server, 0)); + } +} + +TEST(FlatBuffersLifecycleTest, DefaultMethodConcurrencyRecoversAfterRejection) { + ExerciseMethodConcurrency(false); +} + +TEST(FlatBuffersLifecycleTest, SpecificMethodConcurrencyOverridesDefaultAndRecovers) { + ExerciseMethodConcurrency(true); +} + +class RetryOnlyAccessDenied : public brpc::RetryPolicy { +public: + bool DoRetry(const brpc::Controller* cntl) const override { + if (cntl->ErrorCode() == EACCES) { + ++retries; + return true; + } + return false; + } + mutable std::atomic retries{0}; +}; + +TEST_F(FlatBuffersProtocolTest, RealNetworkRetryPreservesConstRequestAndAttachment) { + RetryOnlyAccessDenied policy; + brpc::ChannelOptions options; + options.protocol = "fb_rpc"; + options.connection_type = "single"; + options.timeout_ms = kRpcTimeoutMs; + options.connect_timeout_ms = kRpcTimeoutMs; + options.max_retry = 2; + options.retry_policy = &policy; + brpc::Channel retry_channel; + ASSERT_EQ(0, retry_channel.Init(server.listen_address(), &options)); + Message mutable_request = MakePayload(8193, 7654, 19); + memset(mutable_request.mutable_buf_begin(), 0x69, + mutable_request.get_meta_size()); + const Message request = std::move(mutable_request); + const std::string original = Bytes(request); + const size_t prefix_size = request.get_meta_size(); + const std::string prefix(reinterpret_cast(request.data()) - + prefix_size, prefix_size); + const std::string attachment("\0retry\xff", 7); + service.fail_next_code = EACCES; + brpc::Controller cntl; + cntl.request_attachment().append(attachment); + Message response; + Completion done; + retry_channel.FBCallMethod(method(41), &cntl, &request, &response, &done); + ASSERT_TRUE(done.Wait()); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(1, done.calls()); + EXPECT_EQ(1, cntl.retried_count()); + EXPECT_EQ(1, policy.retries.load()); + EXPECT_EQ(2, service.entered.load()); + ExpectReply(response, request, method(41)); + EXPECT_EQ(attachment, cntl.response_attachment().to_string()); + EXPECT_EQ(original, Bytes(request)); + EXPECT_EQ(prefix_size, request.get_meta_size()); + EXPECT_EQ(prefix, std::string(reinterpret_cast(request.data()) - + prefix_size, prefix_size)); + + // The same policy must not retry a different application error. + service.fail_next_code = brpc::EREQUEST; + brpc::Controller no_retry; + Message no_retry_response; + retry_channel.FBCallMethod(method(), &no_retry, &request, + &no_retry_response, nullptr); + EXPECT_EQ(brpc::EREQUEST, no_retry.ErrorCode()); + EXPECT_EQ(0, no_retry.retried_count()); + EXPECT_EQ(1, policy.retries.load()); + EXPECT_EQ(3, service.entered.load()); +} + +TEST_F(FlatBuffersProtocolTest, PooledAndShortConnectionsSupportSyncAndAsyncCalls) { + for (const char* connection_type : {"pooled", "short"}) { + SCOPED_TRACE(connection_type); + brpc::Channel selected; + ASSERT_EQ(0, InitChannel(&selected, server.listen_address(), + "fb_rpc", connection_type)); + for (size_t size : {0u, 8193u}) { + for (bool asynchronous : {false, true}) { + const Message request = MakePayload(size, size + 23, 7); + brpc::Controller cntl; + cntl.request_attachment().append("connection-attachment"); + Message response; + Completion done; + selected.FBCallMethod(method(), &cntl, &request, &response, + asynchronous ? &done : nullptr); + if (asynchronous) { + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + } + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + const brpc::ConnectionType expected_type = + std::string(connection_type) == "pooled" ? + brpc::CONNECTION_TYPE_POOLED : brpc::CONNECTION_TYPE_SHORT; + EXPECT_EQ(expected_type, cntl.connection_type()); + ExpectReply(response, request, method()); + EXPECT_EQ("connection-attachment", + cntl.response_attachment().to_string()); + } + } + } + EXPECT_EQ(8, service.entered.load()); +} + +struct DeferredJoinState { + ~DeferredJoinState() { service.ReleaseHeld(); } + PayloadService service; + Message request = MakePayload(); + Message response; + brpc::Controller cntl; + Completion done; + // Destroy the server before the request/controller/service on early exits. + brpc::Server server; + std::mutex mutex; + std::condition_variable condition; + bool join_started = false; + bool joined = false; + int join_result = -1; +}; + +TEST(FlatBuffersLifecycleTest, StopRejectsRemovalUntilDeferredDoneAndJoin) { + auto state = std::make_shared(); + ASSERT_EQ(0, state->server.AddFlatBuffersService( + &state->service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, state->server.Start("127.0.0.1:0", nullptr)); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, state->server.listen_address())); + const MethodDescriptor* method = state->service.GetDescriptor()->method(0); + state->service.HoldNext(); + state->cntl.set_timeout_ms(kWaitTimeoutMs); + channel.FBCallMethod(method, &state->cntl, &state->request, + &state->response, &state->done); + EXPECT_TRUE(state->service.WaitHeld()); + EXPECT_EQ(0, state->done.calls()); + EXPECT_NE(0, state->server.RemoveFlatBuffersService(&state->service)); + EXPECT_EQ(0, state->server.Stop(0)); + EXPECT_NE(0, state->server.RemoveFlatBuffersService(&state->service)); + EXPECT_EQ(1, state->server.GetFlatBuffersServiceCount()); + + std::thread joiner([state] { + { + std::lock_guard lock(state->mutex); + state->join_started = true; + state->condition.notify_all(); + } + const int result = state->server.Join(); + { + std::lock_guard lock(state->mutex); + state->join_result = result; + state->joined = true; + state->condition.notify_all(); + } + }); + { + std::unique_lock lock(state->mutex); + EXPECT_TRUE(state->condition.wait_for( + lock, std::chrono::milliseconds(kWaitTimeoutMs), + [&state] { return state->join_started; })); + EXPECT_FALSE(state->condition.wait_for( + lock, std::chrono::milliseconds(100), + [&state] { return state->joined; })); + } + EXPECT_EQ(0, state->done.calls()); + state->service.ReleaseHeld(); + const bool completed = state->done.Wait(); + EXPECT_TRUE(completed); + EXPECT_EQ(1, state->done.calls()); + EXPECT_FALSE(state->cntl.Failed()) << state->cntl.ErrorText(); + if (completed && !state->cntl.Failed()) { + ExpectReply(state->response, state->request, method); + } + bool joined; + { + std::unique_lock lock(state->mutex); + joined = state->condition.wait_for( + lock, std::chrono::milliseconds(kWaitTimeoutMs), + [&state] { return state->joined; }); + } + if (!joined) { + // A broken Join must fail rather than hang the suite. The thread owns + // every object it may still access, so detaching cannot use dead locals. + joiner.detach(); + FAIL() << "Server::Join did not return after deferred done completed"; + } + joiner.join(); + EXPECT_EQ(0, state->join_result); + ASSERT_EQ(0, state->server.RemoveFlatBuffersService(&state->service)); + EXPECT_EQ(0, state->server.GetFlatBuffersServiceCount()); +} + +// A normal protobuf service built from descriptors avoids another generated +// test proto while exercising the actual baidu_std serializer and dispatcher. +class DynamicEchoService : public google::protobuf::Service { +public: + DynamicEchoService() { + google::protobuf::FileDescriptorProto file; + file.set_name("flatbuffers_protocol_pb_compat.proto"); + file.set_package("brpc_fbtest_pb"); + auto* message = file.add_message_type(); + message->set_name("Payload"); + auto* field = message->add_field(); + field->set_name("text"); + field->set_number(1); + field->set_type(google::protobuf::FieldDescriptorProto::TYPE_STRING); + field->set_label(google::protobuf::FieldDescriptorProto::LABEL_OPTIONAL); + auto* service = file.add_service(); + service->set_name("EchoService"); + auto* method = service->add_method(); + method->set_name("Echo"); + method->set_input_type(".brpc_fbtest_pb.Payload"); + method->set_output_type(".brpc_fbtest_pb.Payload"); + const auto* built = _pool.BuildFile(file); + if (built) { + _descriptor = built->service(0); + _prototype = _factory.GetPrototype(built->message_type(0)); + } + } + const google::protobuf::ServiceDescriptor* GetDescriptor() override { + return _descriptor; + } + const google::protobuf::Message& GetRequestPrototype( + const google::protobuf::MethodDescriptor*) const override { + return *_prototype; + } + const google::protobuf::Message& GetResponsePrototype( + const google::protobuf::MethodDescriptor*) const override { + return *_prototype; + } + void CallMethod(const google::protobuf::MethodDescriptor*, + google::protobuf::RpcController* controller, + const google::protobuf::Message* request, + google::protobuf::Message* response, + google::protobuf::Closure* done) override { + brpc::ClosureGuard done_guard(done); + auto* cntl = static_cast(controller); + EXPECT_EQ(nullptr, cntl->flatbuffers_method()); + response->CopyFrom(*request); + } +private: + google::protobuf::DescriptorPool _pool; + google::protobuf::DynamicMessageFactory _factory; + const google::protobuf::ServiceDescriptor* _descriptor = nullptr; + const google::protobuf::Message* _prototype = nullptr; +}; + +TEST(FlatBuffersLifecycleTest, ControllerResetClearsContextBeforeOrdinaryProtobufRpc) { + PayloadService flatbuffers_service; + DynamicEchoService protobuf_service; + ASSERT_NE(nullptr, protobuf_service.GetDescriptor()); + brpc::Server server; + ASSERT_EQ(0, server.AddFlatBuffersService( + &flatbuffers_service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.AddService(&protobuf_service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr)); + brpc::Channel fb_channel; + brpc::Channel pb_channel; + ASSERT_EQ(0, InitChannel(&fb_channel, server.listen_address())); + ASSERT_EQ(0, InitChannel(&pb_channel, server.listen_address(), "baidu_std")); + const MethodDescriptor* fb_method = flatbuffers_service.GetDescriptor()->method(0); + const Message fb_request = MakePayload(); + Message fb_response; + brpc::Controller cntl; + fb_channel.FBCallMethod(fb_method, &cntl, &fb_request, &fb_response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(fb_method, cntl.flatbuffers_method()); + cntl.Reset(); + EXPECT_EQ(nullptr, cntl.flatbuffers_method()); + EXPECT_EQ(nullptr, cntl.method()); + const auto* method = protobuf_service.GetDescriptor()->method(0); + std::unique_ptr request( + protobuf_service.GetRequestPrototype(method).New()); + std::unique_ptr response( + protobuf_service.GetResponsePrototype(method).New()); + const auto* field = request->GetDescriptor()->FindFieldByName("text"); + ASSERT_NE(nullptr, field); + request->GetReflection()->SetString(request.get(), field, "ordinary protobuf"); + pb_channel.CallMethod(method, &cntl, request.get(), response.get(), nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(nullptr, cntl.flatbuffers_method()); + EXPECT_EQ(method, cntl.method()); + EXPECT_EQ("ordinary protobuf", + response->GetReflection()->GetString(*response, field)); + EXPECT_EQ(0, server.Stop(0)); + EXPECT_EQ(0, server.Join()); +} + +TEST(FlatBuffersLifecycleTest, AuthenticatedProtobufSocketCannotBypassFlatBuffersAuthCheck) { + PayloadService flatbuffers_service; + DynamicEchoService protobuf_service; + RejectingAuthenticator auth; + brpc::Server server; + ASSERT_NE(nullptr, protobuf_service.GetDescriptor()); + ASSERT_EQ(0, server.AddFlatBuffersService( + &flatbuffers_service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.AddService(&protobuf_service, brpc::SERVER_DOESNT_OWN_SERVICE)); + ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr)); + brpc::ChannelOptions options; + options.protocol = "baidu_std"; + options.connection_type = "single"; + options.auth = &auth; + options.timeout_ms = kRpcTimeoutMs; + options.connect_timeout_ms = kRpcTimeoutMs; + options.max_retry = 0; + brpc::Channel pb_channel; + brpc::Channel fb_channel; + ASSERT_EQ(0, pb_channel.Init(server.listen_address(), &options)); + // The socket pool key includes the auth pointer, but not the protocol. + options.protocol = "fb_rpc"; + ASSERT_EQ(0, fb_channel.Init(server.listen_address(), &options)); + const auto* pb_method = protobuf_service.GetDescriptor()->method(0); + std::unique_ptr pb_request( + protobuf_service.GetRequestPrototype(pb_method).New()); + std::unique_ptr pb_response( + protobuf_service.GetResponsePrototype(pb_method).New()); + const auto* field = pb_request->GetDescriptor()->FindFieldByName("text"); + ASSERT_NE(nullptr, field); + pb_request->GetReflection()->SetString(pb_request.get(), field, "prime auth socket"); + brpc::Controller first_pb; + pb_channel.CallMethod(pb_method, &first_pb, pb_request.get(), + pb_response.get(), nullptr); + ASSERT_FALSE(first_pb.Failed()) << first_pb.ErrorText(); + ASSERT_EQ(1, auth.generated.load()); + ASSERT_GT(first_pb.local_side().port, 0); + const butil::EndPoint first_connection = first_pb.local_side(); + brpc::ServerStatistics statistics; + server.GetStat(&statistics); + ASSERT_EQ(1u, statistics.connection_count); + + const Message request = MakePayload(); + Message response; + brpc::Controller rejected; + Completion done; + fb_channel.FBCallMethod(flatbuffers_service.GetDescriptor()->method(0), + &rejected, &request, &response, &done); + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + EXPECT_TRUE(rejected.Failed()) + << "Previously authenticated socket bypassed FRPC auth check"; + EXPECT_EQ(0u, response.size()); + EXPECT_EQ(0, flatbuffers_service.entered.load()); + EXPECT_EQ(1, auth.generated.load()); + + // Verify the primed connection stayed usable and did not authenticate again. + brpc::Controller second_pb; + pb_response->Clear(); + pb_channel.CallMethod(pb_method, &second_pb, pb_request.get(), + pb_response.get(), nullptr); + ASSERT_FALSE(second_pb.Failed()) << second_pb.ErrorText(); + EXPECT_EQ(first_connection, second_pb.local_side()); + EXPECT_EQ("prime auth socket", + pb_response->GetReflection()->GetString(*pb_response, field)); + EXPECT_EQ(1, auth.generated.load()); + server.GetStat(&statistics); + EXPECT_EQ(1u, statistics.connection_count); +} + +// Raw peers use both an absolute deadline and socket timeouts; a regression +// must fail a test instead of leaving accept/read/write blocked indefinitely. +bool ConfigureSocket(int fd) { + struct timeval timeout = {kRpcTimeoutMs / 1000, + (kRpcTimeoutMs % 1000) * 1000}; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0) { + return false; + } +#ifdef SO_NOSIGPIPE + const int enabled = 1; + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled)) != 0) { + return false; + } +#endif + return true; +} + +bool WaitFd(int fd, short events, + const std::chrono::steady_clock::time_point& deadline) { + while (true) { + const auto remaining = std::chrono::duration_cast( + deadline - std::chrono::steady_clock::now()).count(); + if (remaining <= 0) { + return false; + } + struct pollfd descriptor = {fd, events, 0}; + const int rc = poll(&descriptor, 1, static_cast(remaining)); + if (rc > 0) { + return (descriptor.revents & (events | POLLHUP | POLLERR)) != 0; + } + if (rc == 0 || errno != EINTR) { + return false; + } + } +} + +bool ReadExactly(int fd, char* bytes, size_t size) { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(kWaitTimeoutMs); + size_t offset = 0; + while (offset < size && WaitFd(fd, POLLIN, deadline)) { + const ssize_t count = recv(fd, bytes + offset, size - offset, 0); + if (count > 0) { + offset += count; + } else if (count == 0 || errno != EINTR) { + return false; + } + } + return offset == size; +} + +bool WriteExactly(int fd, const std::string& bytes) { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(kWaitTimeoutMs); + size_t offset = 0; + while (offset < bytes.size() && WaitFd(fd, POLLOUT, deadline)) { +#ifdef MSG_NOSIGNAL + const int flags = MSG_NOSIGNAL; +#else + const int flags = 0; +#endif + const ssize_t count = send(fd, bytes.data() + offset, + bytes.size() - offset, flags); + if (count > 0) { + offset += count; + } else if (count == 0 || errno != EINTR) { + return false; + } + } + return offset == bytes.size(); +} + +bool ReadFrame(int fd, std::string* meta, std::string* payload) { + std::string header(kHeaderSize, '\0'); + if (!ReadExactly(fd, &header[0], header.size()) || + header.compare(0, 4, "FRPC") != 0) { + return false; + } + const uint32_t body_size = Get32(header, 4, true); + const uint32_t meta_size = Get32(header, 8, true); + if (meta_size > body_size || body_size > 1024 * 1024) { + return false; + } + std::string body(body_size, '\0'); + if (body_size && !ReadExactly(fd, &body[0], body.size())) { + return false; + } + *meta = body.substr(0, meta_size); + *payload = body.substr(meta_size); + return true; +} + +TEST_F(FlatBuffersProtocolTest, ExtendedRequestMetadataTraversesRealSocket) { + const Message request = MakePayload(8193, 7123, 17); + const std::string attachment("\0binary\xff", 8); + std::string meta = RequestMeta(method(41), request.size(), attachment.size(), + 0x1122334455667788ULL); + meta.append("unknown future fields", 21); + butil::fd_guard fd(butil::tcp_connect(server.listen_address(), nullptr, + kRpcTimeoutMs)); + ASSERT_GE(static_cast(fd), 0); + ASSERT_TRUE(ConfigureSocket(fd)); + const std::string wire = Frame(meta, Bytes(request) + attachment); + ASSERT_TRUE(WriteExactly(fd, wire.substr(0, 5))); + ASSERT_TRUE(WriteExactly(fd, wire.substr(5, 13))); + ASSERT_TRUE(WriteExactly(fd, wire.substr(18))); + std::string reply_meta; + std::string reply_payload; + ASSERT_TRUE(ReadFrame(fd, &reply_meta, &reply_payload)); + ASSERT_GE(reply_meta.size(), kResponseMetaSize); + EXPECT_EQ(0u, Get32(reply_meta, 0)); + EXPECT_EQ(0x1122334455667788ULL, Get64(reply_meta, 12)); + const uint32_t message_size = Get32(reply_meta, 4); + ASSERT_EQ(reply_payload.size(), message_size + attachment.size()); + EXPECT_EQ(attachment.size(), Get32(reply_meta, 8)); + EXPECT_EQ(attachment, reply_payload.substr(message_size)); + butil::IOBuf body; + body.append(reply_payload.data(), message_size); + Message response; + ASSERT_TRUE(response.parse_msg_from_iobuf(body, message_size, 0)); + ExpectReply(response, request, method(41)); +} + +TEST_F(FlatBuffersProtocolTest, MalformedRequestLengthsFailBeforeServiceDispatch) { + const Message request = MakePayload(); + const std::string payload = Bytes(request) + "att"; + for (int variant = 0; variant < 6; ++variant) { + SCOPED_TRACE(variant); + std::string meta = RequestMeta(method(), request.size(), 3, 991 + variant); + switch (variant) { + case 0: Put32(&meta, 8, 0); break; + case 1: Put32(&meta, 8, 0xffffffffu); break; + case 2: Put32(&meta, 12, 0xffffffffu); break; + case 3: Put32(&meta, 8, request.size() + 1); break; + case 4: Put32(&meta, 12, 2); break; + case 5: Put32(&meta, 4, 0xffffffffu); break; + } + butil::fd_guard fd(butil::tcp_connect(server.listen_address(), nullptr, + kRpcTimeoutMs)); + ASSERT_GE(static_cast(fd), 0); + ASSERT_TRUE(ConfigureSocket(fd)); + ASSERT_TRUE(WriteExactly(fd, Frame(meta, payload))); + std::string reply_meta; + std::string reply_payload; + ASSERT_TRUE(ReadFrame(fd, &reply_meta, &reply_payload)); + ASSERT_GE(reply_meta.size(), kResponseMetaSize); + EXPECT_EQ(static_cast(variant == 5 ? brpc::ENOMETHOD : + brpc::EREQUEST), + Get32(reply_meta, 0)); + EXPECT_EQ(static_cast(991 + variant), Get64(reply_meta, 12)); + EXPECT_EQ(0u, Get32(reply_meta, 4)); + EXPECT_EQ(0u, Get32(reply_meta, 8)); + EXPECT_TRUE(reply_payload.empty()); + } + EXPECT_EQ(0, service.entered.load()); + brpc::Controller cntl; + Message response; + channel.FBCallMethod(method(), &cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ExpectReply(response, request, method()); +} + +TEST_F(FlatBuffersProtocolTest, TruncatedMetadataAndImpossibleHeaderCloseConnection) { + std::vector frames; + for (size_t size : {0u, 1u, 8u, 19u, 23u}) { + frames.push_back(Frame(std::string(size, '\xff'), "invalid")); + } + frames.push_back(Header(0, 24)); + const Message warmup_request = MakePayload(); + int warmups = 0; + for (const auto& wire : frames) { + SCOPED_TRACE(wire.size()); + butil::fd_guard fd(butil::tcp_connect(server.listen_address(), nullptr, + kRpcTimeoutMs)); + ASSERT_GE(static_cast(fd), 0); + ASSERT_TRUE(ConfigureSocket(fd)); + // NSHEAD precedes FRPC during initial multi-protocol discovery and + // needs 28 bytes. First establish FRPC on this exact connection. + const uint64_t correlation_id = 9001 + warmups; + ASSERT_TRUE(WriteExactly(fd, Frame( + RequestMeta(method(), warmup_request.size(), 0, correlation_id), + Bytes(warmup_request)))); + std::string reply_meta; + std::string reply_payload; + ASSERT_TRUE(ReadFrame(fd, &reply_meta, &reply_payload)); + ASSERT_GE(reply_meta.size(), kResponseMetaSize); + ASSERT_EQ(0u, Get32(reply_meta, 0)); + EXPECT_EQ(correlation_id, Get64(reply_meta, 12)); + ASSERT_EQ(reply_payload.size(), Get32(reply_meta, 4)); + EXPECT_EQ(0u, Get32(reply_meta, 8)); + butil::IOBuf reply_bytes; + reply_bytes.append(reply_payload); + Message warmup_response; + ASSERT_TRUE(warmup_response.parse_msg_from_iobuf( + reply_bytes, reply_payload.size(), 0)); + ExpectReply(warmup_response, warmup_request, method()); + EXPECT_EQ(++warmups, service.entered.load()); + + ASSERT_TRUE(WriteExactly(fd, wire)); + char byte; + ssize_t count; + do { + count = recv(fd, &byte, 1, 0); + } while (count < 0 && errno == EINTR); + // A receive timeout is not evidence that the server rejected the frame. + EXPECT_TRUE(count == 0 || (count < 0 && errno == ECONNRESET)) + << "recv=" << count << " errno=" << errno; + EXPECT_EQ(warmups, service.entered.load()); + } + EXPECT_EQ(static_cast(frames.size()), service.entered.load()); +} + +class ScriptedPeer { +public: + using Reply = std::function; + bool Start(Reply reply) { + butil::EndPoint endpoint; + if (butil::str2endpoint("127.0.0.1:0", &endpoint) != 0) { + return false; + } + _listener.reset(butil::tcp_listen(endpoint)); + if (_listener < 0 || butil::get_local_side(_listener, &_endpoint) != 0) { + return false; + } + _thread = std::thread([this, reply] { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(kWaitTimeoutMs); + if (!WaitFd(_listener, POLLIN, deadline)) { + return; + } + butil::fd_guard connection(accept(_listener, nullptr, nullptr)); + if (connection < 0 || !ConfigureSocket(connection)) { + return; + } + std::string request_meta; + std::string request_payload; + if (!ReadFrame(connection, &request_meta, &request_payload) || + request_meta.size() < kRequestMetaSize) { + return; + } + _served = WriteExactly(connection, reply(request_meta, request_payload)); + // Do not let EOF accidentally turn an accepted malformed response + // into an RPC failure. Keep the connection open until the assertion. + std::unique_lock lock(_mutex); + _condition.wait_for(lock, std::chrono::milliseconds(kWaitTimeoutMs), + [this] { return _release; }); + }); + return true; + } + ~ScriptedPeer() { Join(); } + void Join() { + { + std::lock_guard lock(_mutex); + _release = true; + _condition.notify_all(); + } + if (_thread.joinable()) { + _thread.join(); + } + } + butil::EndPoint endpoint() const { return _endpoint; } + bool served() const { return _served.load(); } +private: + butil::fd_guard _listener; + butil::EndPoint _endpoint; + std::thread _thread; + std::atomic _served{false}; + std::mutex _mutex; + std::condition_variable _condition; + bool _release = false; +}; + +TEST(FlatBuffersResponseTest, ExtendedResponseMetadataUsesDeclaredPayloadOffset) { + ScriptedPeer peer; + ASSERT_TRUE(peer.Start([](const std::string& request_meta, + const std::string& request_payload) { + const uint32_t message_size = Get32(request_meta, 8); + const uint32_t attachment_size = Get32(request_meta, 12); + std::string meta = ResponseMeta(message_size, attachment_size, + Get64(request_meta, 16)); + meta.append("\xff\x01\0\x7f\x03", 5); + return Frame(meta, request_payload); + })); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, peer.endpoint())); + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "Peer", "Echo", {7}})); + const Message request = MakePayload(8193); + Message response; + brpc::Controller cntl; + cntl.request_attachment().append("\0tail", 5); + channel.FBCallMethod(descriptor.method(0), &cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_TRUE(response.Verify()); + EXPECT_EQ(Bytes(request), Bytes(response)); + EXPECT_EQ(std::string("\0tail", 5), cntl.response_attachment().to_string()); + peer.Join(); + EXPECT_TRUE(peer.served()); +} + +TEST(FlatBuffersResponseTest, MalformedResponseFramesFailRpcNotMerelySchemaVerification) { + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "Peer", "Echo", {41}})); + const Message request = MakePayload(); + for (int variant = 0; variant < 12; ++variant) { + SCOPED_TRACE(variant); + ScriptedPeer peer; + ASSERT_TRUE(peer.Start([variant](const std::string& request_meta, + const std::string& request_payload) { + std::string meta = ResponseMeta(Get32(request_meta, 8), 0, + Get64(request_meta, 16)); + std::string payload = request_payload; + switch (variant) { + case 0: meta.clear(); break; + case 1: meta.resize(1); break; + case 2: meta.resize(19); break; + case 3: Put32(&meta, 4, 0xffffffffu); break; + case 4: Put32(&meta, 8, 0xffffffffu); break; + case 5: Put32(&meta, 4, payload.size() + 1); break; + case 6: Put32(&meta, 8, 1); break; + case 7: payload.clear(); Put32(&meta, 4, 0); break; + case 8: return Header(0, 20); + case 9: return Header(0xffffffffu, 20); + case 10: Put64(&meta, 12, Get64(request_meta, 16) + (1ULL << 32)); break; + case 11: { + std::string wire = Frame(meta, payload); + wire.resize(wire.size() - 1); + return wire; + } + } + return Frame(meta, payload); + })); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, peer.endpoint())); + Message response; + brpc::Controller cntl; + Completion done; + channel.FBCallMethod(descriptor.method(0), &cntl, &request, &response, &done); + ASSERT_TRUE(done.Wait()); + EXPECT_EQ(1, done.calls()); + EXPECT_TRUE(cntl.Failed()) << "Malformed frame reported RPC success"; + if (variant >= 3 && variant <= 7) { + EXPECT_EQ(brpc::ERESPONSE, cntl.ErrorCode()) << cntl.ErrorText(); + } + EXPECT_EQ(0u, response.size()); + peer.Join(); + EXPECT_TRUE(peer.served()); + } +} + +TEST(FlatBuffersResponseTest, SchemaVerificationIsExplicitlyTheCallersResponsibility) { + ScriptedPeer peer; + ASSERT_TRUE(peer.Start([](const std::string& request_meta, const std::string&) { + const std::string invalid(16, '\xff'); + return Frame(ResponseMeta(invalid.size(), 0, Get64(request_meta, 16)), + invalid); + })); + brpc::Channel channel; + ASSERT_EQ(0, InitChannel(&channel, peer.endpoint())); + ServiceDescriptor descriptor; + ASSERT_EQ(0, descriptor.init({"brpc_fbtest", "Peer", "Echo", {7}})); + const Message request = MakePayload(); + Message response; + brpc::Controller cntl; + channel.FBCallMethod(descriptor.method(0), &cntl, &request, &response, nullptr); + // The wire sizes are valid; no typed schema is known by the FRPC transport. + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_FALSE(response.Verify()); + peer.Join(); + EXPECT_TRUE(peer.served()); +} + +} // namespace +#endif // BRPC_WITH_FLATBUFFERS diff --git a/test/brpc_flatbuffers_unittest.cpp b/test/brpc_flatbuffers_unittest.cpp new file mode 100644 index 0000000000..e4defbc369 --- /dev/null +++ b/test/brpc_flatbuffers_unittest.cpp @@ -0,0 +1,528 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "butil/config.h" +#if BRPC_WITH_FLATBUFFERS +#include +#include +#include +#include +#include +#include +#include +#include +#include "brpc/flatbuffers/message.h" +#include "brpc/flatbuffers/service.h" +#include "flatbuffers_message_generated.h" + +#if !BRPC_WITH_GLOG +namespace logging { +DECLARE_bool(crash_on_fatal_log); +} +#endif + +namespace { +using brpc::flatbuffers::Message; +using brpc::flatbuffers::MessageBuilder; +using brpc::flatbuffers::SlabAllocator; +using brpc::flatbuffers::ServiceDescriptor; +using brpc::flatbuffers::BrpcDescriptorTable; +using brpc_fbtest::Payload; + +void FinishPayload(::flatbuffers::FlatBufferBuilder& builder, + const std::string& text, int64_t value = 42) { + auto str = builder.CreateString(text); + std::vector numbers = {1, -2, 3, 4000}; + auto values = builder.CreateVector(numbers); + builder.Finish(brpc_fbtest::CreatePayload(builder, value, str, values)); +} + +Message MakeMessage(size_t length = 16, int64_t value = 42) { + MessageBuilder builder(8); + FinishPayload(builder, std::string(length, 'x'), value); + return builder.ReleaseMessage(); +} + +void ExpectPayload(const Message& msg, size_t length, int64_t value = 42) { + ASSERT_TRUE(msg.Verify()); + const Payload* root = msg.GetRoot(); + ASSERT_NE(nullptr, root); + EXPECT_EQ(value, root->value()); + ASSERT_NE(nullptr, root->message()); + EXPECT_EQ(std::string(length, 'x'), root->message()->str()); + ASSERT_NE(nullptr, root->values()); + ASSERT_EQ(4u, root->values()->size()); + EXPECT_EQ(-2, root->values()->Get(1)); +} + +TEST(FlatbuffersTest, EmptyAndClear) { + Message msg; + EXPECT_EQ(nullptr, msg.data()); + EXPECT_EQ(nullptr, msg.GetRoot()); + EXPECT_EQ(nullptr, msg.GetMutableRoot()); + EXPECT_EQ(nullptr, msg.reduce_meta_size_and_get_buf(0)); + EXPECT_FALSE(msg.Verify()); + EXPECT_EQ(0u, msg.size()); + butil::IOBuf wire; + EXPECT_FALSE(brpc::flatbuffers::SerializeFbToIOBUF(&msg, wire)); + EXPECT_FALSE(brpc::flatbuffers::SerializeFbToIOBUF(nullptr, wire)); + EXPECT_FALSE(brpc::flatbuffers::ParseFbFromIOBUF(nullptr, 0, wire)); + msg = MakeMessage(); + msg.Clear(); + msg.Clear(); + EXPECT_EQ(nullptr, msg.data()); + EXPECT_EQ(0u, msg.get_meta_size()); + EXPECT_EQ(0u, msg.size()); +} + +TEST(FlatbuffersTest, ReleaseLifetimeAndGrowth) { + for (size_t length : {0u, 16u, 63u, 1024u, 8192u, 32768u}) { + SCOPED_TRACE(length); + Message msg; + const uint8_t* payload = nullptr; + { + MessageBuilder builder(8); + FinishPayload(builder, std::string(length, 'x')); + payload = builder.GetBufferPointer(); + msg = builder.ReleaseMessage(); + EXPECT_EQ(payload, msg.data()); + EXPECT_EQ(0u, builder.GetSize()); + FinishPayload(builder, "replacement"); + } + ExpectPayload(msg, length); + EXPECT_EQ(std::string(brpc::flatbuffers::kDefaultMetaSize, '\0'), + std::string(static_cast(msg.mutable_buf_begin()), + msg.get_meta_size())); + } +} + +TEST(FlatbuffersTest, MessageMoveAndSwap) { + Message first = MakeMessage(16, 1); + const uint8_t* ptr = first.data(); + Message second(std::move(first)); + EXPECT_EQ(ptr, second.data()); + EXPECT_EQ(nullptr, first.data()); + EXPECT_EQ(0u, first.size()); + EXPECT_EQ(0u, first.get_meta_size()); + EXPECT_FALSE(first.Verify()); + first = MakeMessage(8192, 2); + second = std::move(first); + EXPECT_EQ(nullptr, first.data()); + ExpectPayload(second, 8192, 2); + Message* alias = &second; + second = std::move(*alias); + ExpectPayload(second, 8192, 2); + first = MakeMessage(16, 3); + first.Swap(second); + ExpectPayload(first, 8192, 2); + ExpectPayload(second, 16, 3); + Message empty; + second = std::move(empty); + EXPECT_EQ(nullptr, second.data()); + EXPECT_EQ(nullptr, empty.data()); +} + +TEST(FlatbuffersTest, BuilderMoveAndReuse) { + MessageBuilder first(8); + auto str = first.CreateString(std::string(8192, 'x')); + MessageBuilder second(std::move(first)); + second.Finish(brpc_fbtest::CreatePayload(second, 8, str)); + Message msg = second.ReleaseMessage(); + ASSERT_TRUE(msg.Verify()); + EXPECT_EQ(8192u, msg.GetRoot()->message()->size()); + FinishPayload(first, std::string(16, 'x')); + ExpectPayload(first.ReleaseMessage(), 16); + FinishPayload(second, std::string(63, 'x')); + first = std::move(second); + MessageBuilder* alias = &first; + first = std::move(*alias); + ExpectPayload(first.ReleaseMessage(), 63); + FinishPayload(second, std::string(1024, 'x')); + ExpectPayload(second.ReleaseMessage(), 1024); +} + +TEST(FlatbuffersTest, BuilderSwapFinishedAndUnfinished) { + MessageBuilder first; + FinishPayload(first, std::string(16, 'x')); + MessageBuilder second; + auto str = second.CreateString(std::string(63, 'x')); + first.Swap(second); + ExpectPayload(second.ReleaseMessage(), 16); + first.Finish(brpc_fbtest::CreatePayload(first, 9, str)); + Message msg = first.ReleaseMessage(); + ASSERT_TRUE(msg.Verify()); + EXPECT_EQ(9, msg.GetRoot()->value()); + EXPECT_EQ(63u, msg.GetRoot()->message()->size()); +} + +TEST(FlatbuffersTest, SharedStringsAfterMoveAndImport) { + MessageBuilder first(8); + auto old = first.CreateSharedString("shared"); + MessageBuilder second(std::move(first)); + auto same = second.CreateSharedString("shared"); + auto other = second.CreateSharedString("other"); + std::vector<::flatbuffers::Offset<::flatbuffers::String> > strings = { + old, same, other}; + second.Finish(second.CreateVector(strings)); + Message msg = second.ReleaseMessage(); + auto root = ::flatbuffers::GetRoot< + ::flatbuffers::Vector<::flatbuffers::Offset<::flatbuffers::String> > >( + msg.data()); + EXPECT_EQ("shared", root->Get(0)->str()); + EXPECT_EQ("shared", root->Get(1)->str()); + EXPECT_EQ("other", root->Get(2)->str()); + ::flatbuffers::FlatBufferBuilder foreign; + auto text = foreign.CreateSharedString("shared"); + MessageBuilder imported(std::move(foreign)); + imported.CreateSharedString("after import"); + imported.Finish(brpc_fbtest::CreatePayload(imported, 1, text)); + ASSERT_TRUE(imported.ReleaseMessage().Verify()); +} + +class CountingAllocator : public ::flatbuffers::Allocator { +public: + CountingAllocator(int* allocations, int* frees, int* destructors) + : _allocations(allocations), _frees(frees), _destructors(destructors) {} + ~CountingAllocator() override { ++*_destructors; } + uint8_t* allocate(size_t n) override { + ++*_allocations; + return new uint8_t[n]; + } + void deallocate(uint8_t* p, size_t) override { + ++*_frees; + delete[] p; + } +private: + int* _allocations; + int* _frees; + int* _destructors; +}; + +TEST(FlatbuffersTest, ImportForeignAllocatorAndScratch) { + int allocations = 0; + int frees = 0; + int destructors = 0; + Message msg; + { + ::flatbuffers::FlatBufferBuilder foreign(8, + new CountingAllocator(&allocations, &frees, &destructors), true); + auto text = foreign.CreateString(std::string(8192, 'x')); + const auto table = foreign.StartTable(); + foreign.AddOffset(Payload::VT_MESSAGE, text); + foreign.AddElement(Payload::VT_VALUE, 123, 0); + MessageBuilder imported(std::move(foreign)); + EXPECT_EQ(allocations, frees); + EXPECT_EQ(1, destructors); + imported.Finish(::flatbuffers::Offset(imported.EndTable(table))); + msg = imported.ReleaseMessage(); + FinishPayload(foreign, "reuse source"); + } + ASSERT_TRUE(msg.Verify()); + EXPECT_EQ(123, msg.GetRoot()->value()); + EXPECT_EQ(8192u, msg.GetRoot()->message()->size()); + EXPECT_EQ(allocations, frees); + EXPECT_EQ(1, destructors); +} + +TEST(FlatbuffersTest, ImportBorrowedAllocatorDoesNotOwnAllocator) { + int allocations = 0; + int frees = 0; + int destructors = 0; + Message msg; + { + CountingAllocator allocator(&allocations, &frees, &destructors); + ::flatbuffers::FlatBufferBuilder foreign(8, &allocator, false); + FinishPayload(foreign, std::string(1024, 'x')); + MessageBuilder imported(std::move(foreign)); + EXPECT_EQ(allocations, frees); + EXPECT_EQ(0, destructors); + msg = imported.ReleaseMessage(); + } + EXPECT_EQ(1, destructors); + ExpectPayload(msg, 1024); +} + +TEST(FlatbuffersTest, ImportFinishedDefaultBuilderAndEmpty) { + ::flatbuffers::FlatBufferBuilder foreign(8); + FinishPayload(foreign, std::string(8192, 'x')); + MessageBuilder imported(std::move(foreign)); + ExpectPayload(imported.ReleaseMessage(), 8192); + ::flatbuffers::FlatBufferBuilder empty; + imported = std::move(empty); + FinishPayload(imported, std::string(16, 'x')); + ExpectPayload(imported.ReleaseMessage(), 16); + FinishPayload(imported, std::string(63, 'x')); + ::flatbuffers::FlatBufferBuilder& alias = imported; + imported = std::move(alias); + ExpectPayload(imported.ReleaseMessage(), 63); +} + +TEST(FlatbuffersTest, AllocatorFrontBackAndMove) { + SlabAllocator first; + uint8_t* data = first.allocate(64); + EXPECT_EQ(0u, reinterpret_cast(data) % + brpc::flatbuffers::kBufferAlignment); + memset(data, 'f', 16); + memset(data + 48, 'b', 16); + SlabAllocator second(std::move(first)); + size_t previous = 64; + for (size_t next : {128u, 512u, 8192u, 16384u}) { + data = second.reallocate_downward(data, previous, next, 16, 16); + EXPECT_EQ(std::string(16, 'f'), std::string((char*)data, 16)); + EXPECT_EQ(std::string(16, 'b'), std::string((char*)data + next - 16, 16)); + previous = next; + } + first = std::move(second); + SlabAllocator* alias = &first; + first = std::move(*alias); + first.deallocate(data, 16384); + second.deallocate(nullptr, 0); + data = second.allocate(32); + second.deallocate(data, 32); +} + +TEST(FlatbuffersTest, SerializeConstAndRetainWireStorage) { + butil::IOBuf wire; + { + const Message msg = MakeMessage(8192); + ASSERT_TRUE(brpc::flatbuffers::SerializeFbToIOBUF(&msg, wire)); + EXPECT_EQ(msg.size() + msg.get_meta_size(), wire.size()); + } + Message parsed; + const size_t meta = brpc::flatbuffers::kDefaultMetaSize; + ASSERT_TRUE(brpc::flatbuffers::ParseFbFromIOBUF( + &parsed, wire.size() - meta, wire, meta)); + wire.clear(); + ExpectPayload(parsed, 8192); +} + +TEST(FlatbuffersTest, ParseAlignedStorageIsSharedAndRetained) { + Message source = MakeMessage(); + void* raw = nullptr; + ASSERT_EQ(0, posix_memalign(&raw, brpc::flatbuffers::kBufferAlignment, + source.size())); + memcpy(raw, source.data(), source.size()); + int frees = 0; + butil::IOBuf wire; + ASSERT_EQ(0, wire.append_user_data(raw, source.size(), [&frees](void* p) { + ++frees; + free(p); + })); + Message parsed; + ASSERT_TRUE(parsed.parse_msg_from_iobuf(wire, source.size(), 0)); + EXPECT_EQ(raw, parsed.data()); + wire.clear(); + EXPECT_EQ(0, frees); + ExpectPayload(parsed, 16); + parsed.Clear(); + EXPECT_EQ(1, frees); +} + +TEST(FlatbuffersTest, ParseFragmentedUnalignedAndReplace) { + for (size_t length : {16u, 8192u, 32768u}) { + Message source = MakeMessage(length); + butil::IOBuf wire; + const size_t split = source.size() / 2; + void* first = malloc(split); + void* second = malloc(source.size() - split); + memcpy(first, source.data(), split); + memcpy(second, source.data() + split, source.size() - split); + ASSERT_EQ(0, wire.append_user_data(first, split, free)); + ASSERT_EQ(0, wire.append_user_data(second, source.size() - split, free)); + ASSERT_EQ(2u, wire.backing_block_num()); + Message parsed = MakeMessage(1); + ASSERT_TRUE(parsed.parse_msg_from_iobuf(wire, source.size(), 0)); + ASSERT_TRUE(parsed.parse_msg_from_iobuf(wire, source.size(), 0)); + wire.clear(); + ExpectPayload(parsed, length); + EXPECT_EQ(0u, reinterpret_cast(parsed.data()) % + brpc::flatbuffers::kBufferAlignment); + + char* bytes = static_cast(malloc(source.size() + 1)); + bytes[0] = 'h'; + memcpy(bytes + 1, source.data(), source.size()); + ASSERT_EQ(0, wire.append_user_data(bytes, source.size() + 1, free)); + ASSERT_TRUE(parsed.parse_msg_from_iobuf(wire, source.size(), 1)); + EXPECT_NE(bytes + 1, reinterpret_cast(parsed.data())); + wire.clear(); + ExpectPayload(parsed, length); + EXPECT_EQ(1u, parsed.get_meta_size()); + } +} + +TEST(FlatbuffersTest, ParseRejectsBadFramingWithoutChangingMessage) { + Message msg = MakeMessage(16); + const uint8_t* data = msg.data(); + butil::IOBuf buf; + buf.append("abcd"); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, 0, 4)); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, 4, 1)); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, 5, 0)); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, 1, 5)); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, std::numeric_limits::max(), 5)); + EXPECT_FALSE(msg.parse_msg_from_iobuf(buf, 5, std::numeric_limits::max())); + EXPECT_EQ(data, msg.data()); + ExpectPayload(msg, 16); +} + +TEST(FlatbuffersTest, InvalidPayloadAndOptionalString) { + Message msg; + for (size_t size : {1u, 3u, 4u, 16u, 64u}) { + butil::IOBuf buf; + buf.append(std::string(size, '\xff')); + ASSERT_TRUE(msg.parse_msg_from_iobuf(buf, size, 0)); + EXPECT_FALSE(msg.Verify()); + } + MessageBuilder builder; + builder.Finish(brpc_fbtest::CreatePayload(builder)); + msg = builder.ReleaseMessage(); + ASSERT_TRUE(msg.Verify()); + EXPECT_EQ(nullptr, msg.GetRoot()->message()); + // The schema allows an absent string; callers must not dereference it. + msg = MakeMessage(); + memset(msg.mutable_data(), 0xff, sizeof(::flatbuffers::uoffset_t)); + EXPECT_FALSE(msg.Verify()); +} + +TEST(FlatbuffersTest, ShrinkMetadataDoesNotMovePayload) { + Message msg = MakeMessage(16); + const uint8_t* data = msg.data(); + EXPECT_EQ(nullptr, msg.reduce_meta_size_and_get_buf(65)); + ASSERT_NE(nullptr, msg.reduce_meta_size_and_get_buf(36)); + memset(msg.mutable_buf_begin(), 'm', 36); + EXPECT_EQ(data, msg.data()); + ExpectPayload(msg, 16); + EXPECT_EQ(data, msg.reduce_meta_size_and_get_buf(0)); + EXPECT_EQ(data, msg.reduce_meta_size_and_get_buf(0)); + EXPECT_EQ(nullptr, msg.reduce_meta_size_and_get_buf(1)); + EXPECT_EQ(0u, msg.get_meta_size()); + butil::IOBuf wire; + ASSERT_TRUE(msg.append_msg_to_iobuf(wire)); + EXPECT_EQ(msg.size(), wire.size()); +} + +void* FailBlockAllocation(size_t) { return nullptr; } + +class FlatbuffersDeathTest : public ::testing::Test { +protected: + void SetUp() override { + _old_style = GTEST_FLAG_GET(death_test_style); + GTEST_FLAG_SET(death_test_style, "threadsafe"); +#if !BRPC_WITH_GLOG + _old = logging::FLAGS_crash_on_fatal_log; + logging::FLAGS_crash_on_fatal_log = false; +#endif + } + void TearDown() override { + GTEST_FLAG_SET(death_test_style, _old_style); +#if !BRPC_WITH_GLOG + logging::FLAGS_crash_on_fatal_log = _old; +#endif + } +private: + bool _old = false; + std::string _old_style; +}; + +TEST_F(FlatbuffersDeathTest, AllocationFailureIsChecked) { + EXPECT_DEATH({ + butil::iobuf::blockmem_allocate = FailBlockAllocation; + SlabAllocator allocator; + allocator.allocate(1024 * 1024); + }, "Fail to allocate"); + EXPECT_DEATH({ + SlabAllocator allocator; + uint8_t* data = allocator.allocate(1024 * 1024); + butil::iobuf::blockmem_allocate = FailBlockAllocation; + allocator.reallocate_downward(data, 1024 * 1024, 2 * 1024 * 1024, 8, 8); + }, "Fail to allocate"); +} + +TEST_F(FlatbuffersDeathTest, RejectsUnsafeAllocatorInputsAndUnfinishedRelease) { + EXPECT_DEATH({ SlabAllocator a; a.allocate(0); }, ""); + EXPECT_DEATH({ SlabAllocator a; a.allocate(std::numeric_limits::max()); }, ""); + EXPECT_DEATH({ + SlabAllocator a; + uint8_t* p = a.allocate(8); + a.reallocate_downward(p, 8, 16, 8, 1); + }, ""); + EXPECT_DEATH({ MessageBuilder builder; builder.ReleaseMessage(); }, "Finish"); + EXPECT_DEATH({ Message msg; Message other; msg.MergeFrom(other); }, "move-only"); +} + +TEST(FlatbuffersDescriptorTest, SparseStableIDsAndCanonicalNames) { + ServiceDescriptor first; + ASSERT_EQ(0, first.init({"test.", "Benchmark", "One Two Three", {2, 5, 1}})); + EXPECT_EQ("test.Benchmark", first.full_name()); + EXPECT_EQ(3, first.method_count()); + EXPECT_EQ(2, first.method(0)->index()); + EXPECT_EQ(5, first.method(1)->index()); + EXPECT_EQ("test.Benchmark.Two", first.method(1)->full_name()); + EXPECT_EQ(&first, first.method(0)->service()); + EXPECT_EQ(first.method(1), first.FindMethodByIndex(5)); + EXPECT_EQ(nullptr, first.FindMethodByIndex(3)); + EXPECT_EQ(nullptr, first.method(-1)); + EXPECT_EQ(nullptr, first.method(3)); + ServiceDescriptor reduced; + ASSERT_EQ(0, reduced.init({"test", "Benchmark", "Three Two", {1, 5}})); + EXPECT_EQ(first.index(), reduced.index()); + EXPECT_EQ(first.FindMethodByIndex(5)->index(), reduced.method(1)->index()); + EXPECT_EQ(first.FindMethodByIndex(5)->full_name(), reduced.method(1)->full_name()); + EXPECT_NE(0, first.init({"test", "Other", "Call", {3}})); + EXPECT_EQ("test.Benchmark", first.full_name()); +} + +TEST(FlatbuffersDescriptorTest, LegacyGlobalAndWhitespace) { + ServiceDescriptor desc; + ASSERT_EQ(0, desc.init({"", "Global", " First\tSecond\n Third ", {}})); + EXPECT_EQ("Global", desc.full_name()); + EXPECT_EQ(3, desc.method_count()); + EXPECT_EQ(0, desc.method(0)->index()); + EXPECT_EQ(2, desc.method(2)->index()); + EXPECT_EQ("Global.First", desc.method(0)->full_name()); +} + +TEST(FlatbuffersDescriptorTest, InvalidTablesAndFailureOwnership) { + const std::vector invalid = { + {"test", "", "A", {}}, {"test", "Service", "", {}}, + {"test", "Service", "A A", {1, 2}}, + {"test", "Service", "A B", {1, 1}}, + {"test", "Service", "A B", {1}}, + {"test", "Service", "A B", {-1, 2}}, + {"test..", "Service", "A", {}}, {".", "Service", "A", {}}, + {"test", "Bad.Name", "A", {}}, {"test", "Service", "1Bad", {}} + }; + ServiceDescriptor desc; + for (const auto& table : invalid) { + EXPECT_NE(0, desc.init(table)); + EXPECT_EQ(0, desc.method_count()); + ServiceDescriptor* output = &desc; + EXPECT_NE(0, brpc::flatbuffers::parse_service_descriptors(table, &output)); + EXPECT_EQ(&desc, output); + } + ASSERT_EQ(0, desc.init({"test", "Service", "A", {0}})); + EXPECT_NE(0, brpc::flatbuffers::parse_service_descriptors( + {"test", "Service", "A", {0}}, nullptr)); + ServiceDescriptor* output = nullptr; + ASSERT_EQ(0, brpc::flatbuffers::parse_service_descriptors( + {"test", "Service", "A", {0}}, &output)); + std::unique_ptr owner(output); + EXPECT_EQ("test.Service", owner->full_name()); +} + +} // namespace +#endif // BRPC_WITH_FLATBUFFERS diff --git a/test/flatbuffers_codegen/CMakeLists.txt b/test/flatbuffers_codegen/CMakeLists.txt new file mode 100644 index 0000000000..971197d843 --- /dev/null +++ b/test/flatbuffers_codegen/CMakeLists.txt @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +set(protobuf_MODULE_COMPATIBLE ON CACHE BOOL "Expose Protobuf compatibility variables for codegen tests" FORCE) +find_package(Protobuf CONFIG QUIET) +set(CODEGEN_PROTOBUF_CONFIG ${Protobuf_FOUND}) +if(NOT Protobuf_FOUND) + find_package(Protobuf REQUIRED) +endif() +set(CODEGEN_CXX_STANDARD 14) +if(Protobuf_VERSION VERSION_GREATER 4.21) + set(CODEGEN_CXX_STANDARD 17) + if(NOT CODEGEN_PROTOBUF_CONFIG) + message(FATAL_ERROR "Protobuf 5+ needs its CMake config package for codegen tests") + endif() +endif() +message(STATUS "Codegen tests: Protobuf ${Protobuf_VERSION}, C++${CODEGEN_CXX_STANDARD}") +find_program(FLATC_EXECUTABLE flatc) +if(NOT FLATC_EXECUTABLE) + message(FATAL_ERROR "Codegen tests need the official flatc executable") +endif() +get_filename_component(BRPC_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +set(GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${GENERATED_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/include/butil") +# Keep test-only configuration in this build tree, never in the repository. +set(WITH_FLATBUFFERS_VAL 1) +configure_file("${BRPC_SOURCE_DIR}/config.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/include/butil/config.h") +set(CODEGEN_INCLUDE_DIRS + "${CMAKE_CURRENT_BINARY_DIR}/include" + "${BRPC_SOURCE_DIR}/src" + "${GENERATED_DIR}" + "${FLATBUFFERS_INCLUDE_DIR}" + ${Protobuf_INCLUDE_DIRS}) +get_target_property(CODEGEN_PROTOBUF_INCLUDE_DIRS protobuf::libprotobuf INTERFACE_INCLUDE_DIRECTORIES) +if(CODEGEN_PROTOBUF_INCLUDE_DIRS) + list(APPEND CODEGEN_INCLUDE_DIRS ${CODEGEN_PROTOBUF_INCLUDE_DIRS}) +endif() +if(TARGET absl::base) + get_target_property(CODEGEN_ABSL_INCLUDE_DIRS absl::base INTERFACE_INCLUDE_DIRECTORIES) + if(CODEGEN_ABSL_INCLUDE_DIRS) + list(APPEND CODEGEN_INCLUDE_DIRS ${CODEGEN_ABSL_INCLUDE_DIRS}) + endif() +endif() +add_custom_command( + OUTPUT "${GENERATED_DIR}/echo.brpc.fb.cpp" + "${GENERATED_DIR}/echo.brpc.fb.h" + "${GENERATED_DIR}/echo_generated.h" + COMMAND "${FLATC_EXECUTABLE}" --cpp -o "${GENERATED_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/echo.fbs" + COMMAND brpc_flatc -o "${GENERATED_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/echo.fbs" + DEPENDS brpc_flatc "${CMAKE_CURRENT_SOURCE_DIR}/echo.fbs" + VERBATIM) +add_library(brpc_codegen_compile OBJECT "${GENERATED_DIR}/echo.brpc.fb.cpp") +target_include_directories(brpc_codegen_compile PRIVATE ${CODEGEN_INCLUDE_DIRS}) +target_compile_features(brpc_codegen_compile PRIVATE cxx_std_${CODEGEN_CXX_STANDARD}) + +add_test(NAME flatbuffers_codegen_acceptance + COMMAND "${CMAKE_COMMAND}" + "-DGENERATOR=$" + "-DFLATC=${FLATC_EXECUTABLE}" + "-DSCHEMA=${CMAKE_CURRENT_SOURCE_DIR}/echo.fbs" + "-DWORK=${CMAKE_CURRENT_BINARY_DIR}/acceptance" + "-DCXX=${CMAKE_CXX_COMPILER}" + "-DCXX_STANDARD=${CODEGEN_CXX_STANDARD}" + "-DINCLUDE_DIRS=${CODEGEN_INCLUDE_DIRS}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/acceptance.cmake") + +set(BRPC_CODEGEN_BRPC_LIBRARY "" CACHE FILEPATH + "Optional existing FlatBuffers-enabled libbrpc for runtime acceptance") +set(BRPC_CODEGEN_EXTRA_LIBRARIES "" CACHE STRING + "Additional link dependencies of the supplied libbrpc") +if(BRPC_CODEGEN_BRPC_LIBRARY) + find_package(Threads REQUIRED) + find_package(OpenSSL REQUIRED) + find_package(ZLIB REQUIRED) + find_library(CODEGEN_GFLAGS_LIBRARY gflags) + find_library(CODEGEN_LEVELDB_LIBRARY leveldb) + add_executable(brpc_codegen_runtime runtime.cpp $) + target_include_directories(brpc_codegen_runtime PRIVATE ${CODEGEN_INCLUDE_DIRS}) + target_compile_features(brpc_codegen_runtime PRIVATE cxx_std_${CODEGEN_CXX_STANDARD}) + target_link_libraries(brpc_codegen_runtime PRIVATE + "${BRPC_CODEGEN_BRPC_LIBRARY}" protobuf::libprotobuf + ${CODEGEN_GFLAGS_LIBRARY} ${CODEGEN_LEVELDB_LIBRARY} + OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB Threads::Threads + ${CMAKE_DL_LIBS} ${BRPC_CODEGEN_EXTRA_LIBRARIES}) + add_test(NAME flatbuffers_codegen_runtime COMMAND brpc_codegen_runtime) +endif() diff --git a/test/flatbuffers_codegen/acceptance.cmake b/test/flatbuffers_codegen/acceptance.cmake new file mode 100644 index 0000000000..118b271755 --- /dev/null +++ b/test/flatbuffers_codegen/acceptance.cmake @@ -0,0 +1,349 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +if(NOT DEFINED CXX_STANDARD) + set(CXX_STANDARD 14) +endif() +file(READ "${SCHEMA}" schema_text) +file(MAKE_DIRECTORY "${WORK}") + +function(expect_rejected name replacement) + set(directory "${WORK}/${name}") + file(MAKE_DIRECTORY "${directory}") + string(REPLACE "(id: 7)" "${replacement}" text "${schema_text}") + file(WRITE "${directory}/echo.fbs" "${text}") + execute_process(COMMAND "${GENERATOR}" -o "${directory}" "${directory}/echo.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if("${result}" STREQUAL "0" OR error STREQUAL "") + message(FATAL_ERROR "${name}: invalid schema was accepted or lacked diagnostic") + endif() + if(EXISTS "${directory}/echo.brpc.fb.h" OR EXISTS "${directory}/echo.brpc.fb.cpp") + message(FATAL_ERROR "${name}: invalid schema emitted service files") + endif() + message(STATUS "Rejected ${name}: ${error}") +endfunction() + +function(expect_rejected_rpc_name name) + set(directory "${WORK}/reserved_${name}") + file(MAKE_DIRECTORY "${directory}") + file(REMOVE "${directory}/echo.brpc.fb.h" "${directory}/echo.brpc.fb.cpp") + string(REPLACE "Inspect(" "${name}(" text "${schema_text}") + file(WRITE "${directory}/echo.fbs" "${text}") + execute_process(COMMAND "${GENERATOR}" -o "${directory}" "${directory}/echo.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if("${result}" STREQUAL "0") + message(SEND_ERROR "RPC ${name}: generator incorrectly accepted a fixed Stub field name") + return() + endif() + if(NOT error MATCHES "method name collides with generated API: ${name}") + message(FATAL_ERROR "RPC ${name}: expected a name-collision diagnostic: ${error}") + endif() + if(EXISTS "${directory}/echo.brpc.fb.h" OR EXISTS "${directory}/echo.brpc.fb.cpp") + message(FATAL_ERROR "RPC ${name}: rejected schema emitted service files") + endif() + message(STATUS "Rejected RPC ${name}: ${error}") +endfunction() + +function(expect_compiles name text) + set(directory "${WORK}/${name}") + file(MAKE_DIRECTORY "${directory}") + file(WRITE "${directory}/echo.fbs" "${text}") + execute_process(COMMAND "${FLATC}" --cpp -o "${directory}" "${directory}/echo.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: official flatc failed: ${error}") + endif() + execute_process(COMMAND "${GENERATOR}" -o "${directory}" "${directory}/echo.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: brpc_flatc failed: ${error}") + endif() + set(includes "-I${directory}") + foreach(include_dir IN LISTS INCLUDE_DIRS) + list(APPEND includes "-I${include_dir}") + endforeach() + execute_process(COMMAND "${CXX}" "-std=c++${CXX_STANDARD}" ${includes} + -c "${directory}/echo.brpc.fb.cpp" -o "${directory}/echo.o" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: generated code did not compile: ${error}") + endif() + # The generated header must also compile without incidental prior includes. + file(WRITE "${directory}/header.cpp" "#include \"echo.brpc.fb.h\"\n") + execute_process(COMMAND "${CXX}" "-std=c++${CXX_STANDARD}" ${includes} + -c "${directory}/header.cpp" -o "${directory}/header.o" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: generated header is not self-contained: ${error}") + endif() + foreach(suffix brpc.fb.h brpc.fb.cpp) + file(READ "${directory}/echo.${suffix}" generated) + if(NOT generated MATCHES "Licensed to the Apache Software Foundation") + message(FATAL_ERROR "${name}: ${suffix} lacks Apache license") + endif() + if(generated MATCHES "FLATBUFFERS_VERSION") + message(FATAL_ERROR "${name}: service output pins FlatBuffers version") + endif() + endforeach() + message(STATUS "Compiled ${name}") +endfunction() + +function(expect_distinct_headers name first_stem second_stem) + set(directory "${WORK}/header_guards_${name}") + set(includes) + foreach(include_dir IN LISTS INCLUDE_DIRS) + list(APPEND includes "-I${include_dir}") + endforeach() + foreach(side first second) + set(stem "${${side}_stem}") + set(output "${directory}/${side}") + file(MAKE_DIRECTORY "${output}") + string(REPLACE "namespace codegen.example;" "namespace guard_${name}.${side};" + text "${schema_text}") + if(side STREQUAL "first") + set(first_text "${text}") + endif() + file(WRITE "${output}/${stem}.fbs" "${text}") + execute_process(COMMAND "${FLATC}" --cpp -o "${output}" "${output}/${stem}.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}/${side}: official flatc failed: ${error}") + endif() + execute_process(COMMAND "${GENERATOR}" -o "${output}" "${output}/${stem}.fbs" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}/${side}: brpc_flatc failed: ${error}") + endif() + file(WRITE "${output}/single.cpp" + "#include \"${stem}.brpc.fb.h\"\n::guard_${name}::${side}::Echo* service = nullptr;\n") + execute_process(COMMAND "${CXX}" "-std=c++${CXX_STANDARD}" ${includes} + -c "${output}/single.cpp" -o "${output}/single.o" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}/${side}: standalone header failed: ${error}") + endif() + endforeach() + foreach(reverse FALSE TRUE) + set(first "#include \"first/${first_stem}.brpc.fb.h\"\n") + set(second "#include \"second/${second_stem}.brpc.fb.h\"\n") + if(reverse) + set(headers "${second}${first}") + else() + set(headers "${first}${second}") + endif() + file(WRITE "${directory}/combined_${reverse}.cpp" + "${headers}::guard_${name}::first::Echo* first_echo = nullptr;\n::guard_${name}::second::Echo* second_echo = nullptr;\n") + execute_process(COMMAND "${CXX}" "-std=c++${CXX_STANDARD}" ${includes} + -c "${directory}/combined_${reverse}.cpp" + -o "${directory}/combined_${reverse}.o" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: combined headers (reverse=${reverse}) failed: ${error}") + endif() + endforeach() + # A checkout/output directory change must not alter generated identifiers. + set(relocated "${directory}/relocated") + file(MAKE_DIRECTORY "${relocated}") + file(WRITE "${relocated}/${first_stem}.fbs" "${first_text}") + execute_process(COMMAND "${GENERATOR}" -o . "${first_stem}.fbs" + WORKING_DIRECTORY "${relocated}" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${name}: relocated generation failed: ${error}") + endif() + foreach(suffix brpc.fb.h brpc.fb.cpp) + file(READ "${directory}/first/${first_stem}.${suffix}" original) + file(READ "${relocated}/${first_stem}.${suffix}" regenerated) + if(NOT "${original}" STREQUAL "${regenerated}") + message(FATAL_ERROR "${name}: ${suffix} depends on checkout/output paths") + endif() + endforeach() + message(STATUS "Distinct, relocatable header guards: ${name}") +endfunction() + +expect_distinct_headers(punctuation foo-bar foo_bar) +expect_distinct_headers(letter_case FooBar foobar) +expect_distinct_headers(same_basename echo echo) + +expect_rejected_rpc_name(channel_) +expect_rejected_rpc_name(owned_channel_) +expect_rejected(missing_id "") +expect_rejected(duplicate_id "(id: 41)") +expect_rejected(negative_id "(id: -1)") +expect_rejected(overflow_id "(id: 2147483648)") +expect_rejected(string_id "(id: \"7\")") +expect_rejected(streaming "(id: 7, streaming: \"server\")") +expect_compiles(sparse_ids "${schema_text}") + +set(shadow_names request response controller done method std BrpcFlatbuffersFail + STUB_OWNS_CHANNEL STUB_DOESNT_OWN_CHANNEL ChannelOwnership) +set(shadow_schema "namespace codegen.names;\ntable Request { text:string; }\ntable Response { text:string; }\nrpc_service Names {\n") +set(method_id 0) +foreach(name IN LISTS shadow_names) + math(EXPR method_id "${method_id} + 7") + string(APPEND shadow_schema " ${name}(Request):Response (id: ${method_id});\n") +endforeach() +string(APPEND shadow_schema "}\nrpc_service ChannelOwnership { Ping(Request):Response (id: 3); }\n") +expect_compiles(shadowed_names "${shadow_schema}") + +# Supply RUNTIME_LIBRARIES when invoking this script directly to exercise the +# new names against an existing FlatBuffers-enabled bRPC library as well. +if(RUNTIME_LIBRARIES) + set(directory "${WORK}/shadowed_names") + set(runtime_source [=[ +#include "echo.brpc.fb.h" +#include + +using ::brpc::flatbuffers::Message; +using ::google::protobuf::Closure; +using ::google::protobuf::RpcController; + +class Completion : public Closure { +public: + void Run() override { ++runs; } + int runs = 0; +}; + +class Implementation : public ::codegen::names::Names { +public: + int selected = 0; +]=]) + set(method_id 0) + foreach(name IN LISTS shadow_names) + math(EXPR method_id "${method_id} + 7") + string(APPEND runtime_source + " void ${name}(RpcController*, const Message*, Message*, Closure* completion) override { selected = ${method_id}; completion->Run(); }\n") + endforeach() + string(APPEND runtime_source [=[ +}; + +class LocalChannel : public ::brpc::flatbuffers::RpcChannel { +public: + explicit LocalChannel(::brpc::flatbuffers::Service* service) : service_(service) {} + void FBCallMethod(const ::brpc::flatbuffers::MethodDescriptor* method, + RpcController* controller, const Message* request, + Message* response, Closure* done) override { + service_->FBCallMethod(method, controller, request, response, done); + } +private: + ::brpc::flatbuffers::Service* service_; +}; + +int main() { + ::brpc::flatbuffers::MessageBuilder builder; + builder.Finish(::codegen::names::CreateRequest(builder)); + Message request = builder.ReleaseMessage(); + Message response; + Implementation implementation; + LocalChannel channel(&implementation); + ::codegen::names::Names::Stub stub(&channel); + ::codegen::names::Names defaults; + LocalChannel default_channel(&defaults); + ::codegen::names::Names::Stub default_stub(&default_channel); + ::codegen::names::Names::Stub disconnected(nullptr); + Completion completion; + int expected_runs = 0; +]=]) + set(method_id 0) + foreach(name IN LISTS shadow_names) + math(EXPR method_id "${method_id} + 7") + string(APPEND runtime_source + " stub.${name}(nullptr, &request, &response, &completion);\n" + " if (implementation.selected != ${method_id} || completion.runs != ++expected_runs) return 1;\n" + " default_stub.${name}(nullptr, &request, &response, &completion);\n" + " if (completion.runs != ++expected_runs) return 2;\n" + " disconnected.${name}(nullptr, &request, &response, &completion);\n" + " if (completion.runs != ++expected_runs) return 3;\n") + endforeach() + string(APPEND runtime_source [=[ + ::codegen::names::ChannelOwnership service; + LocalChannel service_channel(&service); + ::codegen::names::ChannelOwnership::Stub named_stub(&service_channel, + ::brpc::flatbuffers::Service::STUB_DOESNT_OWN_CHANNEL); + named_stub.Ping(nullptr, &request, &response, &completion); + if (completion.runs != ++expected_runs) return 4; + ::codegen::names::Names::Stub owned_stub(new LocalChannel(&implementation), + ::brpc::flatbuffers::Service::STUB_OWNS_CHANNEL); + owned_stub.request(nullptr, &request, &response, &completion); + if (implementation.selected != 7 || completion.runs != ++expected_runs) return 5; + std::cout << "Shadowed-name dispatch, failures, and constructors passed\n"; + return 0; +} +]=]) + file(WRITE "${directory}/runtime.cpp" "${runtime_source}") + set(includes "-I${directory}") + foreach(include_dir IN LISTS INCLUDE_DIRS) + list(APPEND includes "-I${include_dir}") + endforeach() + execute_process(COMMAND "${CXX}" "-std=c++${CXX_STANDARD}" ${includes} + "${directory}/runtime.cpp" "${directory}/echo.o" ${RUNTIME_LIBRARIES} + -o "${directory}/runtime" + RESULT_VARIABLE result ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "Shadowed-name runtime link failed: ${error}") + endif() + execute_process(COMMAND "${directory}/runtime" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error) + if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "Shadowed-name runtime failed: ${output}${error}") + endif() + message(STATUS "${output}") +endif() + +string(REPLACE + " Repeat(Request):Response (id: 41);\n Inspect(Request):Response (id: 7);" + " Inspect(Request):Response (id: 7);\n Repeat(Request):Response (id: 41);" + reordered "${schema_text}") +expect_compiles(reordered "${reordered}") +file(READ "${WORK}/reordered/echo.brpc.fb.cpp" reordered_source) +if(NOT reordered_source MATCHES "case 41:" OR NOT reordered_source MATCHES "case 7:") + message(FATAL_ERROR "Reordering changed wire IDs") +endif() +string(FIND "${reordered_source}" "void Echo_Stub::Repeat(" repeat_begin) +if(repeat_begin LESS 0) + message(FATAL_ERROR "Reordered stub lacks Repeat") +endif() +string(SUBSTRING "${reordered_source}" ${repeat_begin} -1 repeat_body) +string(FIND "${repeat_body}" "\n}\n" repeat_end) +string(SUBSTRING "${repeat_body}" 0 ${repeat_end} repeat_body) +if(NOT repeat_body MATCHES "method\\(1\\)") + message(FATAL_ERROR "Reordered stub does not use its dense method position") +endif() + +string(REPLACE "namespace codegen.example;" "" global_schema "${schema_text}") +expect_compiles(global_namespace "${global_schema}") +string(REPLACE "(id: 7)" "(id: 0)" zero_schema "${schema_text}") +expect_compiles(zero_id "${zero_schema}") +string(REPLACE "(id: 7)" "(id: 2147483647)" maximum_schema "${schema_text}") +expect_compiles(maximum_id "${maximum_schema}") + +# Include schemas are read through the official Parser and not re-emitted. +file(MAKE_DIRECTORY "${WORK}/included") +file(WRITE "${WORK}/included/types.fbs" + "namespace shared; table Input { note:string; } table Output { note:string; }\n" + "rpc_service Imported { Ping(Input):Output (id: 3); }\n") +execute_process(COMMAND "${FLATC}" --cpp -o "${WORK}/included" + "${WORK}/included/types.fbs" RESULT_VARIABLE result) +if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "Cannot generate included table definitions") +endif() +expect_compiles(included + "include \"types.fbs\"; namespace api; rpc_service Local { Call(shared.Input):shared.Output (id: 17); }") +file(READ "${WORK}/included/echo.brpc.fb.h" included_header) +if(included_header MATCHES "class Imported") + message(FATAL_ERROR "Included service was emitted twice") +endif() diff --git a/test/flatbuffers_codegen/echo.fbs b/test/flatbuffers_codegen/echo.fbs new file mode 100644 index 0000000000..d023205f30 --- /dev/null +++ b/test/flatbuffers_codegen/echo.fbs @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +namespace codegen.example; + +table Request { + text:string; +} + +table Response { + text:string; +} + +rpc_service Echo { + Repeat(Request):Response (id: 41); + Inspect(Request):Response (id: 7); +} + +rpc_service Other { + Repeat(Request):Response (id: 41); +} + +root_type Request; diff --git a/test/flatbuffers_codegen/runtime.cpp b/test/flatbuffers_codegen/runtime.cpp new file mode 100644 index 0000000000..4b923939ba --- /dev/null +++ b/test/flatbuffers_codegen/runtime.cpp @@ -0,0 +1,308 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "echo.brpc.fb.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +#define CODEGEN_CHECK(condition) \ + do { \ + if (!(condition)) { \ + throw std::runtime_error("check failed: " #condition); \ + } \ + } while (0) + +using brpc::flatbuffers::Message; +using brpc::flatbuffers::MessageBuilder; +using brpc::flatbuffers::MethodDescriptor; +using brpc::flatbuffers::ServiceDescriptor; +using codegen::example::Echo; +using codegen::example::Other; +using codegen::example::Request; +using codegen::example::Response; +using google::protobuf::Closure; +using google::protobuf::RpcController; + +class Controller : public RpcController { +public: + void Reset() override { failures = 0; error.clear(); } + bool Failed() const override { return failures != 0; } + std::string ErrorText() const override { return error; } + void StartCancel() override {} + void SetFailed(const std::string& reason) override { ++failures; error = reason; } + bool IsCanceled() const override { return false; } + void NotifyOnCancel(Closure*) override {} + int failures = 0; + std::string error; +}; + +class Done : public Closure { +public: + void Run() override { ++runs; } + int runs = 0; +}; + +class DeletingDone : public Closure { +public: + explicit DeletingDone(int* runs) : runs_(runs) {} + void Run() override { ++*runs_; delete this; } +private: + int* runs_; +}; + +Message MakeRequest(const char* text) { + MessageBuilder builder; + const auto value = text ? builder.CreateString(text) : + ::flatbuffers::Offset<::flatbuffers::String>(); + builder.Finish(codegen::example::CreateRequest(builder, value)); + return builder.ReleaseMessage(); +} + +class Implementation : public Echo { +public: + void Repeat(RpcController*, const Message* request, + Message* response, Closure* done) override { + ++calls; + selected = 41; + Reply(request, response, done); + } + void Inspect(RpcController*, const Message* request, + Message* response, Closure* done) override { + ++calls; + selected = 7; + Reply(request, response, done); + } + void Reply(const Message* request, Message* response, Closure* done) { + const auto* input = request->GetRoot(); + MessageBuilder builder; + const auto value = input->text() ? builder.CreateString(input->text()->str()) : + ::flatbuffers::Offset<::flatbuffers::String>(); + builder.Finish(codegen::example::CreateResponse(builder, value)); + *response = builder.ReleaseMessage(); + if (done) { + done->Run(); + } + } + int calls = 0; + int selected = -1; +}; + +class LocalChannel : public brpc::flatbuffers::RpcChannel { +public: + explicit LocalChannel(Echo* service, int* destroyed = nullptr) + : service_(service), destroyed_(destroyed) {} + ~LocalChannel() override { if (destroyed_) { ++*destroyed_; } } + void FBCallMethod(const MethodDescriptor* method, RpcController* controller, + const Message* request, Message* response, Closure* done) override { + last_method = method; + service_->FBCallMethod(method, controller, request, response, done); + } + const MethodDescriptor* last_method = nullptr; +private: + Echo* service_; + int* destroyed_; +}; + +void TestConcurrentDescriptors() { + const int thread_count = 24; + std::atomic start(false); + std::atomic invalid(0); + std::vector descriptors(thread_count); + std::vector threads; + for (int i = 0; i < thread_count; ++i) { + threads.emplace_back([&, i] { + while (!start.load()) { + std::this_thread::yield(); + } + for (int n = 0; n < 1000; ++n) { + descriptors[i] = Echo::descriptor(); + if (descriptors[i]->method_count() != 2 || + descriptors[i]->method(0)->index() != 41 || + descriptors[i]->method(1)->index() != 7) { + ++invalid; + } + } + }); + } + start.store(true); + for (auto& thread : threads) { + thread.join(); + } + CODEGEN_CHECK(invalid.load() == 0); + for (const auto* descriptor : descriptors) { + CODEGEN_CHECK(descriptor == descriptors.front()); + } + const auto* descriptor = descriptors.front(); + CODEGEN_CHECK(descriptor->full_name() == "codegen.example.Echo"); + CODEGEN_CHECK(descriptor->FindMethodByIndex(41) == descriptor->method(0)); + CODEGEN_CHECK(descriptor->FindMethodByIndex(7) == descriptor->method(1)); + CODEGEN_CHECK(descriptor->FindMethodByIndex(0) == nullptr); + CODEGEN_CHECK(descriptor->method(0)->service() == descriptor); +} + +void ExpectFailure(const std::function& call) { + Controller controller; + Done done; + call(&controller, &done); + CODEGEN_CHECK(controller.Failed()); + CODEGEN_CHECK(controller.failures == 1); + CODEGEN_CHECK(!controller.ErrorText().empty()); + CODEGEN_CHECK(done.runs == 1); +} + +void TestDispatchAndErrors() { + Implementation service; + LocalChannel channel(&service); + Echo::Stub stub(&channel); + const auto* descriptor = Echo::descriptor(); + CODEGEN_CHECK(stub.GetDescriptor() == descriptor); + CODEGEN_CHECK(stub.channel() == &channel); + for (const char* text : {static_cast(nullptr), "hello"}) { + Message request = MakeRequest(text); + for (int position = 0; position < 2; ++position) { + Controller controller; + Done done; + Message response; + if (position == 0) { + stub.Repeat(&controller, &request, &response, &done); + } else { + stub.Inspect(&controller, &request, &response, &done); + } + CODEGEN_CHECK(!controller.Failed()); + CODEGEN_CHECK(done.runs == 1); + CODEGEN_CHECK(channel.last_method == descriptor->method(position)); + CODEGEN_CHECK(service.selected == (position == 0 ? 41 : 7)); + CODEGEN_CHECK(response.Verify()); + const auto* output = response.GetRoot(); + CODEGEN_CHECK(text ? output->text() && output->text()->str() == text : + output->text() == nullptr); + } + } + const int calls = service.calls; + Message request = MakeRequest(nullptr); + Message response; + Message invalid; + const MethodDescriptor unknown("Unknown", descriptor, 99); + const MethodDescriptor forged("Forged", descriptor, 41); + for (const auto* method : {static_cast(nullptr), + Other::descriptor()->method(0), &unknown, &forged}) { + ExpectFailure([&](Controller* controller, Done* done) { + service.FBCallMethod(method, controller, &request, &response, done); + }); + } + for (int position = 0; position < 2; ++position) { + ExpectFailure([&](Controller* controller, Done* done) { + service.FBCallMethod(descriptor->method(position), controller, + &invalid, &response, done); + }); + } + ExpectFailure([&](Controller* controller, Done* done) { + service.FBCallMethod(descriptor->method(0), controller, nullptr, &response, done); + }); + ExpectFailure([&](Controller* controller, Done* done) { + service.FBCallMethod(descriptor->method(0), controller, &request, nullptr, done); + }); + CODEGEN_CHECK(service.calls == calls); + + Echo unimplemented; + for (int position = 0; position < 2; ++position) { + ExpectFailure([&](Controller* controller, Done* done) { + unimplemented.FBCallMethod(descriptor->method(position), controller, + &request, &response, done); + }); + } + ExpectFailure([&](Controller* controller, Done* done) { + unimplemented.Repeat(controller, &request, &response, done); + }); + Echo::Stub disconnected(nullptr); + ExpectFailure([&](Controller* controller, Done* done) { + disconnected.Repeat(controller, &request, &response, done); + }); + ExpectFailure([&](Controller* controller, Done* done) { + disconnected.Inspect(controller, &request, &response, done); + }); + Controller controller; + service.FBCallMethod(nullptr, &controller, &request, &response, nullptr); + CODEGEN_CHECK(controller.failures == 1); + Done done; + service.FBCallMethod(nullptr, nullptr, &request, &response, &done); + CODEGEN_CHECK(done.runs == 1); + int deleted_runs = 0; + service.FBCallMethod(nullptr, nullptr, &request, &response, + new DeletingDone(&deleted_runs)); + CODEGEN_CHECK(deleted_runs == 1); +} + +void TestOwnershipAndAsyncCompletion() { + Implementation service; + int destroyed = 0; + { + Echo::Stub owner(new LocalChannel(&service, &destroyed), + brpc::flatbuffers::Service::STUB_OWNS_CHANNEL); + } + CODEGEN_CHECK(destroyed == 1); + { + LocalChannel borrowed(&service, &destroyed); + { + Echo::Stub stub(&borrowed); + } + CODEGEN_CHECK(destroyed == 1); + } + CODEGEN_CHECK(destroyed == 2); + + class Deferred : public Echo { + public: + void Repeat(RpcController*, const Message*, Message*, Closure* done) override { + pending = done; + } + Closure* pending = nullptr; + } deferred; + Message request = MakeRequest(nullptr); + Message response; + Controller controller; + Done done; + deferred.FBCallMethod(Echo::descriptor()->method(0), &controller, + &request, &response, &done); + CODEGEN_CHECK(!controller.Failed()); + CODEGEN_CHECK(done.runs == 0); + CODEGEN_CHECK(deferred.pending == &done); + deferred.pending->Run(); + CODEGEN_CHECK(done.runs == 1); +} + +} // namespace + +int main() { + try { + TestConcurrentDescriptors(); + TestDispatchAndErrors(); + TestOwnershipAndAsyncCompletion(); + std::cout << "Descriptor, sparse dispatch, verifier, callback and ownership checks passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/test/flatbuffers_message.fbs b/test/flatbuffers_message.fbs new file mode 100644 index 0000000000..0288499125 --- /dev/null +++ b/test/flatbuffers_message.fbs @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +namespace brpc_fbtest; + +table Payload { + value:long; + message:string; + values:[int]; +} + +root_type Payload; diff --git a/tools/flatbuffers/CMakeLists.txt b/tools/flatbuffers/CMakeLists.txt new file mode 100644 index 0000000000..05179ac090 --- /dev/null +++ b/tools/flatbuffers/CMakeLists.txt @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +cmake_minimum_required(VERSION 3.10) +project(brpc_flatc LANGUAGES CXX) + +find_path(FLATBUFFERS_INCLUDE_DIR flatbuffers/idl.h) +find_library(FLATBUFFERS_LIBRARY NAMES flatbuffers) +if(NOT FLATBUFFERS_INCLUDE_DIR OR NOT FLATBUFFERS_LIBRARY) + message(FATAL_ERROR "Install official FlatBuffers development headers and libflatbuffers") +endif() + +add_executable(brpc_flatc brpc_flatc.cpp) +target_compile_features(brpc_flatc PRIVATE cxx_std_11) +target_include_directories(brpc_flatc SYSTEM PRIVATE "${FLATBUFFERS_INCLUDE_DIR}") +target_link_libraries(brpc_flatc PRIVATE "${FLATBUFFERS_LIBRARY}") +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(brpc_flatc PRIVATE -Wall -Wextra -Wpedantic) +endif() + +include(CTest) +if(BUILD_TESTING) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../test/flatbuffers_codegen" + "${CMAKE_CURRENT_BINARY_DIR}/tests") +endif() diff --git a/tools/flatbuffers/README.md b/tools/flatbuffers/README.md new file mode 100644 index 0000000000..55f44729b7 --- /dev/null +++ b/tools/flatbuffers/README.md @@ -0,0 +1,170 @@ +# Standalone FlatBuffers service generator + +For runtime enablement and a complete client/server check, see the +[English guide](../../docs/en/flatbuffers.md) or +[中文指南](../../docs/cn/flatbuffers.md). + +`brpc_flatc` generates bRPC service bindings. The separate `fb_rpc` transport +implements their channel interface in `brpc::Channel`. The generator uses the installed, official +`flatbuffers/idl.h` Parser and `libflatbuffers`; it does not download or modify a +FlatBuffers fork. The official `flatc --cpp` remains responsible for table types. + +## Build and generate + +The tool needs a C++11 compiler, CMake, and official FlatBuffers development +headers/library. To build only the generator: + +```sh +cmake -S tools/flatbuffers -B /tmp/brpc-codegen-build -DBUILD_TESTING=OFF +cmake --build /tmp/brpc-codegen-build -j2 +mkdir -p /tmp/brpc-generated +flatc --cpp -o /tmp/brpc-generated test/flatbuffers_codegen/echo.fbs +/tmp/brpc-codegen-build/brpc_flatc -o /tmp/brpc-generated test/flatbuffers_codegen/echo.fbs +``` + +For a non-default installation, add `-DCMAKE_PREFIX_PATH=/path/to/flatbuffers` +and/or explicit `FLATBUFFERS_INCLUDE_DIR` and `FLATBUFFERS_LIBRARY` values. +Run official `flatc` from that matching installation, not an unrelated binary +found earlier on `PATH`. The runtime alone does not link `libflatbuffers`, but +this generator needs it. + +The output directory must already exist. The command line is: + +```text +brpc_flatc [-I include_dir]... [-o existing_output_dir] schema.fbs +``` + +For `echo.fbs`, the two tools produce: + +- Official `flatc`: `echo_generated.h`. +- `brpc_flatc`: `echo.brpc.fb.h` and `echo.brpc.fb.cpp`, both Apache-licensed. + +Compile the generated `.cpp` into the application with C++14 or newer and bRPC +configured with FlatBuffers support. When the detected `Protobuf_VERSION` is +greater than 4.21, use C++17 and the matching Protobuf/Abseil dependency set. Keep the generated header, official table header, and bRPC +headers on the include path. The generated service files do not pin a +FlatBuffers version; use an official `flatc` matching your installed table +headers, as required by official generated code. + +Schema includes are resolved relative to the input file and through repeated +`-I` options (`-Idir` also works). Run official `flatc --cpp` for each included +schema to obtain its table header. Run `brpc_flatc` separately for each schema +that declares services; imported services are not emitted again. Pass the same +include directories to both tools. This tool processes one `.fbs` per invocation +and follows the official default `*_generated.h` naming convention. + +## Explicit, stable IDs + +```fbs +namespace example.api; + +table Request { text:string; } +table Response { text:string; } + +rpc_service Echo { + Repeat(Request):Response (id: 41); + Inspect(Request):Response (id: 7); +} +``` + +Every RPC method must have an explicit integer `(id: N)` in `[0, 2147483647]`. +IDs must be unique **within a service**. Missing, negative, duplicate, quoted, +or out-of-range IDs fail generation with a diagnostic and nonzero exit status. +Reordering declarations never changes their wire IDs. This does not require +explicit IDs on FlatBuffers table fields. + +The generated names are `example::api::Echo`, +`example::api::Echo_Stub`, and the alias `example::api::Echo::Stub`. +They are **not** the old `EchoStub` spelling. Methods take protobuf +`RpcController`/`Closure` and `brpc::flatbuffers::Message` parameters. Derive from +`Echo` and override the schema methods; supply an implementation of +`brpc::flatbuffers::RpcChannel` to a stub, such as a `brpc::Channel` initialized +with `ChannelOptions::protocol = "fb_rpc"`. Register the implementation with +`Server::AddFlatBuffersService`. See [FlatBuffers RPC](../../docs/en/flatbuffers.md) +for ownership, protocol limitations, and response verification. Channel ownership +defaults to borrowed; `Service::STUB_OWNS_CHANNEL` transfers ownership to the stub. + +This intentionally bounded generator supports unary table RPCs, multiple +services/methods, namespaces, included request/response tables, and absent +optional strings. Streaming and C++ keyword names in service/type/namespace +positions are rejected rather than silently misgenerated. Method names that +collide with generated service APIs are also rejected. Schema file basenames +may contain ASCII letters, digits, underscores, dots, and hyphens. +Service header guards preserve the exact filename and first exported service's +fully-qualified identity. They distinguish punctuation/case and same basenames +in different namespaces without depending on checkout or output paths. Schemas +included together must still define distinct fully-qualified C++ services. + +## Descriptor and completion contracts + +- `Echo::descriptor()` owns a `ServiceDescriptor` in a function-local static + RAII holder. C++11 initialization is thread-safe; the holder and its owned + method descriptors are destroyed normally at process exit. There is no + leaked singleton allocation or unsynchronized lazy initialization. +- The descriptor table contains the namespace prefix, service name, ordered + method names, and explicit IDs. Stubs use `method(position)`; service dispatch + switches on the stable `method->index()`. +- Service dispatch checks descriptor ownership and identity, non-null request + and response, and `request->Verify()` before calling user code. +- Invalid calls, null stub channels, and default unimplemented methods call + `controller->SetFailed()` when a controller exists, then run non-null `done` + exactly once. Null controllers and callbacks are tolerated on failure. +- Successful dispatch transfers completion responsibility to the user method. + The generated dispatcher does not run `done` again, so asynchronous service + implementations remain possible. User methods and channel implementations + must themselves honor the exactly-once completion contract. + +## Independent acceptance tests + +No root build file needs modification. The default standalone build enables +CTest and additionally needs official `flatc` and Protobuf development headers. +The commands below use CMake/CTest 3.17+ for `--no-tests=error` (the standalone +generator itself has a CMake 3.10 minimum): + +```sh +cmake -S tools/flatbuffers -B /tmp/brpc-codegen-build -DBUILD_TESTING=ON +cmake --build /tmp/brpc-codegen-build -j2 +(cd /tmp/brpc-codegen-build && ctest --no-tests=error --output-on-failure) +``` + +The build compiles the generated service against the real bRPC headers. CTest +rejects missing/duplicate/negative/overflow/string IDs and streaming RPCs, then +compiles the service and self-contained header for sparse IDs, reordered +methods, global/nested namespaces, zero/max IDs, and included table types. Test +configuration headers and generated artifacts stay in the standalone build +directory, not in the source tree. This differs from the root bRPC configure, +which also writes its source `src/butil/config.h`. + +Expect **one** CTest named `flatbuffers_codegen_acceptance` in this mode. Its +inner checks also compile colliding-basename headers alone and together in both +orders, and compare regenerated bytes after relocating the schema. An empty +CTest selection is not acceptance. + +To enable additional behavior tests against an **already built** +FlatBuffers-enabled bRPC library, use matching runtime headers/dependencies and +an existing library. A shared library avoids having to reconstruct all static +platform link dependencies. Build it with root `BUILD_SHARED_LIBS=ON` and target +`brpc-shared` first; use `.dylib` on macOS instead of `.so`: + +```sh +BRPC_LIBRARY=/absolute/path/to/FB-on/output/lib/libbrpc.so +cmake -S tools/flatbuffers -B /tmp/brpc-codegen-build \ + -DBUILD_TESTING=ON \ + -DBRPC_CODEGEN_BRPC_LIBRARY="$BRPC_LIBRARY" +cmake --build /tmp/brpc-codegen-build -j2 +(cd /tmp/brpc-codegen-build && ctest --no-tests=error --output-on-failure) +``` + +Expect **two** CTests in this mode: `flatbuffers_codegen_acceptance` and +`flatbuffers_codegen_runtime`. For non-default dependency installations, pass +their prefixes through `CMAKE_PREFIX_PATH`; modern Protobuf needs its CMake +config package and Abseil. A static `libbrpc.a` is also accepted, but the caller +must supply its complete transitive/platform link requirements (including macOS +frameworks where needed) via `BRPC_CODEGEN_EXTRA_LIBRARIES`. + +The runtime test covers concurrent first descriptor access, sparse +stub/dispatch mapping, absent and present strings, invalid/foreign/forged +methods, failed request verification, null arguments, unimplemented methods, +exactly-once error callbacks (including self-deleting closures), deferred +completion, and channel ownership. It uses a local abstract channel; no network +RPC compatibility is claimed. diff --git a/tools/flatbuffers/brpc_flatc.cpp b/tools/flatbuffers/brpc_flatc.cpp new file mode 100644 index 0000000000..70c5da9b7c --- /dev/null +++ b/tools/flatbuffers/brpc_flatc.cpp @@ -0,0 +1,495 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const char kLicense[] = R"(// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +// Generated by brpc_flatc. Do not edit. + +)"; + +const char kArguments[] = + "::google::protobuf::RpcController* controller,\n" + " const ::brpc::flatbuffers::Message* request,\n" + " ::brpc::flatbuffers::Message* response,\n" + " ::google::protobuf::Closure* done"; + +struct Service { + const flatbuffers::ServiceDef* definition; + std::vector ids; +}; + +std::string JoinNamespace(const flatbuffers::Definition& definition, + const std::string& separator) { + std::string result; + if (definition.defined_namespace) { + for (const auto& component : definition.defined_namespace->components) { + if (!result.empty()) { + result += separator; + } + result += component; + } + } + return result; +} + +std::string Qualified(const flatbuffers::Definition& definition) { + const std::string ns = JoinNamespace(definition, "::"); + return "::" + (ns.empty() ? "" : ns + "::") + definition.name; +} + +std::string GuardComponent(const std::string& value) { + static const char digits[] = "0123456789ABCDEF"; + std::string result; + for (unsigned char c : value) { + result += digits[c >> 4]; + result += digits[c & 15]; + } + return result; +} + +bool IsCppIdentifier(const std::string& name) { + static const std::set keywords = { + "alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", + "bitor", "bool", "break", "case", "catch", "char", "char16_t", + "char32_t", "class", "compl", "concept", "const", "const_cast", + "consteval", "constexpr", "constinit", "continue", "co_await", + "co_return", "co_yield", "decltype", "default", "delete", "do", + "double", "dynamic_cast", "else", "enum", "explicit", "export", + "extern", "false", "float", "for", "friend", "goto", "if", + "inline", "int", "long", "mutable", "namespace", "new", + "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", + "private", "protected", "public", "register", "reinterpret_cast", + "requires", "return", "short", "signed", "sizeof", "static", + "static_assert", "static_cast", "struct", "switch", "template", + "this", "thread_local", "throw", "true", "try", "typedef", + "typeid", "typename", "union", "unsigned", "using", "virtual", + "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + }; + return !name.empty() && keywords.count(name) == 0; +} + +bool ValidateName(const flatbuffers::Definition& definition, + std::string* error) { + if (!IsCppIdentifier(definition.name)) { + *error = "C++ keyword is not supported: " + definition.name; + return false; + } + if (definition.defined_namespace) { + for (const auto& component : definition.defined_namespace->components) { + if (!IsCppIdentifier(component)) { + *error = "C++ keyword namespace is not supported: " + component; + return false; + } + } + } + return true; +} + +bool ParseId(const flatbuffers::Value& value, int32_t* id) { + if (value.constant.empty() || !flatbuffers::IsInteger(value.type.base_type)) { + return false; + } + int64_t number = 0; + for (char c : value.constant) { + if (c < '0' || c > '9') { + return false; + } + number = number * 10 + (c - '0'); + if (number > std::numeric_limits::max()) { + return false; + } + } + *id = static_cast(number); + return true; +} + +bool CollectServices(const flatbuffers::Parser& parser, + std::vector* services, std::string* error) { + for (const auto* definition : parser.services_.vec) { + // Included schemas are generated separately, just as with flatc --cpp. + if (definition->generated) { + continue; + } + if (!ValidateName(*definition, error)) { + return false; + } + Service service = {definition, {}}; + std::set ids; + if (definition->calls.vec.empty()) { + *error = "rpc_service must contain at least one method: " + + definition->name; + return false; + } + for (const auto* call : definition->calls.vec) { + if (!ValidateName(*call, error) || + !ValidateName(*call->request, error) || + !ValidateName(*call->response, error)) { + return false; + } + if (call->name == definition->name || + call->name == definition->name + "_Stub" || + call->name == "descriptor" || call->name == "GetDescriptor" || + call->name == "FBCallMethod" || call->name == "Stub" || + call->name == "channel" || call->name == "channel_" || + call->name == "owned_channel_") { + *error = "method name collides with generated API: " + call->name; + return false; + } + const auto* attribute = call->attributes.Lookup("id"); + int32_t id = 0; + if (!attribute || !ParseId(*attribute, &id)) { + *error = definition->name + "." + call->name + + " requires an explicit nonnegative int32 (id: N)"; + return false; + } + if (!ids.insert(id).second) { + *error = "duplicate method id " + std::to_string(id) + + " in " + definition->name; + return false; + } + if (call->attributes.Lookup("streaming")) { + *error = "streaming RPC is not supported: " + call->name; + return false; + } + service.ids.push_back(id); + } + services->push_back(service); + } + if (services->empty()) { + *error = "input schema contains no rpc_service to generate"; + return false; + } + return true; +} + +void OpenNamespace(const flatbuffers::Definition& definition, + std::ostream& out) { + if (definition.defined_namespace) { + for (const auto& component : definition.defined_namespace->components) { + out << "namespace " << component << " {\n"; + } + } + out << '\n'; +} + +void CloseNamespace(const flatbuffers::Definition& definition, + std::ostream& out) { + if (definition.defined_namespace) { + const auto& components = definition.defined_namespace->components; + for (auto it = components.rbegin(); it != components.rend(); ++it) { + out << "} // namespace " << *it << '\n'; + } + } + out << '\n'; +} + +void GenerateHeader(const Service& service, std::ostream& out) { + const auto& definition = *service.definition; + const std::string& name = definition.name; + OpenNamespace(definition, out); + out << "class " << name << "_Stub;\n\n" + << "class " << name << " : public ::brpc::flatbuffers::Service {\n" + << "public:\n" + << " typedef " << name << "_Stub Stub;\n" + << " static const ::brpc::flatbuffers::ServiceDescriptor* descriptor();\n" + << " const ::brpc::flatbuffers::ServiceDescriptor* GetDescriptor() override;\n" + << " void FBCallMethod(\n" + << " const ::brpc::flatbuffers::MethodDescriptor* method,\n" + << " " << kArguments << ") override;\n"; + for (const auto* call : definition.calls.vec) { + out << " virtual void " << call->name << "(\n" + << " " << kArguments << ");\n"; + } + out << "};\n\n" + << "class " << name << "_Stub : public " << name << " {\n" + << "public:\n" + << " explicit " << name << "_Stub(\n" + << " ::brpc::flatbuffers::RpcChannel* channel,\n" + << " ::brpc::flatbuffers::Service::ChannelOwnership ownership =\n" + << " ::brpc::flatbuffers::Service::STUB_DOESNT_OWN_CHANNEL);\n" + << " ::brpc::flatbuffers::RpcChannel* channel() const { return channel_; }\n"; + for (const auto* call : definition.calls.vec) { + out << " void " << call->name << "(\n" + << " " << kArguments << ") override;\n"; + } + out << "\nprivate:\n" + << " ::brpc::flatbuffers::RpcChannel* channel_;\n" + << " ::std::unique_ptr<::brpc::flatbuffers::RpcChannel> owned_channel_;\n" + << "};\n\n"; + CloseNamespace(definition, out); +} + +void GenerateSource(const Service& service, std::ostream& out) { + const auto& definition = *service.definition; + const std::string& name = definition.name; + OpenNamespace(definition, out); + out << "const ::brpc::flatbuffers::ServiceDescriptor* " << name + << "::descriptor() {\n" + << " struct Holder {\n" + << " ::brpc::flatbuffers::ServiceDescriptor value;\n" + << " Holder() {\n" + << " const ::brpc::flatbuffers::BrpcDescriptorTable table = {\n" + << " \"" << JoinNamespace(definition, ".") << "\", \"" + << name << "\",\n \""; + for (size_t i = 0; i < definition.calls.vec.size(); ++i) { + out << (i == 0 ? "" : " ") << definition.calls.vec[i]->name; + } + out << "\", {"; + for (size_t i = 0; i < service.ids.size(); ++i) { + out << (i == 0 ? "" : ", ") << service.ids[i]; + } + out << "}\n };\n" + << " if (value.init(table) != 0) {\n" + << " throw ::std::runtime_error(\"invalid generated service descriptor\");\n" + << " }\n" + << " }\n" + << " };\n" + << " static const Holder holder;\n" + << " return &holder.value;\n" + << "}\n\n" + << "const ::brpc::flatbuffers::ServiceDescriptor* " << name + << "::GetDescriptor() {\n" + << " return descriptor();\n" + << "}\n\n" + << "void " << name << "::FBCallMethod(\n" + << " const ::brpc::flatbuffers::MethodDescriptor* method,\n" + << " " << kArguments << ") {\n" + << " if (!method || method->service() != descriptor() ||\n" + << " descriptor()->FindMethodByIndex(method->index()) != method) {\n" + << " ::BrpcFlatbuffersFail(controller, done, \"invalid service method\");\n" + << " return;\n" + << " }\n" + << " if (!request || !response) {\n" + << " ::BrpcFlatbuffersFail(controller, done, \"null request or response\");\n" + << " return;\n" + << " }\n" + << " switch (method->index()) {\n"; + for (size_t i = 0; i < definition.calls.vec.size(); ++i) { + const auto& call = *definition.calls.vec[i]; + out << " case " << service.ids[i] << ":\n" + << " if (!request->Verify<" << Qualified(*call.request) + << ">()) {\n" + << " ::BrpcFlatbuffersFail(controller, done, \"invalid " + << call.request->name << " request\");\n" + << " return;\n" + << " }\n" + << " this->" << call.name << "(controller, request, response, done);\n" + << " return;\n"; + } + out << " default:\n" + << " ::BrpcFlatbuffersFail(controller, done, \"unknown method id\");\n" + << " return;\n" + << " }\n" + << "}\n\n"; + for (const auto* call : definition.calls.vec) { + out << "void " << name << "::" << call->name << "(\n" + << " " << kArguments << ") {\n" + << " (void)request;\n" + << " (void)response;\n" + << " ::BrpcFlatbuffersFail(controller, done, \"method not implemented: " + << name << "." << call->name << "\");\n" + << "}\n\n"; + } + out << name << "_Stub::" << name << "_Stub(\n" + << " ::brpc::flatbuffers::RpcChannel* channel,\n" + << " ::brpc::flatbuffers::Service::ChannelOwnership ownership)\n" + << " : channel_(channel),\n" + << " owned_channel_(ownership == ::brpc::flatbuffers::Service::STUB_OWNS_CHANNEL\n" + << " ? channel : nullptr) {}\n\n"; + for (size_t i = 0; i < definition.calls.vec.size(); ++i) { + out << "void " << name << "_Stub::" << definition.calls.vec[i]->name + << "(\n " << kArguments << ") {\n" + << " if (!channel_) {\n" + << " ::BrpcFlatbuffersFail(controller, done, \"null RPC channel\");\n" + << " return;\n" + << " }\n" + << " channel_->FBCallMethod(descriptor()->method(" << i + << "), controller, request, response, done);\n" + << "}\n\n"; + } + CloseNamespace(definition, out); +} + +bool WriteFile(const std::string& path, const std::string& contents) { + std::ofstream stream(path.c_str(), std::ios::binary | std::ios::trunc); + stream << contents; + stream.close(); + return !stream.fail(); +} + +void Usage(std::ostream& out) { + out << "Usage: brpc_flatc [-I include_dir]... [-o existing_output_dir] schema.fbs\n" + << "Run official flatc --cpp separately to produce schema_generated.h.\n"; +} + +int Run(int argc, char** argv) { + std::string input; + std::string output_dir = "."; + std::vector include_dirs; + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + Usage(std::cout); + return 0; + } + if (arg == "-o" || arg == "-I") { + if (++i == argc) { + Usage(std::cerr); + return 1; + } + if (arg == "-o") { + output_dir = argv[i]; + } else { + include_dirs.push_back(argv[i]); + } + } else if (arg.compare(0, 2, "-I") == 0 && arg.size() > 2) { + include_dirs.push_back(arg.substr(2)); + } else if (arg.empty() || arg[0] == '-' || !input.empty()) { + Usage(std::cerr); + return 1; + } else { + input = arg; + } + } + if (input.size() < 5 || input.substr(input.size() - 4) != ".fbs" || + output_dir.empty()) { + Usage(std::cerr); + return 1; + } + const size_t slash = input.find_last_of("/\\"); + const std::string basename = input.substr(slash == std::string::npos ? 0 : slash + 1); + const std::string stem = basename.substr(0, basename.size() - 4); + if (stem.empty() || stem.find_first_not_of( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-") != + std::string::npos) { + std::cerr << "brpc_flatc: unsupported schema filename\n"; + return 1; + } + include_dirs.push_back(slash == std::string::npos ? "." : + input.substr(0, slash == 0 ? 1 : slash)); + std::vector include_paths; + for (const auto& directory : include_dirs) { + include_paths.push_back(directory.c_str()); + } + include_paths.push_back(nullptr); + std::ifstream stream(input.c_str(), std::ios::binary); + if (!stream) { + std::cerr << "brpc_flatc: cannot read " << input << '\n'; + return 1; + } + const std::string schema((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + if (stream.bad() || schema.find('\0') != std::string::npos) { + std::cerr << "brpc_flatc: invalid schema input\n"; + return 1; + } + flatbuffers::Parser parser; + if (!parser.Parse(schema.c_str(), include_paths.data(), input.c_str())) { + std::cerr << parser.error_ << '\n'; + return 1; + } + std::vector services; + std::string error; + if (!CollectServices(parser, &services, &error)) { + std::cerr << "brpc_flatc: " << error << '\n'; + return 1; + } + // Coexisting service headers cannot define the same qualified service. + // Preserve filename bytes and that service identity, without tying output + // to checkout paths or folding punctuation and case into the same guard. + const std::string guard = "BRPC_FLATBUFFERS_GENERATED_" + GuardComponent(stem) + + "_" + GuardComponent(Qualified(*services.front().definition)) + "_H_"; + std::ostringstream header; + header << kLicense << "#ifndef " << guard << "\n#define " << guard << "\n\n" + << "#include \n" + << "#include \n" + << "#include \"brpc/flatbuffers/message.h\"\n" + << "#include \"brpc/flatbuffers/service.h\"\n" + << "#include \"" << stem << "_generated.h\"\n\n" + << "#if !BRPC_WITH_FLATBUFFERS\n" + << "#error \"Generated services require BRPC_WITH_FLATBUFFERS\"\n" + << "#endif\n\n"; + std::ostringstream source; + source << kLicense << "#include \"" << stem << ".brpc.fb.h\"\n\n" + << "#include \n\n" + << "namespace {\n" + << "void BrpcFlatbuffersFail(::google::protobuf::RpcController* controller,\n" + << " ::google::protobuf::Closure* done,\n" + << " const char* reason) {\n" + << " if (controller) {\n" + << " controller->SetFailed(reason);\n" + << " }\n" + << " if (done) {\n" + << " done->Run();\n" + << " }\n" + << "}\n" + << "} // namespace\n\n"; + for (const auto& service : services) { + GenerateHeader(service, header); + GenerateSource(service, source); + } + header << "#endif // " << guard << '\n'; + if (output_dir.back() != '/') { + output_dir += '/'; + } + if (!WriteFile(output_dir + stem + ".brpc.fb.h", header.str()) || + !WriteFile(output_dir + stem + ".brpc.fb.cpp", source.str())) { + std::cerr << "brpc_flatc: cannot write output in " << output_dir << '\n'; + return 1; + } + return 0; +} + +} // namespace + +int main(int argc, char** argv) { + try { + return Run(argc, argv); + } catch (const std::exception& error) { + std::cerr << "brpc_flatc: " << error.what() << '\n'; + return 1; + } +}