Skip to content

Feat/2498 grpc protocol#2499

Merged
robfrank merged 40 commits intomainfrom
feat/2498-grpc-protocol
Sep 11, 2025
Merged

Feat/2498 grpc protocol#2499
robfrank merged 40 commits intomainfrom
feat/2498-grpc-protocol

Conversation

@robfrank
Copy link
Collaborator

@robfrank robfrank commented Sep 11, 2025

This is just a copy of #2462

What does this PR do?

This PR introduces full gRPC support for ArcadeDB, providing a strongly-typed, high-performance API for remote access to the database.

Key highlights include:

  • Streaming query execution with multiple retrieval modes:

    • CURSOR – existing behavior, iterating and streaming results as they are retrieved.
    • MATERIALIZE_ALL – materializes all results before streaming in batches.
    • PAGED – reissues queries per batch using LIMIT/SKIP for efficient paging.
  • Insert operations:

    • BulkInsert – single request for large batch inserts.
    • InsertStream – client-streaming bulk insert with server-side batching and acknowledgements.
    • InsertBidirectional – full duplex insert mode with per-batch acknowledgements and commit/rollback control.
  • Record operations:

    • Create, update, and lookup records with schema-aware property handling (including embedded and list properties).
  • Transaction support:

    • Added TransactionContext flags for begin, commit, and rollback to coordinate transactional operations across requests.
    • Enhanced type conversion utilities for safe proto ↔︎ Java object mapping, including embedded documents and collections.
  • Client-side API:

    • Full client-side implementation in RemoteGrpcDatabase with a simple, extensible API.

Motivation

  • To provide a native, efficient, and type-safe remote API for ArcadeDB that goes beyond REST/HTTP performance and reliability limitations.
  • To support streaming use cases and bulk data ingestion for high-volume workloads.
  • To align ArcadeDB with modern distributed and reactive application architectures where gRPC is the standard for service-to-service communication.
  • To improve developer experience with generated client stubs and auto-completion.

Related issues

N/A


Additional Notes

I don't yet have a set of client-side tests. These will be provided shortly.


Checklist

  • mvn -DskipTests install
  • Create a Docker image with mvn clean package -Pdocker in the package module

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @robfrank, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances ArcadeDB's communication capabilities by integrating a gRPC protocol. This new feature provides a more performant and robust alternative to the existing HTTP API, particularly beneficial for high-throughput data operations and real-time streaming. The changes span both server and client components, ensuring a comprehensive and well-tested implementation, alongside improvements to server-side monitoring for better operational visibility.

Highlights

  • New gRPC Protocol Implementation: A complete gRPC layer has been added, including .proto definitions for various database operations (CRUD, queries, commands, transactions, admin tasks).
  • Dedicated gRPC Client Library: A new arcadedb-grpc-client module provides a Java client that uses the gRPC protocol, offering methods for standard database interactions as well as new streaming and bulk ingest capabilities.
  • Server-Side gRPC Plugin: The arcadedb-grpcw module introduces GrpcServerPlugin to enable and configure the gRPC server within ArcadeDB, supporting standard and XDS modes, TLS, and message compression.
  • Enhanced Observability and Security: Server-side gRPC includes interceptors for logging, metrics (Micrometer), authentication, and compression, providing better insights and control over gRPC traffic.
  • Comprehensive Testing and Benchmarking: New end-to-end tests (RemoteGrpcDatabaseTest, RemoteGrpcDatabaseRegressionTest) and a benchmarking tool (ArcadeDbHTTPTvsGRPCBench) have been added to validate functionality and compare performance against the existing HTTP protocol.
  • Improved Server Monitoring: The ServerMonitor has been refactored to leverage JMX for more robust JVM monitoring, including heap and safepoint metrics.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@robfrank robfrank linked an issue Sep 11, 2025 that may be closed by this pull request
8 tasks
@robfrank robfrank self-assigned this Sep 11, 2025
@robfrank robfrank added enhancement New feature or request java labels Sep 11, 2025
@robfrank robfrank added this to the 25.9.1 milestone Sep 11, 2025
@gemini-code-assist
Copy link
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@codacy-production
Copy link

codacy-production bot commented Sep 11, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-1.08% 0.32%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (a074591) 70999 45646 64.29%
Head commit (65bb979) 72486 (+1487) 45819 (+173) 63.21% (-1.08%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#2499) 1551 5 0.32%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@robfrank
Copy link
Collaborator Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive gRPC support for ArcadeDB, including a new gRPC client, server plugin, and Protobuf definitions. The implementation covers streaming queries, bulk inserts, record operations, and transaction management, which is a significant and valuable addition. My review focuses on improving robustness, performance, and maintainability. Key areas for improvement include removing hardcoded debug statements, addressing a bug in JDK version compatibility, making debugging features opt-in, and replacing fragile reflection-based calls with type-safe interfaces. Additionally, there are some cleanup opportunities regarding temporary or duplicated files.

Comment on lines +2191 to +2193
private static String legacyTidName(Thread t) {
return t.threadId() + ":" + t.getName();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The legacyTidName method is intended as a fallback for JDKs older than 19, but it incorrectly calls t.threadId(), which was introduced in Java 19. This will cause a NoSuchMethodError on older supported JDKs. The correct fallback should use the deprecated t.getId() to maintain compatibility.

Suggested change
private static String legacyTidName(Thread t) {
return t.threadId() + ":" + t.getName();
}
private static String legacyTidName(Thread t) {
return t.getId() + ":" + t.getName();
}

Comment on lines +326 to +496
@SuppressWarnings("unchecked")
private Collection<String> getDatabaseNames() {
// Replace with your server API:
// e.g., ((ArcadeDBServer)server).getDatabaseNames()
try {
var m = server.getClass().getMethod("getDatabaseNames");
Object res = m.invoke(server);
if (res instanceof Collection<?> c) {
return (Collection<String>) c;
} else if (res instanceof String[] arr) {
return Arrays.asList(arr);
}
} catch (Throwable ignore) {
}
return Collections.emptyList();
}

private boolean containsDatabaseIgnoreCase(String name) {
for (String n : getDatabaseNames()) {
if (n.equalsIgnoreCase(name))
return true;
}
return false;
}

/**
* Create DB physically with READ_WRITE mode. Adjust to your server signature.
*/
private void createDatabasePhysical(String name) throws Exception {
// Typical signature: createDatabase(String, ComponentFile.MODE)
var m = server.getClass().getMethod("createDatabase", String.class, ComponentFile.MODE.class);
m.invoke(server, name, ComponentFile.MODE.READ_WRITE);
}

/**
* Drop DB physically. Prefer API with 'removeFiles' boolean if available.
*/
private void dropDatabasePhysical(String name) throws Exception {
try {
var m = server.getClass().getMethod("dropDatabase", String.class, boolean.class);
m.invoke(server, name, Boolean.TRUE);
} catch (NoSuchMethodException nsme) {
// Fallback: dropDatabase(String) if present
var m2 = server.getClass().getMethod("dropDatabase", String.class);
m2.invoke(server, name);
}
}

/**
* Open database for read ops. Adjust to your server's open/get method.
*/
private Database openDatabase(String name) throws Exception {
// Commonly: server.getDatabase(name) or server.openDatabase(name)
try {
var m = server.getClass().getMethod("getDatabase", String.class);
Object db = m.invoke(server, name);
return (Database) db;
} catch (NoSuchMethodException nsme) {
var m2 = server.getClass().getMethod("openDatabase", String.class);
Object db = m2.invoke(server, name);
return (Database) db;
}
}

/**
* Approximate record count with a quick pass across types.
*/
private long approximateRecordCount(Database db) {
long total = 0L;
try {
for (DocumentType t : db.getSchema().getTypes()) {
try {
// exact=false when supported; otherwise this counts exactly
total += db.countType(t.getName(), false);
} catch (Throwable ignore) {
}
}
} catch (Throwable ignore) {
}
return total;
}

private boolean existsVertexType(Schema s, String name) {

try {
return s.existsType(name);
} catch (Throwable t) {
return false;
}
}

private boolean existsEdgeType(Schema s, String name) {

try {

return s.existsType(name);
} catch (Throwable t) {
return false;
}
}

// ---------- safe server info fallbacks (optional; return sentinel values if
// not exposed) ----------

private String safeServerVersion() {
try {
var m = server.getClass().getMethod("getProductVersion");
Object v = m.invoke(server);
return (v != null) ? v.toString() : "unknown";
} catch (Throwable t) {
return "unknown";
}
}

private long safeServerStartMs() {
try {
var m = server.getClass().getMethod("getStartTime");
Object v = m.invoke(server);
if (v instanceof Number n)
return n.longValue();
} catch (Throwable t) {
// ignore
}
return 0L;
}

private int safeHttpPort() {
try {
var m = server.getClass().getMethod("getHttpServer");
Object http = m.invoke(server);
if (http != null) {
var pm = http.getClass().getMethod("getPort");
Object p = pm.invoke(http);
if (p instanceof Number n)
return n.intValue();
}
} catch (Throwable ignore) {
}
return -1;
}

private int safeGrpcPort() {
try {
var m = server.getClass().getMethod("getGrpcServer");
Object g = m.invoke(server);
if (g != null) {
var pm = g.getClass().getMethod("getPort");
Object p = pm.invoke(g);
if (p instanceof Number n)
return n.intValue();
}
} catch (Throwable ignore) {
}
return -1;
}

private int safeBinaryPort() {
try {
var m = server.getClass().getMethod("getBinaryServer");
Object b = m.invoke(server);
if (b != null) {
var pm = b.getClass().getMethod("getPort");
Object p = pm.invoke(b);
if (p instanceof Number n)
return n.intValue();
}
} catch (Throwable ignore) {
}
return -1;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This class uses reflection extensively to interact with ArcadeDBServer (e.g., in getDatabaseNames, createDatabasePhysical, safeServerVersion). This approach is brittle and can lead to runtime errors if the method names or signatures in ArcadeDBServer change. It would be much safer and more maintainable to define an interface with the required methods and have ArcadeDBServer implement it. The gRPC service could then call the methods through the interface, ensuring type safety and compile-time checks.

Comment on lines +1 to +60

PWD: /Users/ocohen/git/Verdance/ArcadeDB-GRPC/arcadedb-25.8.1-SNAPSHOT


./bin/server.sh
-Darcadedb.server.rootPassword=root1234
-Darcadedb.server.name=Arcade_GRPC_Test
-Darcadedb.dumpConfigAtStartup=true
-Darcadedb.server.mode=development
-Darcadedb.server.rootPath=../var/arcadedb
-Darcadedb.server.plugins=GRPC:com.arcadedb.server.grpc.GrpcServerPlugin
-Xms512M -Xmx4096M -XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0
-Darcadedb.server.httpIncomingPort=2489
-Darcadedb.grpc.enabled=true
-Darcadedb.grpc.port=50059
-Darcadedb.grpc.mode=standard
-Darcadedb.grpc.reflection.enabled=true
-Darcadedb.grpc.health.enabled=true


Ports:

HTTP: 2489
GTPC: 50059

root
root1234

Logging:

d exec -it arcadedb1-vulcan sh


vi /home/arcadedb/config/arcadedb-log.properties


handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler

.level = INFO
com.arcadedb.level = INFO
com.arcadedb.server.grpc.level = FINE

java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = com.arcadedb.utility.AnsiLogFormatter

java.util.logging.FileHandler.level = FINE
java.util.logging.FileHandler.pattern=./log/arcadedb.log
java.util.logging.FileHandler.formatter = com.arcadedb.log.LogFormatter
java.util.logging.FileHandler.limit=100000000
java.util.logging.FileHandler.count=10




tail -f /home/arcadedb/log/arcadedb.log.0




d exec -it arcadedb1-vulcan sh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to contain temporary developer notes, including a user-specific local path (/Users/ocohen/...). Such files should generally not be committed to the repository. Please consider removing this file or cleaning it up and moving it to a more appropriate location if it contains valuable documentation.

Comment on lines +2027 to +2060
private String mapRecordType(GrpcRecord grpcRecord) {

// Determine record category from type name
String typeName = grpcRecord.getType();

// Check schema to determine actual type
try {

if (typeName != null && !typeName.isBlank() && getSchema().existsType(typeName)) {

Object type = getSchema().getType(typeName);

if (type instanceof com.arcadedb.schema.VertexType) {

return "v";
} else if (type instanceof com.arcadedb.schema.EdgeType) {

return "e";
} else if (type instanceof com.arcadedb.schema.DocumentType) {

return "d";
} else {

return null;
}
} else {

return null;
}
} catch (Exception e) {

throw new RuntimeException(e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mapRecordType method is called as a fallback when a GrpcRecord doesn't contain the @cat property. This method performs a schema lookup for every record, which can be a performance bottleneck when processing large result sets. To optimize this, the server should always include the @cat property in the GrpcRecord payload, making this client-side lookup unnecessary.

}

// Optional knobs you can toggle from the TM for a single run
private volatile boolean txDebugEnabled = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The transaction debugging feature (txDebugEnabled) is enabled by default. This can add performance overhead and verbose logging in production environments. It's recommended to have debugging features disabled by default and enabled only when needed.

Suggested change
private volatile boolean txDebugEnabled = true;
private volatile boolean txDebugEnabled = false;

for (String propName : doc.getPropertyNames()) {
Object value = doc.get(propName);

System.out.print("toProtoRecord: " + propName + ": " + value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This System.out.print statement appears to be a leftover from debugging. It should be removed or replaced with a proper logging statement guarded by a log level check (e.g., logger.debug(...)).

Comment on lines +1 to +281
syntax = "proto3";

package com.arcadedb.grpc;

option java_multiple_files = true;
option java_package = "com.arcadedb.server.grpc";
option java_outer_classname = "ArcadeDbProto";

import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";

// Main ArcadeDB Service
service ArcadeDbService {
// Database operations
rpc CreateDatabase(CreateDatabaseRequest) returns (CreateDatabaseResponse);
rpc DropDatabase(DropDatabaseRequest) returns (DropDatabaseResponse);
rpc ListDatabases(ListDatabasesRequest) returns (ListDatabasesResponse);
rpc GetDatabaseInfo(GetDatabaseInfoRequest) returns (GetDatabaseInfoResponse);

// Query operations
rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse);
rpc ExecuteCommand(ExecuteCommandRequest) returns (ExecuteCommandResponse);

// Transaction operations
rpc BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse);
rpc CommitTransaction(CommitTransactionRequest) returns (CommitTransactionResponse);
rpc RollbackTransaction(RollbackTransactionRequest) returns (RollbackTransactionResponse);

// Record operations
rpc CreateRecord(CreateRecordRequest) returns (CreateRecordResponse);
rpc GetRecord(GetRecordRequest) returns (GetRecordResponse);
rpc UpdateRecord(UpdateRecordRequest) returns (UpdateRecordResponse);
rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse);

// Streaming operations
rpc StreamQuery(StreamQueryRequest) returns (stream QueryResult);
rpc BulkInsert(stream BulkInsertRequest) returns (BulkInsertResponse);

// Server operations
rpc GetServerStatus(google.protobuf.Empty) returns (ServerStatusResponse);
rpc Ping(google.protobuf.Empty) returns (PingResponse);
}

// Common messages
message DatabaseCredentials {
string username = 1;
string password = 2;
}

message TransactionContext {
string transaction_id = 1;
string database = 2;
}

// Database operations
message CreateDatabaseRequest {
string database_name = 1;
DatabaseCredentials credentials = 2;
map<string, string> options = 3;
}

message CreateDatabaseResponse {
bool success = 1;
string message = 2;
string database_id = 3;
}

message DropDatabaseRequest {
string database_name = 1;
DatabaseCredentials credentials = 2;
}

message DropDatabaseResponse {
bool success = 1;
string message = 2;
}

message ListDatabasesRequest {
DatabaseCredentials credentials = 1;
}

message ListDatabasesResponse {
repeated DatabaseInfo databases = 1;
}

message DatabaseInfo {
string name = 1;
int64 size = 2;
string status = 3;
map<string, string> properties = 4;
}

message GetDatabaseInfoRequest {
string database_name = 1;
DatabaseCredentials credentials = 2;
}

message GetDatabaseInfoResponse {
DatabaseInfo info = 1;
}

// Query operations
message ExecuteQueryRequest {
string database = 1;
string query = 2;
map<string, google.protobuf.Value> parameters = 3;
DatabaseCredentials credentials = 4;
TransactionContext transaction = 5;
int32 limit = 6;
int32 timeout_ms = 7;
}

message ExecuteQueryResponse {
repeated QueryResult results = 1;
int64 execution_time_ms = 2;
string query_plan = 3;
}

message ExecuteCommandRequest {
string database = 1;
string command = 2;
map<string, google.protobuf.Value> parameters = 3;
DatabaseCredentials credentials = 4;
TransactionContext transaction = 5;
}

message ExecuteCommandResponse {
bool success = 1;
string message = 2;
int64 affected_records = 3;
int64 execution_time_ms = 4;
}

message QueryResult {
repeated Record records = 1;
repeated ColumnMetadata columns = 2;
int64 total_records = 3;
}

message ColumnMetadata {
string name = 1;
string type = 2;
bool nullable = 3;
}

// Transaction operations
message BeginTransactionRequest {
string database = 1;
DatabaseCredentials credentials = 2;
TransactionIsolation isolation = 3;
}

enum TransactionIsolation {
READ_UNCOMMITTED = 0;
READ_COMMITTED = 1;
REPEATABLE_READ = 2;
SERIALIZABLE = 3;
}

message BeginTransactionResponse {
string transaction_id = 1;
int64 timestamp = 2;
}

message CommitTransactionRequest {
TransactionContext transaction = 1;
DatabaseCredentials credentials = 2;
}

message CommitTransactionResponse {
bool success = 1;
string message = 2;
int64 timestamp = 3;
}

message RollbackTransactionRequest {
TransactionContext transaction = 1;
DatabaseCredentials credentials = 2;
}

message RollbackTransactionResponse {
bool success = 1;
string message = 2;
}

// Record operations
message Record {
string rid = 1;
string type = 2;
map<string, google.protobuf.Value> properties = 3;
int32 version = 4;
}

message CreateRecordRequest {
string database = 1;
string type = 2;
map<string, google.protobuf.Value> properties = 3;
DatabaseCredentials credentials = 4;
TransactionContext transaction = 5;
}

message CreateRecordResponse {
Record record = 1;
bool success = 2;
string message = 3;
}

message GetRecordRequest {
string database = 1;
string rid = 2;
DatabaseCredentials credentials = 3;
TransactionContext transaction = 4;
}

message GetRecordResponse {
Record record = 1;
bool found = 2;
}

message UpdateRecordRequest {
string database = 1;
string rid = 2;
map<string, google.protobuf.Value> properties = 3;
DatabaseCredentials credentials = 4;
TransactionContext transaction = 5;
int32 expected_version = 6;
}

message UpdateRecordResponse {
Record record = 1;
bool success = 2;
string message = 3;
}

message DeleteRecordRequest {
string database = 1;
string rid = 2;
DatabaseCredentials credentials = 3;
TransactionContext transaction = 4;
}

message DeleteRecordResponse {
bool success = 1;
string message = 2;
}

// Streaming operations
message StreamQueryRequest {
string database = 1;
string query = 2;
map<string, google.protobuf.Value> parameters = 3;
DatabaseCredentials credentials = 4;
int32 batch_size = 5;
}

message BulkInsertRequest {
string database = 1;
string type = 2;
Record record = 3;
DatabaseCredentials credentials = 4;
}

message BulkInsertResponse {
int64 total_inserted = 1;
int64 total_failed = 2;
repeated string errors = 3;
}

// Server operations
message ServerStatusResponse {
string version = 1;
string status = 2;
int64 uptime_ms = 3;
int32 active_connections = 4;
map<string, string> metrics = 5;
}

message PingResponse {
int64 timestamp = 1;
string message = 2;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to be a duplicate or an older version of the protobuf definition located in grpc/src/main/proto/arcadedb-server.proto. Maintaining multiple versions of the API contract can lead to confusion and build issues. This file should likely be removed to ensure there is a single source of truth for the gRPC API.

@robfrank robfrank merged commit aae1b32 into main Sep 11, 2025
15 of 21 checks passed
@robfrank robfrank deleted the feat/2498-grpc-protocol branch September 11, 2025 13:50
@robfrank robfrank mentioned this pull request Sep 11, 2025
2 tasks
robfrank added a commit that referenced this pull request Nov 10, 2025
)

Co-authored-by: Oleg Cohen <oleg.cohen@shakeiq.ai>

(cherry picked from commit aae1b32)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add grpc protocol wrapper

2 participants