diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6506f5152d..6dc1891c5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: name: Detect Changed Paths runs-on: ubuntu-latest outputs: + compiler: ${{ steps.filter.outputs.compiler }} cpp: ${{ steps.filter.outputs.cpp }} cpp_code: ${{ steps.filter.outputs.cpp_code }} java_code: ${{ steps.filter.outputs.java_code }} @@ -68,6 +69,7 @@ jobs: BASE_REF: ${{ github.base_ref }} run: | if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then + echo "compiler=true" >> "$GITHUB_OUTPUT" echo "cpp=true" >> "$GITHUB_OUTPUT" echo "cpp_code=true" >> "$GITHUB_OUTPUT" echo "java_code=true" >> "$GITHUB_OUTPUT" @@ -84,6 +86,12 @@ jobs: git fetch --no-tags --depth=1 origin "$BASE_REF:refs/remotes/origin/$BASE_REF" changed_files="$(git diff --name-only "origin/$BASE_REF" HEAD)" + if grep -Eq '^(compiler/)' <<< "$changed_files"; then + echo "compiler=true" >> "$GITHUB_OUTPUT" + else + echo "compiler=false" >> "$GITHUB_OUTPUT" + fi + if grep -Eq '^(cpp/|examples/cpp/|benchmarks/cpp/|integration_tests/idl_tests/cpp/|bazel/|BUILD$|WORKSPACE$|MODULE\.bazel$|\.bazelrc$)' <<< "$changed_files"; then echo "cpp=true" >> "$GITHUB_OUTPUT" else @@ -687,8 +695,88 @@ jobs: - name: Run CI run: python ./ci/run_ci.py java --version integration_tests - grpc_tests: - name: Java/Python/Go/Rust gRPC Tests + grpc_java_python_tests: + name: Java/Python gRPC Tests + needs: changes + if: needs.changes.outputs.compiler == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: "temurin" + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + cache: "pip" + - name: Cache Maven local repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Install Java artifacts for gRPC tests + run: | + cd java + mvn -T16 --no-transfer-progress clean install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + - name: Install Python gRPC dependencies + run: | + python -m pip install "grpcio>=1.62.2,<1.71" + python -m pip install -v -e python + - name: Generate gRPC test sources + run: python integration_tests/grpc_tests/generate_grpc.py + - name: Run Java/Python gRPC Tests + run: | + cd integration_tests/grpc_tests/java + mvn -T16 --no-transfer-progress -Dtest=PythonGrpcInteropTest test + + grpc_java_go_tests: + name: Java/Go gRPC Tests + needs: changes + if: needs.changes.outputs.compiler == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: "temurin" + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + cache: "pip" + - name: Cache Maven local repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Install Java artifacts for gRPC tests + run: | + cd java + mvn -T16 --no-transfer-progress clean install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + - name: Generate gRPC test sources + run: python integration_tests/grpc_tests/generate_grpc.py + - name: Build Go gRPC peer + run: | + cd integration_tests/grpc_tests/go + go build -o grpc-interop . + - name: Run Java/Go gRPC Tests + run: | + cd integration_tests/grpc_tests/java + mvn -T16 --no-transfer-progress -Dtest=GoGrpcInteropTest test + + grpc_java_rust_tests: + name: Java/Rust gRPC Tests + needs: changes + if: needs.changes.outputs.compiler == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -718,8 +806,14 @@ jobs: run: | cd java mvn -T16 --no-transfer-progress clean install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true - - name: Run Java/Python/Go/Rust gRPC Tests - run: ./integration_tests/grpc_tests/run_tests.sh + - name: Generate gRPC test sources + run: python integration_tests/grpc_tests/generate_grpc.py + - name: Build Rust gRPC peer + run: cargo build --manifest-path integration_tests/grpc_tests/rust/Cargo.toml --workspace --quiet + - name: Run Java/Rust gRPC Tests + run: | + cd integration_tests/grpc_tests/java + mvn -T16 --no-transfer-progress -Dtest=RustGrpcInteropTest test javascript: name: JavaScript CI diff --git a/ci/release.py b/ci/release.py index a6c810988f..779d903a5f 100644 --- a/ci/release.py +++ b/ci/release.py @@ -282,6 +282,12 @@ def bump_rust_version(new_version): rust_version, _update_cargo_package_version, ) + _bump_version( + "integration_tests/grpc_tests/rust", + "Cargo.toml", + rust_version, + _update_rust_version, + ) def bump_kotlin_version(new_version): diff --git a/compiler/fory_compiler/generators/go.py b/compiler/fory_compiler/generators/go.py index c4cc57e73c..383dd8a6e3 100644 --- a/compiler/fory_compiler/generators/go.py +++ b/compiler/fory_compiler/generators/go.py @@ -207,10 +207,6 @@ def generate(self) -> List[GeneratedFile]: # Generate a single Go file with all types files.append(self.generate_file()) - # Generate gRPC service stubs if requested - if self.options.grpc: - files.extend(self.generate_services()) - return files def get_package_name(self) -> str: diff --git a/compiler/fory_compiler/generators/services/base.py b/compiler/fory_compiler/generators/services/base.py index 2bc2bcfb2d..402d5b8f89 100644 --- a/compiler/fory_compiler/generators/services/base.py +++ b/compiler/fory_compiler/generators/services/base.py @@ -18,7 +18,7 @@ "Shared utilities for gRPC service stub generators." from enum import Enum -from typing import List, Dict +from typing import Dict from fory_compiler.ir.ast import RpcMethod @@ -49,6 +49,3 @@ def __init__(self): def add(self, alias: str, import_path: str) -> None: self._imports[alias] = import_path - - def go_imports(self) -> List[str]: - return sorted(self._imports.values()) diff --git a/compiler/fory_compiler/generators/services/go.py b/compiler/fory_compiler/generators/services/go.py index 399dbe6853..264c24aed1 100644 --- a/compiler/fory_compiler/generators/services/go.py +++ b/compiler/fory_compiler/generators/services/go.py @@ -17,7 +17,7 @@ """Go gRPC service code generator.""" -from typing import List +from typing import Dict, List from fory_compiler.generators.services.base import ( ImportTracker, StreamingMode, @@ -36,8 +36,22 @@ def generate_services(self) -> List[GeneratedFile]: ] if not local_services: return [] + self._validate_method_name_collisions(local_services) return [self._generate_grpc_file(local_services)] + def _validate_method_name_collisions(self, services: List[Service]) -> None: + for service in services: + seen: Dict[str, str] = {} + for method in service.methods: + go_name = self.to_pascal_case(method.name) + prior = seen.get(go_name) + if prior is not None: + raise ValueError( + f"Go gRPC method name collision in service {service.name}: " + f"{prior} and {method.name} both generate {go_name}" + ) + seen[go_name] = method.name + def _generate_grpc_file(self, services: List[Service]) -> GeneratedFile: """Generate one _grpc.go file containing all services in the schema.""" lines: List[str] = [] @@ -85,7 +99,6 @@ def _build_import_block(self, tracker: ImportTracker) -> List[str]: '"google.golang.org/grpc/codes"', '"google.golang.org/grpc/mem"', '"google.golang.org/grpc/status"', - '"github.com/apache/fory/go/fory"', ] for alias, path in tracker._imports.items(): @@ -142,7 +155,6 @@ def _generate_client_struct(self, service: Service) -> List[str]: lines: List[str] = [] lines.append(f"type {self.to_camel_case(service.name)}Client struct {{") lines.append("\tcc grpc.ClientConnInterface") - lines.append("\tfory *fory.Fory") lines.append("}") lines.append("") return lines @@ -150,11 +162,9 @@ def _generate_client_struct(self, service: Service) -> List[str]: def _generate_new_client(self, service: Service) -> List[str]: lines: List[str] = [] lines.append( - f"func New{service.name}Client(cc grpc.ClientConnInterface, f *fory.Fory) {service.name}Client {{" - ) - lines.append( - f"\treturn &{self.to_camel_case(service.name)}Client{{cc: cc, fory: f}}" + f"func New{service.name}Client(cc grpc.ClientConnInterface) {service.name}Client {{" ) + lines.append(f"\treturn &{self.to_camel_case(service.name)}Client{{cc: cc}}") lines.append("}") lines.append("") return lines @@ -164,31 +174,24 @@ def _generate_codec(self) -> List[str]: lines.append( "// CodecV2 implements grpc/encoding.CodecV2 using Fory serialization." ) - lines.append( - "// Pass a configured *fory.Fory instance with all message types registered." - ) - lines.append("type CodecV2 struct {") - lines.append("\tFory *fory.Fory") - lines.append("}") + lines.append("// It uses the generated package-level thread-safe Fory runtime.") + lines.append("type CodecV2 struct{}") lines.append("") lines.append( - "// Marshal serializes v with Fory. The result is copied before being handed" + "// Marshal serializes v with Fory. The generated thread-safe runtime returns" ) lines.append( - "// to gRPC because Fory reuses its internal write buffer across calls —" + "// a stable copy before releasing pooled Fory instances, so gRPC never sees" ) lines.append( - "// streaming handlers may buffer multiple frames before sending, and without" + "// the reusable internal buffers owned by plain fory.Fory runtimes." ) - lines.append("// a copy all frames would alias the last serialized value.") lines.append("func (c CodecV2) Marshal(v any) (mem.BufferSlice, error) {") - lines.append("\tb, err := c.Fory.Marshal(v)") + lines.append("\tb, err := getFory().Serialize(v)") lines.append("\tif err != nil {") lines.append("\t\treturn nil, err") lines.append("\t}") - lines.append("\tout := make([]byte, len(b))") - lines.append("\tcopy(out, b)") - lines.append("\treturn mem.BufferSlice{mem.NewBuffer(&out, nil)}, nil") + lines.append("\treturn mem.BufferSlice{mem.NewBuffer(&b, nil)}, nil") lines.append("}") lines.append("") lines.append( @@ -204,7 +207,7 @@ def _generate_codec(self) -> List[str]: lines.append("\tfor _, buf := range data {") lines.append("\t\tn += copy(b[n:], buf.ReadOnlyData())") lines.append("\t}") - lines.append("\treturn c.Fory.Unmarshal(b, v)") + lines.append("\treturn getFory().Deserialize(b, v)") lines.append("}") lines.append("") lines.append( @@ -229,7 +232,7 @@ def _generate_client_methods( ) lines.append(f"\tout := new({res_type[1:]})") lines.append( - "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)" + "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)" ) lines.append( f'\terr := c.cc.Invoke(ctx, "{self.get_grpc_method_path(service, method)}", in, out, callOpts...)' @@ -245,7 +248,7 @@ def _generate_client_methods( f"func (c *{self.to_camel_case(service.name)}Client) {self.to_pascal_case(method.name)}(ctx context.Context, in {req_type}, opts ...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, error) {{" ) lines.append( - "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)" + "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)" ) lines.append( f'\tstream, err := c.cc.NewStream(ctx, &_{service.name}_serviceDesc.Streams[{stream_index}], "{self.get_grpc_method_path(service, method)}", callOpts...)' @@ -271,7 +274,7 @@ def _generate_client_methods( f"func (c *{self.to_camel_case(service.name)}Client) {self.to_pascal_case(method.name)}(ctx context.Context, opts ...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, error) {{" ) lines.append( - "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{Fory: c.fory})}, opts...)" + "\tcallOpts := append([]grpc.CallOption{grpc.ForceCodecV2(CodecV2{})}, opts...)" ) lines.append( f'\tstream, err := c.cc.NewStream(ctx, &_{service.name}_serviceDesc.Streams[{stream_index}], "{self.get_grpc_method_path(service, method)}", callOpts...)' @@ -683,9 +686,7 @@ def _generate_unary_type_desc(self, service: Service) -> List[str]: mode = streaming_mode(method) if mode is StreamingMode.UNARY: lines.append("\t\t{") - lines.append( - f'\t\t\tMethodName:\t"{self.to_pascal_case(method.name)}",' - ) + lines.append(f'\t\t\tMethodName:\t"{method.name}",') lines.append( f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler," ) @@ -700,9 +701,7 @@ def _generate_stream_type_desc(self, service: Service) -> List[str]: continue else: lines.append("\t\t{") - lines.append( - f'\t\t\tStreamName:\t"{self.to_pascal_case(method.name)}",' - ) + lines.append(f'\t\t\tStreamName:\t"{method.name}",') lines.append( f"\t\t\tHandler:\t_{service.name}_{self.to_pascal_case(method.name)}_Handler," ) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 8170b9988a..2fb7ad62b4 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -237,19 +237,59 @@ def test_go_grpc_service_codegen(): files = generate_service_files(schema, GoGenerator) assert len(files) == 1 content = next(iter(files.values())) - assert "func NewGreeterClient(" in content + assert "func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient" in content assert "func RegisterGreeterServer(" in content assert "type GreeterClient interface" in content assert "type GreeterServer interface" in content assert "type UnimplementedGreeterServer struct" in content - assert "CodecV2{Fory: c.fory}" in content - assert "type CodecV2 struct" in content + assert "CodecV2{}" in content + assert "type CodecV2 struct{}" in content assert "func (c CodecV2) Marshal(v any) (mem.BufferSlice, error)" in content assert "func (c CodecV2) Unmarshal(data mem.BufferSlice, v any) error" in content + assert "getFory().Serialize(v)" in content + assert "getFory().Deserialize(b, v)" in content + assert "*fory.Fory" not in content + assert "c.fory" not in content assert '"/demo.greeter.Greeter/SayHello"' in content assert "mustEmbedUnimplementedGreeterServer()" in content +def test_go_grpc_service_desc_uses_idl_method_names(): + schema = parse_fdl( + dedent( + """ + package demo.routes; + + message Req {} + message Res {} + + service Router { + rpc sayHello (Req) returns (Res); + rpc stream_replies (Req) returns (stream Res); + rpc clientTalk (stream Req) returns (Res); + rpc bidi_chat (stream Req) returns (stream Res); + } + """ + ) + ) + + content = next(iter(generate_service_files(schema, GoGenerator).values())) + assert "SayHello(ctx context.Context" in content + assert "StreamReplies(ctx context.Context" in content + assert "ClientTalk(ctx context.Context" in content + assert "BidiChat(ctx context.Context" in content + assert '"/demo.routes.Router/sayHello"' in content + assert '"/demo.routes.Router/stream_replies"' in content + assert '"/demo.routes.Router/clientTalk"' in content + assert '"/demo.routes.Router/bidi_chat"' in content + assert 'MethodName:\t"sayHello"' in content + assert 'StreamName:\t"stream_replies"' in content + assert 'StreamName:\t"clientTalk"' in content + assert 'StreamName:\t"bidi_chat"' in content + assert 'MethodName:\t"SayHello"' not in content + assert 'StreamName:\t"StreamReplies"' not in content + + def test_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( @@ -626,6 +666,17 @@ def test_grpc_method_name_collisions_fail(): else: raise AssertionError("Expected Rust gRPC method name collision") + go_generator = GoGenerator( + schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True) + ) + try: + go_generator.generate_services() + except ValueError as e: + assert "Go gRPC method name collision" in str(e) + assert "Foo and foo" in str(e) + else: + raise AssertionError("Expected Go gRPC method name collision") + def test_java_python_grpc_method_keywords_are_safe_names(): schema = parse_fdl( @@ -778,18 +829,21 @@ def test_service_schema_produces_one_file_per_message_per_language(): ) -def test_compile_service_schema_with_grpc_flag(tmp_path: Path): +def test_compile_service_schema_with_grpc_flag(tmp_path: Path, capsys): example_path = Path(__file__).resolve().parents[2] / "examples" / "service.fdl" lang_dirs = {} for lang in ("java", "python", "rust", "go", "cpp", "csharp", "swift"): lang_dirs[lang] = tmp_path / lang ok = compile_file(example_path, lang_dirs, grpc=True, generated_outputs={}) + output = capsys.readouterr().out assert ok is True for lang, lang_dir in lang_dirs.items(): files = [p for p in lang_dir.rglob("*") if p.is_file()] assert len(files) >= 1, f"{lang}: expected at least one file with grpc=True" assert (lang_dirs["java"] / "demo" / "greeter" / "GreeterGrpc.java").exists() assert (lang_dirs["python"] / "demo_greeter_grpc.py").exists() + assert (lang_dirs["go"] / "demo_greeter_grpc.go").exists() + assert output.count("demo_greeter_grpc.go") == 1 assert (lang_dirs["rust"] / "demo_greeter_service.rs").exists() assert (lang_dirs["rust"] / "demo_greeter_service_grpc.rs").exists() diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md index 7204116f14..dbad7568f3 100644 --- a/docs/compiler/compiler-guide.md +++ b/docs/compiler/compiler-guide.md @@ -52,27 +52,27 @@ foryc --scan-generated [OPTIONS] Compile options: -| Option | Description | Default | -| ------------------------------------- | ----------------------------------------------------- | ------------- | -| `--lang` | Comma-separated target languages | `all` | -| `--output`, `-o` | Output directory | `./generated` | -| `-I`, `--proto_path`, `--import_path` | Add directory to import search path (can be repeated) | (none) | -| `--java_out=DST_DIR` | Generate Java code in DST_DIR | (none) | -| `--python_out=DST_DIR` | Generate Python code in DST_DIR | (none) | -| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR | (none) | -| `--go_out=DST_DIR` | Generate Go code in DST_DIR | (none) | -| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR | (none) | -| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR | (none) | -| `--javascript_out=DST_DIR` | Generate JavaScript/TypeScript code in DST_DIR | (none) | -| `--swift_out=DST_DIR` | Generate Swift code in DST_DIR | (none) | -| `--dart_out=DST_DIR` | Generate Dart code in DST_DIR | (none) | -| `--scala_out=DST_DIR` | Generate Scala 3 code in DST_DIR | (none) | -| `--kotlin_out=DST_DIR` | Generate Kotlin code in DST_DIR | (none) | -| `--go_nested_type_style` | Go nested type naming: `camelcase` or `underscore` | `underscore` | -| `--swift_namespace_style` | Swift namespace style: `enum` or `flatten` | `enum` | -| `--emit-fdl` | Emit translated FDL (for non-FDL inputs) | `false` | -| `--emit-fdl-path` | Write translated FDL to this path (file or directory) | (stdout) | -| `--grpc` | Generate gRPC service companions for Java and Python | `false` | +| Option | Description | Default | +| ------------------------------------- | ------------------------------------------------------ | ------------- | +| `--lang` | Comma-separated target languages | `all` | +| `--output`, `-o` | Output directory | `./generated` | +| `-I`, `--proto_path`, `--import_path` | Add directory to import search path (can be repeated) | (none) | +| `--java_out=DST_DIR` | Generate Java code in DST_DIR | (none) | +| `--python_out=DST_DIR` | Generate Python code in DST_DIR | (none) | +| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR | (none) | +| `--go_out=DST_DIR` | Generate Go code in DST_DIR | (none) | +| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR | (none) | +| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR | (none) | +| `--javascript_out=DST_DIR` | Generate JavaScript/TypeScript code in DST_DIR | (none) | +| `--swift_out=DST_DIR` | Generate Swift code in DST_DIR | (none) | +| `--dart_out=DST_DIR` | Generate Dart code in DST_DIR | (none) | +| `--scala_out=DST_DIR` | Generate Scala 3 code in DST_DIR | (none) | +| `--kotlin_out=DST_DIR` | Generate Kotlin code in DST_DIR | (none) | +| `--go_nested_type_style` | Go nested type naming: `camelcase` or `underscore` | `underscore` | +| `--swift_namespace_style` | Swift namespace style: `enum` or `flatten` | `enum` | +| `--emit-fdl` | Emit translated FDL (for non-FDL inputs) | `false` | +| `--emit-fdl-path` | Write translated FDL to this path (file or directory) | (stdout) | +| `--grpc` | Generate gRPC service companions for supported outputs | `false` | Schema-level file options are supported for language-specific generation choices. For `go_nested_type_style` and `swift_namespace_style`, the CLI flag overrides @@ -141,23 +141,23 @@ foryc schema.fdl --output ./src/generated foryc user.fdl order.fdl product.fdl --output ./generated ``` -**Compile a simple schema containing service definitions (Java + Python models):** +**Compile a simple schema containing service definitions (Java + Python + Rust models):** ```bash -foryc compiler/examples/service.fdl --java_out=./generated/java --python_out=./generated/python +foryc compiler/examples/service.fdl --java_out=./generated/java --python_out=./generated/python --rust_out=./generated/rust ``` -**Generate Java and Python gRPC service companions:** +**Generate Java, Python, and Rust gRPC service companions:** ```bash -foryc compiler/examples/service.fdl --java_out=./generated/java --python_out=./generated/python --grpc +foryc compiler/examples/service.fdl --java_out=./generated/java --python_out=./generated/python --rust_out=./generated/rust --grpc ``` The generated gRPC service code uses Fory to serialize request and response -payloads. Java output imports grpc-java APIs and Python output imports `grpc`; -applications that compile or run those generated service files must provide -their own gRPC dependencies. Fory's Java and Python packages do not add a hard -gRPC dependency for this feature. +payloads. Java output imports grpc-java APIs, Python output imports `grpc`, and +Rust output imports `tonic` and `bytes`; applications that compile or run those +generated service files must provide their own gRPC dependencies. Fory packages +do not add a hard gRPC dependency for this feature. **Use import search paths:** diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md index 2744ba6ae7..45e88e10af 100644 --- a/docs/compiler/flatbuffers-idl.md +++ b/docs/compiler/flatbuffers-idl.md @@ -125,8 +125,9 @@ message Container { ### Services FlatBuffers `rpc_service` definitions are translated to Fory services. With -`--grpc`, the compiler emits Java and Python gRPC service companions that use -Fory serialization for request and response payloads. +`--grpc`, the compiler emits gRPC service companions for supported outputs such +as Java, Python, Go, and Rust. These companions use Fory serialization for +request and response payloads. ```fbs rpc_service SearchService { @@ -136,12 +137,12 @@ rpc_service SearchService { ``` ```bash -foryc api.fbs --java_out=./generated/java --python_out=./generated/python --grpc +foryc api.fbs --java_out=./generated/java --python_out=./generated/python --rust_out=./generated/rust --grpc ``` -Generated service code imports grpc APIs, so applications must provide grpc-java -or `grpcio` dependencies when they compile or run those files. Fory packages do -not add gRPC as a hard dependency. +Generated service code imports grpc APIs, so applications must provide grpc-java, +`grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies when they compile or +run those files. Fory packages do not add gRPC as a hard dependency. ### Defaults and Metadata diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md index 567b9c57ff..1764c47a14 100644 --- a/docs/compiler/generated-code.md +++ b/docs/compiler/generated-code.md @@ -404,6 +404,11 @@ Rust output is one module file per schema, for example: - `/addressbook.rs` +When `--grpc` is used and the schema contains services, Rust also emits: + +- `/addressbook_service.rs` +- `/addressbook_service_grpc.rs` + ### Type Generation Unions map to Rust enums with `#[fory(id = ...)]` schema case attributes. @@ -516,6 +521,51 @@ let bytes = person.to_bytes()?; let restored = Person::from_bytes(&bytes)?; ``` +### gRPC Service Companions + +When a schema contains services and the compiler is run with `--grpc`, Rust +generation emits a service API module and a tonic binding module. For a schema +module named `addressbook`, those files are `addressbook_service.rs` and +`addressbook_service_grpc.rs`. + +The service API module contains the async trait and gRPC path constants: + +```rust +#[::tonic::async_trait] +pub trait AddressBookService: ::std::marker::Send + ::std::marker::Sync + 'static { + async fn lookup( + &self, + request: ::tonic::Request, + ) -> ::std::result::Result< + ::tonic::Response, + ::tonic::Status, + >; +} + +pub const ADDRESS_BOOK_SERVICE_SERVICE_NAME: &str = "addressbook.AddressBookService"; +pub const ADDRESS_BOOK_SERVICE_LOOKUP_PATH: &str = "/addressbook.AddressBookService/Lookup"; +``` + +The tonic binding module contains Fory-backed codecs, payload implementations, +and client/server wrappers. It serializes each request or response with the +generated model type's `to_bytes` and `from_bytes` helpers: + +```rust +impl codec::ForyGrpcPayload for crate::addressbook::Person { + fn encode_fory_payload(&self) -> ::std::result::Result<::std::vec::Vec, ::fory::Error> { + self.to_bytes() + } + + fn decode_fory_payload(payload: &[u8]) -> ::std::result::Result { + Self::from_bytes(payload) + } +} +``` + +Applications compiling the generated Rust service files must provide `tonic` and +`bytes` dependencies; Fory's Rust crate does not add those gRPC dependencies as +hard dependencies. + ## C++ ### Output Layout @@ -776,6 +826,42 @@ if err := restored.FromBytes(data); err != nil { } ``` +### gRPC Service Companions + +When a schema contains services and the compiler is run with `--grpc`, Go +generation emits one `_grpc.go` file next to the model file. The +companion contains grpc-go client and server interfaces plus a Fory-backed +`CodecV2`. + +```go +type AddressBookServiceClient interface { + Lookup(ctx context.Context, in *Person, opts ...grpc.CallOption) (*AddressBook, error) +} + +func NewAddressBookServiceClient(cc grpc.ClientConnInterface) AddressBookServiceClient { ... } + +type CodecV2 struct{} +``` + +The generated codec uses the same package-level thread-safe Fory runtime as the +generated `ToBytes` and `FromBytes` helpers. Applications should pass +`CodecV2{}` to grpc-go server options, and generated clients force the same +codec on each call: + +```go +server := grpc.NewServer(grpc.ForceServerCodecV2(addressbook.CodecV2{})) +addressbook.RegisterAddressBookServiceServer(server, service) + +client := addressbook.NewAddressBookServiceClient(conn) +``` + +Go method names are exported as PascalCase identifiers, while the gRPC method +path keeps the exact service and method names from the schema. Regenerate both +peers after changing service or method names. + +Applications compiling these files must provide grpc-go dependencies; Fory Go +packages do not add gRPC as a hard dependency. + ## C\# ### Output Layout diff --git a/docs/compiler/index.md b/docs/compiler/index.md index be9990e012..0bcc5ba6fe 100644 --- a/docs/compiler/index.md +++ b/docs/compiler/index.md @@ -23,9 +23,9 @@ Fory IDL is a schema definition language for Apache Fory that enables type-safe cross-language serialization. Define your data structures once and generate native data structure code for Java, Python, C++, Go, Rust, JavaScript/TypeScript, C#, Swift, Dart, Scala, and Kotlin. Fory IDL can also -describe RPC services; for Java and Python, the compiler can generate gRPC -service companions that use Fory serialization for request and response -payloads. +describe RPC services; for Java, Python, Go, and Rust, the compiler can +generate gRPC service companions that use Fory serialization for request and +response payloads. ## Example Schema @@ -88,15 +88,16 @@ service AnimalService { } ``` -Generate Java and Python models plus gRPC service companions with: +Generate Java, Python, and Rust models plus gRPC service companions with: ```bash -foryc animals.fdl --java_out=./generated/java --python_out=./generated/python --grpc +foryc animals.fdl --java_out=./generated/java --python_out=./generated/python --rust_out=./generated/rust --grpc ``` The generated service code uses normal gRPC APIs, but request and response -objects are serialized with Fory. Applications provide their own grpc-java or -`grpcio` dependencies; Fory packages do not add gRPC as a hard dependency. +objects are serialized with Fory. Applications provide their own grpc-java, +`grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies; Fory packages do +not add gRPC as a hard dependency. ## Why Fory IDL? diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md index a683f8cd23..66e0e4bc1e 100644 --- a/docs/compiler/protobuf-idl.md +++ b/docs/compiler/protobuf-idl.md @@ -49,13 +49,13 @@ how protobuf concepts map to Fory, and how to use protobuf-only Fory extension o | Circular refs | Not supported | Supported | | Unknown fields | Preserved | Not preserved | | Generated types | Protobuf-specific model types | Native language constructs | -| gRPC ecosystem | Native | Java/Python service codegen | +| gRPC ecosystem | Native | Java/Python/Go/Rust service codegen | -Fory can generate Java and Python gRPC service companions with `--grpc`. Those -services use normal gRPC transports but serialize request and response payloads -with Fory rather than protobuf. For broad gRPC ecosystem tooling, schema -reflection, and protobuf-native interceptors, protobuf remains the mature/default -choice. +Fory can generate Java, Python, Go, and Rust gRPC service companions with +`--grpc`. Those services use normal gRPC transports but serialize request and +response payloads with Fory rather than protobuf. For broad gRPC ecosystem +tooling, schema reflection, and protobuf-native interceptors, protobuf remains +the mature/default choice. ## Why Use Apache Fory @@ -311,17 +311,18 @@ modifiers (and optional `ref(weak=true)` where needed). Replace protobuf generation steps with the Fory compiler invocation for target languages. -For Java and Python services, add `--grpc` to emit gRPC companion code: +For supported service outputs, add `--grpc` to emit gRPC companion code: ```bash -foryc api.proto --java_out=./generated/java --python_out=./generated/python --grpc +foryc api.proto --java_out=./generated/java --python_out=./generated/python --rust_out=./generated/rust --grpc ``` -Generated Java service files compile against grpc-java, and generated Python -service modules import `grpc`. Add those dependencies in your application build; -Fory packages do not add gRPC as a hard dependency. Protobuf `oneof` fields are -translated to Fory union fields inside request and response messages. -Direct union RPC request or response types are not part of normal protobuf RPC +Generated Java service files compile against grpc-java, generated Python service +modules import `grpc`, and generated Rust service files import `tonic` and +`bytes`. Add those dependencies in your application build; Fory packages do not +add gRPC as a hard dependency. Protobuf `oneof` fields are translated to Fory +union fields inside request and response messages. Direct union RPC request or +response types are not part of normal protobuf RPC syntax. ### Step 5: Run Compatibility Checks diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md index c3d951b690..c59bbe4c6b 100644 --- a/docs/compiler/schema-idl.md +++ b/docs/compiler/schema-idl.md @@ -907,7 +907,8 @@ union_field := ['repeated'] field_type IDENTIFIER '=' INTEGER [field_options] '; Services define RPC method contracts in Fory IDL. They are optional: schemas with services still generate the normal data model types, and gRPC service code -is generated only when the compiler is run with `--grpc` for Java or Python. +is generated only when the compiler is run with `--grpc` for supported language +outputs such as Java, Python, Go, and Rust. ```protobuf message GetPetRequest [id=200] { @@ -947,9 +948,10 @@ service PetDirectory { - Enum, primitive, collection, map, and array types are not valid direct RPC request or response types. Wrap those values in a message when they are part of a service contract. -- The generated Java and Python gRPC companions use Fory serialization for each - RPC payload. Applications that compile or run those companions provide their - own grpc-java or `grpcio` dependency. +- The generated gRPC companions use Fory serialization for each RPC payload. + Applications that compile or run those companions provide their own gRPC + dependency, such as grpc-java, `grpcio`, grpc-go, or Rust `tonic` and + `bytes`. **Grammar:** diff --git a/docs/guide/go/grpc-support.md b/docs/guide/go/grpc-support.md new file mode 100644 index 0000000000..02418eba1c --- /dev/null +++ b/docs/guide/go/grpc-support.md @@ -0,0 +1,241 @@ +--- +title: gRPC Support +sidebar_position: 13 +id: grpc_support +license: | + 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. +--- + +Fory can generate Go gRPC service companions for schemas that define services. +The generated code uses grpc-go for transport and a Fory-backed `CodecV2` for +request and response payloads. + +Use this mode when every RPC peer is generated from the same Fory IDL, protobuf +IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload +encoding. Use standard protobuf gRPC code generation when clients or tools must +consume protobuf message bytes directly. + +## Add Dependencies + +Add grpc-go to your module. Fory Go packages do not add gRPC as a hard +dependency. + +```bash +go get google.golang.org/grpc +``` + +Your generated code also imports the Fory Go module: + +```bash +go get github.com/apache/fory/go/fory +``` + +## Define a Service + +Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers +`rpc_service` definitions. A Fory IDL service looks like this: + +```protobuf +package demo.greeter; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string reply = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} +``` + +Generate Go model and gRPC companion code with `--grpc`: + +```bash +foryc service.fdl --go_out=./generated/go --grpc +``` + +For this schema, the Go generator emits: + +| File | Purpose | +| ------------------------------ | -------------------------------------------- | +| `greeter/demo_greeter.go` | Fory model types and registration helpers | +| `greeter/demo_greeter_grpc.go` | grpc-go client, server interfaces, and codec | + +Generated Go methods use exported PascalCase names such as `SayHello`. The +underlying gRPC method path keeps the exact schema method name, so names such as +`sayHello` or `say_hello` continue to route by their schema spelling. + +## Implement a Server + +Implement the generated `GreeterServer` interface, create a grpc-go server with +the generated Fory codec, and register the service. + +```go +package main + +import ( + "context" + "log" + "net" + + "google.golang.org/grpc" + + "example.com/app/generated/go/greeter" +) + +type greeterService struct { + greeter.UnimplementedGreeterServer +} + +func (greeterService) SayHello( + ctx context.Context, + request *greeter.HelloRequest, +) (*greeter.HelloReply, error) { + return &greeter.HelloReply{Reply: "Hello, " + request.Name}, nil +} + +func main() { + listener, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatal(err) + } + + server := grpc.NewServer( + grpc.ForceServerCodecV2(greeter.CodecV2{}), + ) + greeter.RegisterGreeterServer(server, greeterService{}) + + if err := server.Serve(listener); err != nil { + log.Fatal(err) + } +} +``` + +`grpc.ForceServerCodecV2(...)` is required so the server decodes incoming frames +with the generated Fory codec instead of the default protobuf codec. + +Use the zero-value generated `CodecV2{}` for the service schema. The generated +client methods force the same codec for outgoing calls. + +## Create a Client + +The generated client constructor accepts a grpc-go connection. Generated client +methods force the generated Fory codec for each call. + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "example.com/app/generated/go/greeter" +) + +func main() { + conn, err := grpc.NewClient( + "localhost:50051", + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + client := greeter.NewGreeterClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + reply, err := client.SayHello(ctx, &greeter.HelloRequest{Name: "Fory"}) + if err != nil { + log.Fatal(err) + } + fmt.Println(reply.Reply) +} +``` + +## Streaming RPCs + +Fory service definitions can use unary, server-streaming, client-streaming, and +bidirectional streaming RPC shapes: + +```protobuf +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc LotsOfReplies (HelloRequest) returns (stream HelloReply); + rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply); + rpc Chat (stream HelloRequest) returns (stream HelloReply); +} +``` + +Generated Go code follows grpc-go conventions: + +- Unary methods take `context.Context`, a request pointer, and return a response + pointer plus `error`. +- Server-streaming client methods return a generated stream client. +- Client-streaming server methods receive a generated stream server. +- Bidirectional streaming methods use generated stream client and server + interfaces. +- The generated codec is used for every message frame, including streaming + frames. + +## Operations + +The generated service companion only supplies Fory serialization. Operational +behavior remains standard grpc-go behavior: + +- Deadlines and cancellations +- TLS and credentials +- Unary and stream interceptors +- Status codes and metadata +- Name resolution and load balancing +- Connection lifecycle and backoff + +## Troubleshooting + +### Missing `google.golang.org/grpc` Packages + +Add grpc-go to your module: + +```bash +go get google.golang.org/grpc +``` + +### `grpc: error while marshaling` + +Confirm that both the client and server use the generated `CodecV2{}` and that +the generated model file is compiled into the same package as the gRPC companion. + +### `UNIMPLEMENTED` + +Confirm that the generated service was registered with +`RegisterGreeterServer(...)`, and that the client and server were generated from +the same package, service, and method names. + +### Protobuf Clients Cannot Decode the Service + +Fory gRPC companions do not use protobuf wire encoding for messages. Use a +Fory-generated client for Fory-generated services, or provide a separate +protobuf service endpoint for generic protobuf clients. diff --git a/docs/guide/go/index.md b/docs/guide/go/index.md index 52f162fcd3..2cf4ebaf40 100644 --- a/docs/guide/go/index.md +++ b/docs/guide/go/index.md @@ -150,6 +150,7 @@ See [Xlang Serialization](xlang-serialization.md) for type mapping and compatibi | [Schema Evolution](schema-evolution.md) | Forward/backward compatibility | | [Custom Serializers](custom-serializers.md) | Extend serialization behavior | | [Thread Safety](thread-safety.md) | Concurrent usage patterns | +| [gRPC Support](grpc-support.md) | Fory payloads over grpc-go | | [Troubleshooting](troubleshooting.md) | Common issues and solutions | ## Related Resources diff --git a/docs/guide/go/troubleshooting.md b/docs/guide/go/troubleshooting.md index 7bcfb2d9f8..2c538385e2 100644 --- a/docs/guide/go/troubleshooting.md +++ b/docs/guide/go/troubleshooting.md @@ -1,6 +1,6 @@ --- title: Troubleshooting -sidebar_position: 13 +sidebar_position: 14 id: troubleshooting license: | Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/docs/guide/java/grpc-support.md b/docs/guide/java/grpc-support.md new file mode 100644 index 0000000000..bfa1781e3e --- /dev/null +++ b/docs/guide/java/grpc-support.md @@ -0,0 +1,236 @@ +--- +title: gRPC Support +sidebar_position: 17 +id: grpc_support +license: | + 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. +--- + +Fory can generate Java gRPC service companions for schemas that define +services. The generated service code uses normal grpc-java channels, servers, +deadlines, status codes, interceptors, and transport security, while request +and response objects are serialized with Fory instead of protobuf. + +Use this mode when both sides of the RPC are generated from the same Fory IDL, +protobuf IDL, or FlatBuffers IDL and you want gRPC transport semantics with +Fory payload encoding. Use standard protobuf gRPC code generation when your API +must be consumed by generic protobuf clients, reflection tools, or components +that expect protobuf message bytes. + +## Add Dependencies + +The generated Java service files compile against grpc-java. Fory Java artifacts +do not add gRPC as a hard dependency, so add grpc-java dependencies in your +application build and align the version with the rest of your service stack. + +Maven: + +```xml + + + org.apache.fory + fory-core + ${fory.version} + + + io.grpc + grpc-api + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-netty-shaded + ${grpc.version} + + +``` + +Gradle: + +```kotlin +dependencies { + implementation("org.apache.fory:fory-core:$foryVersion") + implementation("io.grpc:grpc-api:$grpcVersion") + implementation("io.grpc:grpc-stub:$grpcVersion") + implementation("io.grpc:grpc-netty-shaded:$grpcVersion") +} +``` + +## Define a Service + +Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers +`rpc_service` definitions. A Fory IDL service looks like this: + +```protobuf +package demo.greeter; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string reply = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} +``` + +Generate Java model and gRPC companion code with `--grpc`: + +```bash +foryc service.fdl --java_out=./generated/java --grpc +``` + +For this schema, the Java generator emits: + +| File | Purpose | +| ------------------------ | -------------------------------------------- | +| `HelloRequest.java` | Fory model type for the request | +| `HelloReply.java` | Fory model type for the response | +| `GreeterForyModule.java` | Fory registration module for generated types | +| `GreeterGrpc.java` | grpc-java service base class, stubs, codecs | + +## Implement a Server + +Extend the generated `GreeterGrpc.GreeterImplBase` class and register it with a +standard grpc-java `Server`. + +```java +package demo.greeter; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; + +final class GreeterService extends GreeterGrpc.GreeterImplBase { + @Override + public void sayHello( + HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = new HelloReply(); + reply.setReply("Hello, " + request.getName()); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } +} + +public final class GreeterServer { + public static void main(String[] args) throws Exception { + Server server = + ServerBuilder.forPort(50051) + .addService(new GreeterService()) + .build() + .start(); + server.awaitTermination(); + } +} +``` + +Generated request and response types are registered by the generated code, so +service implementations do not perform manual serializer registration. + +## Create a Client + +Use the generated stubs with an ordinary grpc-java channel: + +```java +package demo.greeter; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + +public final class GreeterClient { + public static void main(String[] args) { + ManagedChannel channel = + ManagedChannelBuilder.forAddress("localhost", 50051) + .usePlaintext() + .build(); + try { + GreeterGrpc.GreeterBlockingStub stub = + GreeterGrpc.newBlockingStub(channel); + + HelloRequest request = new HelloRequest(); + request.setName("Fory"); + HelloReply reply = stub.sayHello(request); + System.out.println(reply.getReply()); + } finally { + channel.shutdownNow(); + } + } +} +``` + +For asynchronous calls, use `GreeterGrpc.newStub(channel)`. For future-based +unary calls, use `GreeterGrpc.newFutureStub(channel)`. + +## Streaming RPCs + +Fory service definitions can use the same gRPC streaming shapes: + +```protobuf +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc LotsOfReplies (HelloRequest) returns (stream HelloReply); + rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply); + rpc Chat (stream HelloRequest) returns (stream HelloReply); +} +``` + +Generated Java service methods follow grpc-java conventions: + +- Unary and server-streaming methods receive a request object and a + `StreamObserver` for responses. +- Client-streaming and bidirectional methods return a `StreamObserver` for + incoming requests and receive a `StreamObserver` for outgoing responses. +- Blocking stubs expose the grpc-java blocking APIs for supported streaming + shapes. + +## Operations + +The generated service code only replaces request and response serialization. +All normal gRPC operational features still belong to grpc-java: + +- Deadlines and cancellations +- TLS and authentication +- Name resolution and load balancing +- Client and server interceptors +- Status codes and metadata +- Channel pooling and lifecycle management + +## Troubleshooting + +### Missing `io.grpc` or Guava Classes + +Add the grpc-java dependencies shown above. Generated Fory service files import +grpc-java APIs, but Fory Java artifacts intentionally do not depend on gRPC. + +### `UNIMPLEMENTED` + +Confirm that the generated service implementation is registered with +`ServerBuilder.addService(...)`, and that the client and server were generated +from the same package, service, and method names. + +### Protobuf Clients Cannot Decode the Service + +Fory gRPC companions do not use protobuf wire encoding for messages. Use a +Fory-generated client for Fory-generated services, or provide a separate +protobuf service endpoint for generic protobuf clients. diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md index ac8ed7f460..580701f477 100644 --- a/docs/guide/java/index.md +++ b/docs/guide/java/index.md @@ -244,6 +244,7 @@ ThreadSafeFory threadLocalFory = Fory.builder() - [Object Copy](object-copy.md) - Deep-copy Java object graphs in memory - [Compression](compression.md) - Integer, long, and array compression options - [Virtual Threads](virtual-threads.md) - Virtual-thread usage and pool sizing guidance +- [gRPC Support](grpc-support.md) - Fory payloads over grpc-java - [Type Registration](type-registration.md) - Class registration and security - [Custom Serializers](custom-serializers.md) - Implement custom serializers - [Xlang Serialization](xlang-serialization.md) - Serialize data for other languages diff --git a/docs/guide/java/troubleshooting.md b/docs/guide/java/troubleshooting.md index 141f645b92..2795f83762 100644 --- a/docs/guide/java/troubleshooting.md +++ b/docs/guide/java/troubleshooting.md @@ -1,6 +1,6 @@ --- title: Troubleshooting -sidebar_position: 17 +sidebar_position: 18 id: troubleshooting license: | Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/docs/guide/python/grpc-support.md b/docs/guide/python/grpc-support.md new file mode 100644 index 0000000000..c5106f91b0 --- /dev/null +++ b/docs/guide/python/grpc-support.md @@ -0,0 +1,210 @@ +--- +title: gRPC Support +sidebar_position: 13 +id: grpc_support +license: | + 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. +--- + +Fory can generate Python gRPC service companions for schemas that define +services. The generated modules use `grpcio` for transport and use Fory to +serialize request and response objects. + +Use this mode when every RPC peer is generated from the same Fory IDL, protobuf +IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload +encoding. Use standard protobuf gRPC code generation when clients or tools must +consume protobuf message bytes directly. + +## Install Dependencies + +Install `grpcio` alongside `pyfory`. The generated companion imports `grpc`, but +`pyfory` does not add gRPC as a hard dependency. + +```bash +pip install pyfory grpcio +``` + +## Define a Service + +Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers +`rpc_service` definitions. A Fory IDL service looks like this: + +```protobuf +package demo.greeter; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string reply = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} +``` + +Generate Python model and gRPC companion code with `--grpc`: + +```bash +foryc service.fdl --python_out=./generated/python --grpc +``` + +For this schema, the Python generator emits: + +| File | Purpose | +| ---------------------- | ------------------------------------------- | +| `demo_greeter.py` | Fory dataclasses and registration helpers | +| `demo_greeter_grpc.py` | `grpcio` stub, servicer base, and registrar | + +The module name is derived from the Fory package by replacing dots with +underscores. A schema with no package uses `generated.py` and +`generated_grpc.py`. + +## Implement a Server + +Subclass the generated servicer and register it with a normal `grpcio` server. +Generated Python method names use snake_case, while the gRPC wire path keeps the +original IDL method name. + +```python +from concurrent import futures + +import grpc + +import demo_greeter +import demo_greeter_grpc + + +class Greeter(demo_greeter_grpc.GreeterServicer): + def say_hello(self, request, context): + return demo_greeter.HelloReply(reply=f"Hello, {request.name}") + + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=8)) + demo_greeter_grpc.add_servicer(Greeter(), server) + server.add_insecure_port("[::]:50051") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() +``` + +Generated request and response types are serialized by the generated companion, +so service implementations do not perform manual Fory registration. + +## Create a Client + +Use the generated stub with a normal `grpcio` channel: + +```python +import grpc + +import demo_greeter +import demo_greeter_grpc + + +def main(): + with grpc.insecure_channel("localhost:50051") as channel: + stub = demo_greeter_grpc.GreeterStub(channel) + reply = stub.say_hello(demo_greeter.HelloRequest(name="Fory")) + print(reply.reply) + + +if __name__ == "__main__": + main() +``` + +`grpcio` still owns channel options, credentials, deadlines, metadata, retries, +and interceptors. + +## Streaming RPCs + +Fory service definitions can use unary, server-streaming, client-streaming, and +bidirectional streaming RPC shapes: + +```protobuf +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc LotsOfReplies (HelloRequest) returns (stream HelloReply); + rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply); + rpc Chat (stream HelloRequest) returns (stream HelloReply); +} +``` + +Generated Python code follows `grpcio` conventions: + +- Unary stubs call `channel.unary_unary(...)`. +- Server-streaming stubs return an iterator over response objects. +- Client-streaming stubs accept an iterator of request objects. +- Bidirectional stubs accept a request iterator and return a response iterator. +- Servicer methods use snake_case names and return either a single response or + an iterator, depending on the streaming shape. + +Example server-streaming implementation: + +```python +class Greeter(demo_greeter_grpc.GreeterServicer): + def lots_of_replies(self, request, context): + for index in range(3): + yield demo_greeter.HelloReply( + reply=f"Hello {request.name}, response {index}" + ) +``` + +## Operations + +The generated service companion only supplies Fory serialization callbacks. +Operational behavior remains standard `grpcio` behavior: + +- Deadlines and cancellations +- TLS and authentication credentials +- Client and server interceptors +- Status codes, details, and metadata +- Channel and server lifecycle +- Thread pool sizing for synchronous servers + +## Troubleshooting + +### `ModuleNotFoundError: No module named 'grpc'` + +Install `grpcio` in the environment that runs the generated service module: + +```bash +pip install grpcio +``` + +### `TypeError: Unsupported gRPC servicer type` + +Pass an instance of the generated servicer subclass to +`demo_greeter_grpc.add_servicer(...)`. If the schema contains multiple services, +the generated registrar accepts only the matching generated servicer types. + +### `UNIMPLEMENTED` + +Confirm that the generated servicer was registered with the server, and that the +client and server were generated from the same package, service, and method +names. + +### Protobuf Clients Cannot Decode the Service + +Fory gRPC companions do not use protobuf wire encoding for messages. Use a +Fory-generated client for Fory-generated services, or provide a separate +protobuf service endpoint for generic protobuf clients. diff --git a/docs/guide/python/index.md b/docs/guide/python/index.md index 36c0cb9ba2..5b74d7de65 100644 --- a/docs/guide/python/index.md +++ b/docs/guide/python/index.md @@ -159,6 +159,7 @@ See [Native Serialization](native-serialization.md) for Python-only serializatio - [Type Registration](type-registration.md) - User-defined type registration - [Custom Serializers](custom-serializers.md) - Extend serialization behavior - [Row Format](row-format.md) - Zero-copy row format +- [gRPC Support](grpc-support.md) - Fory payloads over grpcio ## Links diff --git a/docs/guide/python/troubleshooting.md b/docs/guide/python/troubleshooting.md index bd4124d490..8dc56c160c 100644 --- a/docs/guide/python/troubleshooting.md +++ b/docs/guide/python/troubleshooting.md @@ -1,6 +1,6 @@ --- title: Troubleshooting -sidebar_position: 13 +sidebar_position: 14 id: troubleshooting license: | Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/docs/guide/rust/grpc-support.md b/docs/guide/rust/grpc-support.md new file mode 100644 index 0000000000..3cb6f30117 --- /dev/null +++ b/docs/guide/rust/grpc-support.md @@ -0,0 +1,229 @@ +--- +title: gRPC Support +sidebar_position: 12 +id: grpc_support +license: | + 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. +--- + +Fory can generate Rust gRPC service companions for schemas that define +services. The generated code uses `tonic` for transport and Fory for request and +response payload serialization. + +Use this mode when every RPC peer is generated from the same Fory IDL, protobuf +IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload +encoding. Use standard protobuf gRPC code generation when clients or tools must +consume protobuf message bytes directly. + +## Add Dependencies + +Add `tonic` and `bytes` to the crate that compiles the generated service files. +Fory Rust crates do not add gRPC as a hard dependency. Add `tokio` for async +servers and clients, and `tokio-stream` when your service implementation needs +to build streaming responses. + +```toml +[dependencies] +fory = "1.1.0" +bytes = "1" +tonic = { version = "0.14", features = ["transport"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1" +``` + +Use dependency versions that are compatible with the rest of your service +stack. + +## Define a Service + +Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers +`rpc_service` definitions. A Fory IDL service looks like this: + +```protobuf +package demo.greeter; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string reply = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} +``` + +Generate Rust model and gRPC companion code with `--grpc`: + +```bash +foryc service.fdl --rust_out=./generated/rust --grpc +``` + +For this schema, the Rust generator emits: + +| File | Purpose | +| ------------------------------ | -------------------------------------------- | +| `demo_greeter.rs` | Fory model types and registration helpers | +| `demo_greeter_service.rs` | Async service trait and gRPC path constants | +| `demo_greeter_service_grpc.rs` | tonic client, server wrapper, and Fory codec | + +Add the generated files to your crate root: + +```rust +pub mod demo_greeter; +pub mod demo_greeter_service; +pub mod demo_greeter_service_grpc; +``` + +## Implement a Server + +Implement the generated async trait and add the generated server wrapper to a +normal `tonic` server. + +```rust +use demo_greeter::{HelloReply, HelloRequest}; +use demo_greeter_service::Greeter; +use demo_greeter_service_grpc::greeter_server::GreeterServer; +use tonic::{Request, Response, Status}; + +#[derive(Default)] +struct MyGreeter; + +#[tonic::async_trait] +impl Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + Ok(Response::new(HelloReply { + reply: format!("Hello, {}", request.name), + })) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse()?; + tonic::transport::Server::builder() + .add_service(GreeterServer::new(MyGreeter::default())) + .serve(addr) + .await?; + Ok(()) +} +``` + +Generated request and response types are serialized by the generated service +code, so service implementations do not perform manual Fory registration. + +## Create a Client + +Use the generated tonic client: + +```rust +use demo_greeter::HelloRequest; +use demo_greeter_service_grpc::greeter_client::GreeterClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = GreeterClient::connect("http://[::1]:50051").await?; + let response = client + .say_hello(HelloRequest { + name: "Fory".to_string(), + }) + .await?; + println!("{}", response.into_inner().reply); + Ok(()) +} +``` + +`tonic` still owns channel configuration, TLS, deadlines, metadata, +interceptors, and transport lifecycle. + +## Streaming RPCs + +Fory service definitions can use unary, server-streaming, client-streaming, and +bidirectional streaming RPC shapes: + +```protobuf +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + rpc LotsOfReplies (HelloRequest) returns (stream HelloReply); + rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply); + rpc Chat (stream HelloRequest) returns (stream HelloReply); +} +``` + +Generated Rust code follows tonic conventions: + +- Unary methods use `tonic::Request` and return `tonic::Response`. +- Server-streaming methods return a response whose inner value is a stream of + `Result`. +- Client-streaming and bidirectional methods receive `tonic::Streaming`. +- The generated client module exposes matching async methods for each service + method. +- The generated codec is used for every message frame, including streaming + frames. + +Use the generated trait signatures as the source of truth for the concrete +associated stream types in your service implementation. + +## Thread Safety and Payload Types + +Generated Rust gRPC payloads must be `Send + 'static` so tonic can move request +and response values across async tasks. If a schema uses non-thread-safe +reference metadata for a request or response type, Rust gRPC generation rejects +that service. Use thread-safe reference shapes for gRPC payloads, or keep the +non-thread-safe type out of the RPC boundary. + +## Operations + +The generated service companion only supplies Fory serialization and tonic +bindings. Operational behavior remains standard tonic behavior: + +- Deadlines and cancellations +- TLS and authentication +- Tower middleware and interceptors +- Status codes and metadata +- Channel and server lifecycle +- Backpressure through async streams + +## Troubleshooting + +### Missing `tonic` or `bytes` Crates + +Add the dependencies shown above to the crate that compiles the generated +service files. + +### `UNIMPLEMENTED` + +Confirm that the generated server wrapper was added with +`Server::builder().add_service(...)`, and that the client and server were +generated from the same package, service, and method names. + +### Non-Thread-Safe Reference Errors During Code Generation + +Rust gRPC payloads must be `Send + 'static`. Change the request or response +schema to use thread-safe reference shapes, or wrap the non-thread-safe data in a +type that is not part of the gRPC payload. + +### Protobuf Clients Cannot Decode the Service + +Fory gRPC companions do not use protobuf wire encoding for messages. Use a +Fory-generated client for Fory-generated services, or provide a separate +protobuf service endpoint for generic protobuf clients. diff --git a/docs/guide/rust/index.md b/docs/guide/rust/index.md index 93c3537ed2..c2212a1f7b 100644 --- a/docs/guide/rust/index.md +++ b/docs/guide/rust/index.md @@ -193,3 +193,4 @@ fory-derive/ # Procedural macros - [Polymorphism](polymorphism.md) - Trait object serialization - [Custom Serializers](custom-serializers.md) - Extend serialization behavior - [Row Format](row-format.md) - Zero-copy row-based format +- [gRPC Support](grpc-support.md) - Fory payloads over tonic diff --git a/docs/guide/rust/troubleshooting.md b/docs/guide/rust/troubleshooting.md index 6b46d1b6d3..285c99a8da 100644 --- a/docs/guide/rust/troubleshooting.md +++ b/docs/guide/rust/troubleshooting.md @@ -1,6 +1,6 @@ --- title: Troubleshooting -sidebar_position: 12 +sidebar_position: 13 id: troubleshooting license: | Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/integration_tests/grpc_tests/go/main.go b/integration_tests/grpc_tests/go/main.go index caa9e3909b..0d414d2b5e 100644 --- a/integration_tests/grpc_tests/go/main.go +++ b/integration_tests/grpc_tests/go/main.go @@ -16,7 +16,7 @@ // under the License. // Binary grpc-interop is the Go peer for Java-driven gRPC integration tests. -// It is invoked as a subprocess by GrpcInteropTest.java and supports two modes: +// It is invoked as a subprocess by Java gRPC integration tests and supports two modes: // // server --port-file start a gRPC server and write the bound port to the file // client --target connect to addr and exercise all four streaming modes @@ -32,23 +32,13 @@ import ( "os" "strings" - "github.com/apache/fory/go/fory" grpc_fdl "github.com/apache/fory/integration_tests/grpc_tests/go/generated/grpc_fdl" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/encoding" ) // --- helpers ---------------------------------------------------------------- -func newFory() *fory.Fory { - f := fory.New(fory.WithXlang(true), fory.WithRefTracking(true), fory.WithCompatible(true)) - if err := grpc_fdl.RegisterTypes(f); err != nil { - log.Fatalf("RegisterTypes: %v", err) - } - return f -} - func fdlResponse(req *grpc_fdl.GrpcFdlRequest, tag string, offset int) *grpc_fdl.GrpcFdlResponse { return &grpc_fdl.GrpcFdlResponse{ Id: fmt.Sprintf("%s:%s", tag, req.Id), @@ -73,25 +63,59 @@ func fdlAggregate(requests []*grpc_fdl.GrpcFdlRequest) *grpc_fdl.GrpcFdlResponse } } -// protoFallbackCodec overrides the built-in "proto" codec (v1 registry) with -// Fory so the server can decode requests from Java clients, which send with the -// default content-type (application/grpc) rather than application/grpc+fory. -type protoFallbackCodec struct{ grpc_fdl.CodecV2 } +func fdlRequestUnion(req *grpc_fdl.GrpcFdlRequest) *grpc_fdl.GrpcFdlUnion { + union := grpc_fdl.RequestGrpcFdlUnion(req) + return &union +} -func (protoFallbackCodec) Name() string { return "proto" } +func fdlResponseUnion(response *grpc_fdl.GrpcFdlResponse) *grpc_fdl.GrpcFdlUnion { + union := grpc_fdl.ResponseGrpcFdlUnion(response) + return &union +} -func (c protoFallbackCodec) Marshal(v interface{}) ([]byte, error) { - b, err := c.Fory.Marshal(v) - if err != nil { - return nil, err +func fdlUnionResponse(req *grpc_fdl.GrpcFdlRequest, tag string, offset int) *grpc_fdl.GrpcFdlUnion { + return fdlResponseUnion(fdlResponse(req, tag, offset)) +} + +func fdlUnionAggregate(unions []*grpc_fdl.GrpcFdlUnion) (*grpc_fdl.GrpcFdlUnion, error) { + requests := make([]*grpc_fdl.GrpcFdlRequest, len(unions)) + for i, union := range unions { + req, err := fdlRequestFromUnion(union) + if err != nil { + return nil, err + } + requests[i] = req + } + return fdlResponseUnion(fdlAggregate(requests)), nil +} + +func fdlRequestFromUnion(union *grpc_fdl.GrpcFdlUnion) (*grpc_fdl.GrpcFdlRequest, error) { + if union == nil { + return nil, fmt.Errorf("expected request union, got nil") } - out := make([]byte, len(b)) - copy(out, b) - return out, nil + req, ok := union.AsRequest() + if !ok { + return nil, fmt.Errorf("expected request union, got case %d", union.Case()) + } + return req, nil } -func (c protoFallbackCodec) Unmarshal(data []byte, v interface{}) error { - return c.Fory.Unmarshal(data, v) +func expectUnionResponse(name string, got *grpc_fdl.GrpcFdlUnion, want *grpc_fdl.GrpcFdlUnion) error { + if got == nil || want == nil { + return fmt.Errorf("%s: got %+v, want %+v", name, got, want) + } + gotResponse, ok := got.AsResponse() + if !ok { + return fmt.Errorf("%s: got non-response union case %d", name, got.Case()) + } + wantResponse, ok := want.AsResponse() + if !ok { + return fmt.Errorf("%s: want non-response union case %d", name, want.Case()) + } + if *gotResponse != *wantResponse { + return fmt.Errorf("%s: got %+v, want %+v", name, gotResponse, wantResponse) + } + return nil } // --- server ----------------------------------------------------------------- @@ -144,19 +168,72 @@ func (s *fdlService) BidiStreamMessage(stream grpc_fdl.FdlGrpcService_BidiStream } } -func runServer(portFile string, f *fory.Fory) error { +func (s *fdlService) UnaryUnion(_ context.Context, req *grpc_fdl.GrpcFdlUnion) (*grpc_fdl.GrpcFdlUnion, error) { + message, err := fdlRequestFromUnion(req) + if err != nil { + return nil, err + } + return fdlUnionResponse(message, "unary", 10), nil +} + +func (s *fdlService) ServerStreamUnion(req *grpc_fdl.GrpcFdlUnion, stream grpc_fdl.FdlGrpcService_ServerStreamUnionServer) error { + message, err := fdlRequestFromUnion(req) + if err != nil { + return err + } + for i := 0; i < 3; i++ { + if err := stream.Send(fdlUnionResponse(message, fmt.Sprintf("server-%d", i), i)); err != nil { + return err + } + } + return nil +} + +func (s *fdlService) ClientStreamUnion(stream grpc_fdl.FdlGrpcService_ClientStreamUnionServer) error { + var requests []*grpc_fdl.GrpcFdlUnion + for { + req, err := stream.Recv() + if err == io.EOF { + response, err := fdlUnionAggregate(requests) + if err != nil { + return err + } + return stream.SendAndClose(response) + } + if err != nil { + return err + } + requests = append(requests, req) + } +} + +func (s *fdlService) BidiStreamUnion(stream grpc_fdl.FdlGrpcService_BidiStreamUnionServer) error { + index := 0 + for { + req, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + message, err := fdlRequestFromUnion(req) + if err != nil { + return err + } + if err := stream.Send(fdlUnionResponse(message, fmt.Sprintf("bidi-%d", index), index)); err != nil { + return err + } + index++ + } +} + +func runServer(portFile string) error { lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return fmt.Errorf("listen: %w", err) } - codec := grpc_fdl.CodecV2{Fory: f} - // "fory" (v2): used by Go clients via grpc.ForceCodecV2. - encoding.RegisterCodecV2(codec) - // "proto" (v1): overrides the built-in proto codec so Java clients are - // also handled by Fory. RegisterCodecV2 writes to a separate registry and - // does not replace the v1 "proto" entry; RegisterCodec must be used. - encoding.RegisterCodec(protoFallbackCodec{codec}) - s := grpc.NewServer() + s := grpc.NewServer(grpc.ForceServerCodecV2(grpc_fdl.CodecV2{})) grpc_fdl.RegisterFdlGrpcServiceServer(s, &fdlService{}) // Write the bound port so the Java test harness knows where to connect. @@ -246,19 +323,123 @@ func exerciseMessageStub(stub grpc_fdl.FdlGrpcServiceClient, requests []*grpc_fd return nil } -func runClient(target string, f *fory.Fory) error { +func exerciseUnionStub(stub grpc_fdl.FdlGrpcServiceClient, requests []*grpc_fdl.GrpcFdlUnion) error { + ctx := context.Background() + first := requests[0] + firstMessage, err := fdlRequestFromUnion(first) + if err != nil { + return err + } + + // unary + got, err := stub.UnaryUnion(ctx, first) + if err != nil { + return fmt.Errorf("UnaryUnion: %w", err) + } + if err := expectUnionResponse("UnaryUnion", got, fdlUnionResponse(firstMessage, "unary", 10)); err != nil { + return err + } + + // server streaming + ss, err := stub.ServerStreamUnion(ctx, first) + if err != nil { + return fmt.Errorf("ServerStreamUnion: %w", err) + } + for i := 0; i < 3; i++ { + got, err := ss.Recv() + if err != nil { + return fmt.Errorf("ServerStreamUnion Recv[%d]: %w", i, err) + } + if err := expectUnionResponse( + fmt.Sprintf("ServerStreamUnion[%d]", i), + got, + fdlUnionResponse(firstMessage, fmt.Sprintf("server-%d", i), i), + ); err != nil { + return err + } + } + if _, err := ss.Recv(); err != io.EOF { + return fmt.Errorf("ServerStreamUnion: expected EOF, got %v", err) + } + + // client streaming + cs, err := stub.ClientStreamUnion(ctx) + if err != nil { + return fmt.Errorf("ClientStreamUnion: %w", err) + } + for _, req := range requests { + if err := cs.Send(req); err != nil { + return fmt.Errorf("ClientStreamUnion Send: %w", err) + } + } + csResp, err := cs.CloseAndRecv() + if err != nil { + return fmt.Errorf("ClientStreamUnion CloseAndRecv: %w", err) + } + wantAggregate, err := fdlUnionAggregate(requests) + if err != nil { + return err + } + if err := expectUnionResponse("ClientStreamUnion", csResp, wantAggregate); err != nil { + return err + } + + // bidirectional streaming + bidi, err := stub.BidiStreamUnion(ctx) + if err != nil { + return fmt.Errorf("BidiStreamUnion: %w", err) + } + for i, req := range requests { + message, err := fdlRequestFromUnion(req) + if err != nil { + return err + } + if err := bidi.Send(req); err != nil { + return fmt.Errorf("BidiStreamUnion Send[%d]: %w", i, err) + } + got, err := bidi.Recv() + if err != nil { + return fmt.Errorf("BidiStreamUnion Recv[%d]: %w", i, err) + } + if err := expectUnionResponse( + fmt.Sprintf("BidiStreamUnion[%d]", i), + got, + fdlUnionResponse(message, fmt.Sprintf("bidi-%d", i), i), + ); err != nil { + return err + } + } + if err := bidi.CloseSend(); err != nil { + return fmt.Errorf("BidiStreamUnion CloseSend: %w", err) + } + if _, err := bidi.Recv(); err != io.EOF { + return fmt.Errorf("BidiStreamUnion: expected EOF after CloseSend, got %v", err) + } + + return nil +} + +func runClient(target string) error { conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("dial %s: %w", target, err) } defer conn.Close() - stub := grpc_fdl.NewFdlGrpcServiceClient(conn, f) + stub := grpc_fdl.NewFdlGrpcServiceClient(conn) requests := []*grpc_fdl.GrpcFdlRequest{ {Id: "fdl-a", Count: 1, Payload: "alpha"}, {Id: "fdl-b", Count: 2, Payload: "beta"}, } - return exerciseMessageStub(stub, requests) + if err := exerciseMessageStub(stub, requests); err != nil { + return err + } + + unionRequests := []*grpc_fdl.GrpcFdlUnion{ + fdlRequestUnion(&grpc_fdl.GrpcFdlRequest{Id: "fdl-u-a", Count: 3, Payload: "union-alpha"}), + fdlRequestUnion(&grpc_fdl.GrpcFdlRequest{Id: "fdl-u-b", Count: 4, Payload: "union-beta"}), + } + return exerciseUnionStub(stub, unionRequests) } // --- entry point ------------------------------------------------------------ @@ -274,15 +455,13 @@ func main() { log.Fatal("usage: grpc-interop [flags]") } - f := newFory() - switch os.Args[1] { case "server": serverCmd.Parse(os.Args[2:]) if *serverPortFile == "" { log.Fatal("--port-file is required") } - if err := runServer(*serverPortFile, f); err != nil { + if err := runServer(*serverPortFile); err != nil { log.Fatalf("server error: %v", err) } case "client": @@ -290,7 +469,7 @@ func main() { if *clientTarget == "" { log.Fatal("--target is required") } - if err := runClient(*clientTarget, f); err != nil { + if err := runClient(*clientTarget); err != nil { log.Fatalf("client error: %v", err) } default: diff --git a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java new file mode 100644 index 0000000000..44d9b9c035 --- /dev/null +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcInteropTest.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.apache.fory.grpc_tests; + +import io.grpc.Server; +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +public class GoGrpcInteropTest extends GrpcTestBase { + + @Test + public void testJavaServerGoClient() throws Exception { + Server server = startJavaFdlServer(); + try { + runGo("go-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); + } finally { + server.shutdownNow(); + server.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + public void testGoServerJavaClient() throws Exception { + exercisePeerServer("go-grpc", "Go", "fory-grpc-go-", goCommand("server"), this::exerciseFdl); + } +} diff --git a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java similarity index 85% rename from integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java rename to integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java index bbf6a8629e..730bfbf95b 100644 --- a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcInteropTest.java +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java @@ -40,94 +40,52 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.testng.Assert; -import org.testng.annotations.Test; - -public class GrpcInteropTest { - - @Test - public void testJavaServerPythonClient() throws Exception { - Server server = - ServerBuilder.forPort(0) - .addService(new FdlService()) - .addService(new FbsService()) - .addService(new PbService()) - .build() - .start(); - try { - runPython("python-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); - } finally { - server.shutdownNow(); - server.awaitTermination(10, TimeUnit.SECONDS); - } + +public abstract class GrpcTestBase { + + @FunctionalInterface + protected interface ChannelExercise { + void run(ManagedChannel channel) throws Exception; } - @Test - public void testJavaClientPythonServer() throws Exception { - Path portFile = Files.createTempFile("fory-grpc-python-", ".port"); - Files.deleteIfExists(portFile); - PeerCommand command = pythonCommand("server", "--port-file", portFile.toString()); - Process process = startPeer(command); - PeerOutputCollector outputCollector = - new PeerOutputCollector(process.getInputStream(), "python-grpc-server"); - outputCollector.start(); - try { - int port = waitForPort(process, outputCollector, portFile, "Python"); - ManagedChannel channel = - ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); - try { - exerciseFdl(channel); - exerciseFbs(channel); - exercisePb(channel); - } finally { - channel.shutdownNow(); - channel.awaitTermination(10, TimeUnit.SECONDS); - } - } finally { - process.destroy(); - process.waitFor(10, TimeUnit.SECONDS); - if (process.isAlive()) { - process.destroyForcibly(); - process.waitFor(10, TimeUnit.SECONDS); - } - outputCollector.awaitOutput(); - Files.deleteIfExists(portFile); - } + protected Server startJavaAllSchemasServer() throws IOException { + return ServerBuilder.forPort(0) + .addService(new FdlService()) + .addService(new FbsService()) + .addService(new PbService()) + .build() + .start(); } - @Test - public void testJavaServerRustClient() throws Exception { - Server server = - ServerBuilder.forPort(0) - .addService(new FdlService()) - .addService(new FbsService()) - .addService(new PbService()) - .build() - .start(); - try { - runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); - } finally { - server.shutdownNow(); - server.awaitTermination(10, TimeUnit.SECONDS); - } + protected Server startJavaFdlServer() throws IOException { + return ServerBuilder.forPort(0).addService(new FdlService()).build().start(); } - @Test - public void testJavaClientRustServer() throws Exception { - Path portFile = Files.createTempFile("fory-grpc-rust-", ".port"); + protected void exerciseAllSchemas(ManagedChannel channel) throws InterruptedException { + exerciseFdl(channel); + exerciseFbs(channel); + exercisePb(channel); + } + + protected void exercisePeerServer( + String peer, + String language, + String portPrefix, + PeerCommand command, + ChannelExercise exercise) + throws Exception { + Path portFile = Files.createTempFile(portPrefix, ".port"); Files.deleteIfExists(portFile); - PeerCommand command = rustCommand("server", "--port-file", portFile.toString()); - Process process = startPeer(command); + Process process = startPeer(command.withPortFile(portFile)); PeerOutputCollector outputCollector = - new PeerOutputCollector(process.getInputStream(), "rust-grpc-server"); + new PeerOutputCollector(process.getInputStream(), peer + "-server"); outputCollector.start(); try { - int port = waitForPort(process, outputCollector, portFile, "Rust"); + int port = waitForPort(process, outputCollector, portFile, language); ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); try { - exerciseFdl(channel); - exerciseFbs(channel); - exercisePb(channel); + exercise.run(channel); } finally { channel.shutdownNow(); channel.awaitTermination(10, TimeUnit.SECONDS); @@ -144,7 +102,7 @@ public void testJavaClientRustServer() throws Exception { } } - private void exerciseFdl(ManagedChannel channel) throws InterruptedException { + protected void exerciseFdl(ManagedChannel channel) throws InterruptedException { grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking = grpc_fdl.FdlGrpcServiceGrpc.newBlockingStub(channel); grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async = @@ -161,18 +119,6 @@ private void exerciseFdl(ManagedChannel channel) throws InterruptedException { assertFdlUnions(blocking, async, unions); } - // exerciseFdlMessages exercises the four FDL message streaming modes without union methods. - private void exerciseFdlMessages(ManagedChannel channel) throws InterruptedException { - grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking = - grpc_fdl.FdlGrpcServiceGrpc.newBlockingStub(channel); - grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async = - grpc_fdl.FdlGrpcServiceGrpc.newStub(channel); - - List messages = - Arrays.asList(fdlRequest("fdl-a", 1, "alpha"), fdlRequest("fdl-b", 2, "beta")); - assertFdlMessages(blocking, async, messages); - } - private void assertFdlMessages( grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceBlockingStub blocking, grpc_fdl.FdlGrpcServiceGrpc.FdlGrpcServiceStub async, @@ -227,7 +173,7 @@ private void assertFdlUnions( fdlUnionResponse(fdlRequestFromUnion(requests.get(1)), "bidi-1", 1))); } - private void exerciseFbs(ManagedChannel channel) throws InterruptedException { + protected void exerciseFbs(ManagedChannel channel) throws InterruptedException { grpc_fbs.FbsGrpcServiceGrpc.FbsGrpcServiceBlockingStub blocking = grpc_fbs.FbsGrpcServiceGrpc.newBlockingStub(channel); grpc_fbs.FbsGrpcServiceGrpc.FbsGrpcServiceStub async = @@ -298,7 +244,7 @@ private void assertFbsUnions( fbsUnionResponse(fbsRequestFromUnion(requests.get(1)), "bidi-1", 1))); } - private void exercisePb(ManagedChannel channel) throws InterruptedException { + protected void exercisePb(ManagedChannel channel) throws InterruptedException { grpc_pb.PbGrpcServiceGrpc.PbGrpcServiceBlockingStub blocking = grpc_pb.PbGrpcServiceGrpc.newBlockingStub(channel); grpc_pb.PbGrpcServiceGrpc.PbGrpcServiceStub async = grpc_pb.PbGrpcServiceGrpc.newStub(channel); @@ -328,49 +274,7 @@ private void exercisePb(ManagedChannel channel) throws InterruptedException { pbResponse(requests.get(0), "bidi-0", 0), pbResponse(requests.get(1), "bidi-1", 1))); } - @Test - public void testJavaServerGoClient() throws Exception { - Server server = ServerBuilder.forPort(0).addService(new FdlService()).build().start(); - try { - runGo("go-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); - } finally { - server.shutdownNow(); - server.awaitTermination(10, TimeUnit.SECONDS); - } - } - - @Test - public void testGoServerJavaClient() throws Exception { - Path portFile = Files.createTempFile("fory-grpc-go-", ".port"); - Files.deleteIfExists(portFile); - PeerCommand command = goCommand("server", "--port-file", portFile.toString()); - Process process = startPeer(command); - PeerOutputCollector outputCollector = - new PeerOutputCollector(process.getInputStream(), "go-grpc-server"); - outputCollector.start(); - try { - int port = waitForPort(process, outputCollector, portFile, "Go"); - ManagedChannel channel = - ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); - try { - exerciseFdlMessages(channel); - } finally { - channel.shutdownNow(); - channel.awaitTermination(10, TimeUnit.SECONDS); - } - } finally { - process.destroy(); - process.waitFor(10, TimeUnit.SECONDS); - if (process.isAlive()) { - process.destroyForcibly(); - process.waitFor(10, TimeUnit.SECONDS); - } - outputCollector.awaitOutput(); - Files.deleteIfExists(portFile); - } - } - - private PeerCommand goCommand(String... args) { + protected PeerCommand goCommand(String... args) { Path grpcRoot = repoRoot().resolve("integration_tests").resolve("grpc_tests"); Path goRoot = grpcRoot.resolve("go"); List command = new ArrayList<>(); @@ -384,7 +288,7 @@ private PeerCommand goCommand(String... args) { return peerCommand; } - private void runGo(String peer, String... args) throws IOException, InterruptedException { + protected void runGo(String peer, String... args) throws IOException, InterruptedException { Process process = startPeer(goCommand(args)); PeerOutputCollector outputCollector = new PeerOutputCollector(process.getInputStream(), peer); outputCollector.start(); @@ -406,7 +310,7 @@ private void runGo(String peer, String... args) throws IOException, InterruptedE outputCollector.awaitOutput(); } - private PeerCommand pythonCommand(String... args) { + protected PeerCommand pythonCommand(String... args) { Path repoRoot = repoRoot(); Path grpcRoot = repoRoot.resolve("integration_tests").resolve("grpc_tests"); Path pythonRoot = grpcRoot.resolve("python"); @@ -443,7 +347,7 @@ private PeerCommand pythonCommand(String... args) { return peerCommand; } - private PeerCommand rustCommand(String... args) { + protected PeerCommand rustCommand(String... args) { Path repoRoot = repoRoot(); Path grpcRoot = repoRoot.resolve("integration_tests").resolve("grpc_tests"); Path rustRoot = grpcRoot.resolve("rust"); @@ -471,7 +375,7 @@ private PeerCommand rustCommand(String... args) { return peerCommand; } - private void runPython(String peer, String... args) throws IOException, InterruptedException { + protected void runPython(String peer, String... args) throws IOException, InterruptedException { Process process = startPeer(pythonCommand(args)); PeerOutputCollector outputCollector = new PeerOutputCollector(process.getInputStream(), peer); outputCollector.start(); @@ -493,7 +397,7 @@ private void runPython(String peer, String... args) throws IOException, Interrup outputCollector.awaitOutput(); } - private void runRust(String peer, String... args) throws IOException, InterruptedException { + protected void runRust(String peer, String... args) throws IOException, InterruptedException { Process process = startPeer(rustCommand(args)); PeerOutputCollector outputCollector = new PeerOutputCollector(process.getInputStream(), peer); outputCollector.start(); @@ -589,7 +493,7 @@ private static grpc_fdl.GrpcFdlUnion fdlUnionResponse( private static grpc_fdl.GrpcFdlUnion fdlUnionAggregate(List unions) { return grpc_fdl.GrpcFdlUnion.ofResponse( - fdlAggregate(map(unions, GrpcInteropTest::fdlRequestFromUnion))); + fdlAggregate(map(unions, GrpcTestBase::fdlRequestFromUnion))); } private static grpc_fdl.GrpcFdlRequest fdlRequestFromUnion(grpc_fdl.GrpcFdlUnion union) { @@ -629,7 +533,7 @@ private static grpc_fbs.GrpcFbsUnion fbsUnionResponse( private static grpc_fbs.GrpcFbsUnion fbsUnionAggregate(List unions) { return grpc_fbs.GrpcFbsUnion.ofGrpcFbsResponse( - fbsAggregate(map(unions, GrpcInteropTest::fbsRequestFromUnion))); + fbsAggregate(map(unions, GrpcTestBase::fbsRequestFromUnion))); } private static grpc_fbs.GrpcFbsRequest fbsRequestFromUnion(grpc_fbs.GrpcFbsUnion union) { @@ -795,7 +699,7 @@ public void serverStreamMessage( @Override public StreamObserver clientStreamMessage( StreamObserver responseObserver) { - return collectAndRespond(responseObserver, GrpcInteropTest::fdlAggregate); + return collectAndRespond(responseObserver, GrpcTestBase::fdlAggregate); } @Override @@ -843,7 +747,7 @@ public void serverStreamUnion( @Override public StreamObserver clientStreamUnion( StreamObserver responseObserver) { - return collectAndRespond(responseObserver, GrpcInteropTest::fdlUnionAggregate); + return collectAndRespond(responseObserver, GrpcTestBase::fdlUnionAggregate); } @Override @@ -895,7 +799,7 @@ public void serverStreamMessage( @Override public StreamObserver clientStreamMessage( StreamObserver responseObserver) { - return collectAndRespond(responseObserver, GrpcInteropTest::fbsAggregate); + return collectAndRespond(responseObserver, GrpcTestBase::fbsAggregate); } @Override @@ -943,7 +847,7 @@ public void serverStreamUnion( @Override public StreamObserver clientStreamUnion( StreamObserver responseObserver) { - return collectAndRespond(responseObserver, GrpcInteropTest::fbsUnionAggregate); + return collectAndRespond(responseObserver, GrpcTestBase::fbsUnionAggregate); } @Override @@ -993,7 +897,7 @@ public void serverStreamMessage( @Override public StreamObserver clientStreamMessage( StreamObserver responseObserver) { - return collectAndRespond(responseObserver, GrpcInteropTest::pbAggregate); + return collectAndRespond(responseObserver, GrpcTestBase::pbAggregate); } @Override @@ -1053,10 +957,21 @@ private List await() throws InterruptedException { } } - private static final class PeerCommand { + protected static final class PeerCommand { private List command; private Path workDir; private final java.util.Map environment = new java.util.HashMap<>(); + + private PeerCommand withPortFile(Path portFile) { + List serverCommand = new ArrayList<>(command); + serverCommand.add("--port-file"); + serverCommand.add(portFile.toString()); + PeerCommand peerCommand = new PeerCommand(); + peerCommand.command = serverCommand; + peerCommand.workDir = workDir; + peerCommand.environment.putAll(environment); + return peerCommand; + } } private static final class PeerOutputCollector extends Thread { diff --git a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java new file mode 100644 index 0000000000..676defdaa2 --- /dev/null +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonGrpcInteropTest.java @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package org.apache.fory.grpc_tests; + +import io.grpc.Server; +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +public class PythonGrpcInteropTest extends GrpcTestBase { + + @Test + public void testJavaServerPythonClient() throws Exception { + Server server = startJavaAllSchemasServer(); + try { + runPython("python-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); + } finally { + server.shutdownNow(); + server.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + public void testJavaClientPythonServer() throws Exception { + exercisePeerServer( + "python-grpc", + "Python", + "fory-grpc-python-", + pythonCommand("server"), + this::exerciseAllSchemas); + } +} diff --git a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java new file mode 100644 index 0000000000..6dae4a745d --- /dev/null +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcInteropTest.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package org.apache.fory.grpc_tests; + +import io.grpc.Server; +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +public class RustGrpcInteropTest extends GrpcTestBase { + + @Test + public void testJavaServerRustClient() throws Exception { + Server server = startJavaAllSchemasServer(); + try { + runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); + } finally { + server.shutdownNow(); + server.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + public void testJavaClientRustServer() throws Exception { + exercisePeerServer( + "rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"), this::exerciseAllSchemas); + } +} diff --git a/integration_tests/grpc_tests/run_tests.sh b/integration_tests/grpc_tests/run_tests.sh index 22b9cd3aad..8028b34777 100755 --- a/integration_tests/grpc_tests/run_tests.sh +++ b/integration_tests/grpc_tests/run_tests.sh @@ -21,6 +21,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +TEST_CLASSES="${1:-PythonGrpcInteropTest,RustGrpcInteropTest,GoGrpcInteropTest}" python -m pip install "grpcio>=1.62.2,<1.71" python -m pip install -v -e "${ROOT_DIR}/python" @@ -32,5 +33,5 @@ go build -o grpc-interop . cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace --quiet cd "${ROOT_DIR}/integration_tests/grpc_tests/java" mvn -T16 --no-transfer-progress \ - -Dtest=GrpcInteropTest \ + -Dtest="${TEST_CLASSES}" \ test