milkcoke / java-gRPC-practice

This is practicce for gRPC Java + SpringBoot
0 stars 0 forks source link

gRPC

Service is defined using .proto \ Client app directly invokes server method on a different machine.

Quick Start

Generate java codes using protoc.

$ cd [project-root]/src/main/proto
$ protoc --proto_path=. --java_out=../java *.proto

Proto

Platform neutral, Language neutral data type. \ Serialize and Deserialize structured data.

message Person {
  string name = 1;
  int32 age = 2;
}

Compile

compile 하고나면 OuterClass가 나옴.. 에 \ 아니 근데 gralde 에서 설정만으로 바로 protoc 자동실행 못하나? \ 아래처럼 CLI 명령어 치는거 너무 추한데.. maven 쓰긴 싫고 아

Generate source files from *.proto

1. Set build.gradle

2. Install protoc

You have to install proto compiler (protoc) on protocolbuffers github page

3. Set the PATH environment

Register protoc bin file path on PATH.

4. Execute it!

You can give options as shown below.

5. Place the java files

Place the java files from *.proto compiled by protoc in java directory

protoc --proto_path=. --java_out=../java *.proto

Use generated files from *.proto

Place the source files from *.proto compiled by protoc into src/main/package/proto

Builder pattern

constructor's access modifier is 'private'!

You should use builder pattern!


public final class Balance extends
com.google.protobuf.GeneratedMessageV3 implements
BalanceOrBuilder {
// Builder 패턴만 적용 가능
// Use Balance.newBuilder() to construct.
private Balance(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
super(builder);
}
private Balance() {
}

//..Embedding Builder class!
public static final class Builder extends {
    public static Builder newBuilder() {
        return DEFAULT_INSTANCE.toBuilder();
    }    
}

}


#### Client.java
```java
public class Client {
    public static void main(String[] args) {
        Balance.newBuilder()
                .setAmount(10_000)
                .build();
    }
}

Protobuf vs JSON in terms of performance

Protobuf is light-weight and faster than Json.

I tested code executing 1_000_000 times for running Balance class serialize and deserialize \ The test result in as shown below.

Test environment

Index JSON protobuf
provided by jackson Google protobuf
Data type JSON byte stream
Encoding/Decoding Do No need
json 443 ms elapsed
protobuf 40 ms elapsed

10 x times better on protobuffer!

Why protocol buffer is faster?

Data show format as shown below.

json

{
  "amount": 10000  
}

protobuf

1=10000

Each property in protobuf has own tag

syntax = "proto3";

message Balance {
  int32 amount = 1; // 1 is tag
}

Proto buffer's data size is also smaller than JSON.

JSON is key-value type on the other hand, protobuf only has tag. \ 1 ~ 15 tag number charge 1 byte. 16 ~ 2047 tag number charge 2 bytes. \ So it's recommended using 1 ~ 15 tag number.

in JSON, if sent empty collection deserialized as [] (if it's list) \ but in protobuf, sending empty collection, that is not sent.

📝 protobuf is smaller and faster and no need en/decoding than JSON.

Guidelines of Proto

syntax = "proto3";

enum ResolutionLevel {
  HD = 0;
  FHD = 1;
  UHD = 2;
}

message Television {
  string brand = 1;
  reserved 2;
  reserved "year", "model";
  ResolutionLevel resolution = 3;
  int64 price = 4;
}

Proto type

Collections & Map

Java Type Proto Type
Collection / List repeated
Map map

Enum

Use 0 as default value It's important to keep in mind that the default value of 0 is only used if the field is not set.

syntax = "proto3";

message Product {
  int64 serial_number = 1;
}

enum BodyStyle {
  // enum 에서 0 은 default value 로 쓰여야한다.
  UNKNOWN = 0;
  SEDAN = 1;
  COUPE = 2;
  SUV = 3;
}

Default value

Proto type Default value
any number type 0
bool false
string empty string
enum first value (0)
repeated empty list
map wrapper / empty map
@DisplayName("Default value")
@Test
void testDefaultValue() {
    User user = User.newBuilder().build();

    assertThat(user.getAge()).isEqualTo(0); // default number value is zero
    assertThat(user.hasAddress()).isEqualTo(false);
}

Importing modules

syntax = "proto3";

import "car.proto";
import "address.proto";

message User {
  string name = 1;
  int32 age = 2;
  Address address = 3;
  repeated Car car = 4;
}

One of - interface

We can use interface and concrete class in java on .proto using oneof keyword. \ class created by protocol buffer has one's factory method. \ Refer to factory method pattern.

syntax = "proto3";

// This is interface having two concrete class
message Credentials {
  oneof mode {
    EmailCredentials emailMode = 1;
    PhoneOtp phoneMode = 2;
  }
}

message EmailCredentials {
  string email = 1;
  string password = 2;
}

message PhoneOtp {
  int32 number = 1;
  int32 code = 2;
}
class CredentialsTest {

    private Credentials credentials;

    private static void login(Credentials credentials) {

        switch (credentials.getModeCase()) {
            case EMAILMODE -> System.out.println(credentials.getEmailMode());
            case PHONEMODE -> System.out.println(credentials.getPhoneMode());
        }
    }

    @DisplayName("이메일 인증")
    @Test
    void testEmail() {
        EmailCredentials emailCredentials = EmailCredentials.newBuilder()
                .setEmail("falcon@tistory.com")
                .setPassword("khazix123")
                .build();

        credentials = Credentials.newBuilder()
                .setEmailMode(emailCredentials)
                .build();

        login(credentials);
    }
}

Naming convention

In Protocol Buffers (.proto files), the naming convention for properties is snake_case


Synchronous vs Asynchronous

Synchronous Asynchronous
Blocking and wait for the response Register a listner for callback.

This is completely up to the client.


Types of gRPC

Unary

One to one request - response model

Server streaming

One request and multiple streaming response

When to use? response data size is big so required chunking and streaming from server to client instead of sending everything at once.

Client Streaming

Client send multiple streaming requests but server only one response.

When to use? request large data to server (ex. file uploading) and server is required response only once.

Bidirectional Streaming

This is not strictly one to one. \ ex) Send 30 requests and response 10.

when to use? Client and server need coordinate and work together


Recommend project structure

Separate client with stub vs server with interface.

Deadline

Timeout for gRPC to complete. You can test this feature using

  1. Insert method Uninterruptibles.sleepUninterruptibly() on server side
  2. Insert method withDeadline(Deadline.after(2, TimeUnit.SECONDS)) on client side

I'll suggest example as shown below.

BankServiceProxy.java

@Service
@RequiredArgsConstructor
public class DefaultBankServiceProxy implements BankService {
    /** Test 3 wait seconds
    */
    @Override
    public BalanceDTO readBalance(BalanceVO balanceVO) {
        // ..
        Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);

        return new BalanceDTO(balance);
    }
}
@DisplayName("Deadline test 2 seconds but 3 sec blocking")
@Test
void getBalanceTestWithDeadLine() {
    BalanceCheckRequest balanceCheckRequest = BalanceCheckRequest.newBuilder()
            .setAccountNumber(5)
            .build();

    assertThrows(StatusRuntimeException.class, ()->{
        Balance balance = this.bankServiceBlockingStub
                .withDeadline(Deadline.after(2, TimeUnit.SECONDS)) // 2 second deadline.
                .getBalance(balanceCheckRequest);
        System.out.println(balance);
    });

Interceptor

Handle cross-cutting concerns

Interceptor intercepts client request

When to use?

How to use?

Define class implementing ClientInterceptor or ServerInterceptor and register it each side.

DeadlineInterceptor.java

public class DeadlineInterceptor implements ClientInterceptor {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
        // if custom deadline is set, pass it.
        if (callOptions.getDeadline() != null) return next.newCall(method, callOptions);

        return next.newCall(method, callOptions.withDeadline(Deadline.after(2, TimeUnit.SECONDS)));
    }
}

Context

You can use Context for handling in interceptor or transferring to Service It's safe to use the Context. Which has thread local context current RPC can only get data.

Role of Context

In gRPC, the context plays a critical role in providing metadata, cancellation signals, and deadlines for a gRPC call.

In interceptor, Context is not used explicitly. However, it's used implicitly to carry the call-specific information, including deadline and metadata between the client and server.

RequestInterceptor.java

@Slf4j
public class RequestInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
        String requestId = headers.get(Metadata.Key.of("request_id", Metadata.ASCII_STRING_MARSHALLER));

        if (Objects.isNull(requestId)) {
            Status invalidStatus =  Status.UNAUTHENTICATED.withDescription("Should input request_id");
            call.close(invalidStatus, headers);
        }

        // ThreadLocal current thread only the information
        // it's safe to use
        Context context = Context.current().withValue(
                ServerHeaders.CTX_REQUEST_ID,
                requestId
        );

        return Contexts.interceptCall(context, call, headers, next);
//        return next.startCall(call, headers);
    }
}

⚠️ gRPC server sees the last service definition, so last intercept is called first.

Server server = ServerBuilder.forPort(6443)
// ⚠️ interceptors are executed in reverse order
.intercept(new AuthInterceptor())
.intercept(new RequestInterceptor())
.addService(bankService)
.build();

Error Handling via Metadata

Don't make all custom error

Standard exception (e.g. IllegalArgumentException) is apt to many developers. \ If standard one is enough to delivery meaning to client. Don't make custom error exception classes. CustomError can be managed by GlobalExceptionHandler(*ControllerAdvice) and can reduce stacktrace memory cost by overriding fillInStackTrace() as shown below

class CustomError extends RuntimeException {
    @Override
    public synchronized Throwable fillInStackTrace() {
        // don't require calling super.fillInStackTrace() recursively.
        return this;
    }
}

Refer to this blog post

What's difference Metadata and Trailers ?

Both are used to send additional information. In terms of HTTP/2 both are headers. \ However, there's some difference. \ Metadata is available to both the client and server at all times during the lifecycle of the call. \ On the other hand, trailers can be sent only from server to client after the request has been processed and response has been sent. \ Trailers is useful when to send response size, checksum, error message, status code etc..