2

I'm drafting a chat service in gRPC java with bidirectional streaming. Simplified flow is like below,

  1. When user joins chat service, user's StreamObserver will be stored in a chat room repository i.e. a simple HashMap holding userId - StreamObserver in the server.
  2. After a while, when user sends a chat message, server receives the request and broadcasts the message to all the users in the chat room by iterating StreamObservers stored in the chat room repository and calling onNext method.

This works fine when there's only 1 server existing, however once scaled out to multiple servers, clients' StreamObservers will be stored in a specific server and will not exist in other servers as gRPC opens a single HTTP connection to the server initially connected.

What I need is sending the message to all the users in the same chat room by getting all StreamObservers scattered around the servers, does anyone have good experience with this kind of situation? I tried to store StreamObserver in a single storage however as it isn't serializable, I couldn't store it in a shared storage like redis.

gunjasal
  • 29
  • 2
  • 9
  • You have to make the clients connect to all server instances by using a `NameResolver` to listen to all and then use a load balance strategy to send messages to the server. Here is one example https://sultanov.dev/blog/grpc-client-side-load-balancing/ . did you already do that? – Felipe Jan 27 '21 at 07:09
  • can you post the code of the client that listen to messages from all gRPC server implementation? – Felipe Jan 27 '21 at 09:12
  • 2
    @Felipe Thanks! Never had a thought about client-side load balancing. Will have a look and try. It seems I need a look on [this page](https://github.com/grpc/grpc/blob/master/doc/load-balancing.md), too. Client code is simple as possible. It defines a StreamObserver inside the bidirectional streaming method. I'll first have a look on the client-side LB, and if there's any other problem will post the client code, too. Thanks again! – gunjasal Jan 27 '21 at 11:18
  • I implemented the code as the link says. It does load balance. However the chat is not working. I am working on it (I liked the problem =)). I will post the answer to you. – Felipe Jan 27 '21 at 11:20

1 Answers1

1

I implemented a chat using gRPC and 3 servers with load balance. The first thing to achieve the load balance is to use a ManagedChannel with defaultLoadBalancingPolicy. In my case I used round_robin policy. And create the channel using a MultiAddressNameResolverFactory with the host and ports of the three servers. Here I create a client chat for Alice. Then You copy this class and create a client chat for Bob. This should already do the load balance that you asked.

public class ChatClientAlice {
    private NameResolver.Factory nameResolverFactory;
    private ManagedChannel channel;

    public static void main(String[] args) {
        ChatClientAlice chatClientAlice = new ChatClientAlice();
        chatClientAlice.createChannel();
        chatClientAlice.runBiDiStreamChat();
        chatClientAlice.closeChannel();
    }

    private void createChannel() {
        nameResolverFactory = new MultiAddressNameResolverFactory(
                new InetSocketAddress("localhost", 50000),
                new InetSocketAddress("localhost", 50001),
                new InetSocketAddress("localhost", 50002)
        );
        channel = ManagedChannelBuilder.forTarget("service")
                .nameResolverFactory(nameResolverFactory)
                .defaultLoadBalancingPolicy("round_robin")
                .usePlaintext()
                .build();
    }

    private void closeChannel() { channel.shutdown(); }

    private void runBiDiStreamChat() {
        System.out.println("creating Bidirectional stream stub for Alice");
        EchoServiceGrpc.EchoServiceStub asyncClient = EchoServiceGrpc.newStub(channel);
        CountDownLatch latch = new CountDownLatch(1);

        StreamObserver<EchoRequest> requestObserver = asyncClient.echoBiDi(new StreamObserver<EchoResponse>() {
            @Override
            public void onNext(EchoResponse value) { System.out.println("chat: " + value.getMessage()); }

            @Override
            public void onError(Throwable t) { latch.countDown(); }

            @Override
            public void onCompleted() { latch.countDown(); }
        });

        Stream.iterate(0, i -> i + 1)
                .limit(10)
                .forEach(integer -> {
                    String msg = "Hello, I am " + ChatClientAlice.class.getSimpleName() + "! I am sending stream message number " + integer + ".";
                    System.out.println("Alice says: " + msg);
                    EchoRequest request = EchoRequest.newBuilder()
                            .setMessage(msg)
                            .build();
                    requestObserver.onNext(request);
                    // throttle the stream
                    try { Thread.sleep(5000); } catch (InterruptedException e) { }
                });
        requestObserver.onCompleted();
        System.out.println("Alice BiDi stream is done.");
        try {
            // wait for the time set on the stream + the throttle
            latch.await((5000 * 20), TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

On the server service you will have to use a singleton to store the StreamObservers every time that you receive a new request from new clients. Instead of returning the message to a single observer responseObserver.onNext(response); you will iterate all observers and send the message to all singletonObservers.getObservers().forEach(..... Although this has nothing to do with the load balance strategy I thought that it is worthwhile to post because if you don't implement it well your clients will not receive messages from other clients.

public class ChatServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {

    private final String name;
    private final SingletlonChatStreamObserver singletonObservers;

    ChatServiceImpl(String name) {
        this.name = name;
        this.singletonObservers = SingletlonChatStreamObserver.getInstance();
    }

    @Override
    public StreamObserver<EchoRequest> echoBiDi(StreamObserver<EchoResponse> responseObserver) {
        System.out.println("received bidirectional call");

        singletonObservers.addObserver(responseObserver);
        System.out.println("added responseObserver to the pool of observers: " + singletonObservers.getObservers().size());

        StreamObserver<EchoRequest> requestObserver = new StreamObserver<EchoRequest>() {
            @Override
            public void onNext(EchoRequest value) {
                String msg = value.getMessage();
                System.out.println("received message: " + msg);
                EchoResponse response = EchoResponse.newBuilder()
                        .setMessage(msg)
                        .build();
                // do not send messages to a single observer but to all observers on the pool
                // responseObserver.onNext(response);
                // observers.foreach...
                singletonObservers.getObservers().forEach(observer -> {
                    observer.onNext(response);
                });
            }

            @Override
            public void onError(Throwable t) {
                // observers.remove(responseObserver);
                singletonObservers.getObservers().remove(responseObserver);
                System.out.println("removed responseObserver to the pool of observers");
            }

            @Override
            public void onCompleted() {
                // do not complete messages to a single observer but to all observers on the pool
                // responseObserver.onCompleted();
                // observers.foreach
                singletonObservers.getObservers().forEach(observer -> {
                    observer.onCompleted();
                });

                // observers.remove(responseObserver);
                System.out.println("removed responseObserver to the pool of observers");
            }
        };
        return requestObserver;
    }
}

and this is my SingletlonChatStreamObserver to have only one of this object for all 3 servers:

public class SingletlonChatStreamObserver implements Serializable {

    private static volatile SingletlonChatStreamObserver singletonSoleInstance;
    private static volatile ArrayList<StreamObserver<EchoResponse>> observers;

    private SingletlonChatStreamObserver() {
        //Prevent form the reflection api.
        if (singletonSoleInstance != null) {
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    public static SingletlonChatStreamObserver getInstance() {
        if (singletonSoleInstance == null) { //if there is no instance available... create new one
            synchronized (SingletlonChatStreamObserver.class) {
                if (singletonSoleInstance == null) {
                    observers = new ArrayList<StreamObserver<EchoResponse>>();
                    singletonSoleInstance = new SingletlonChatStreamObserver();
                }
            }
        }
        return singletonSoleInstance;
    }

    //Make singleton from serializing and deserialize operation.
    protected SingletlonChatStreamObserver readResolve() {
        return getInstance();
    }

    public void addObserver(StreamObserver<EchoResponse> streamObserver) {
        observers.add(streamObserver);
    }

    public ArrayList<StreamObserver<EchoResponse>> getObservers() {
        return observers;
    }
}

I will commit the complete code on my explore-grpc project.

Felipe
  • 7,013
  • 8
  • 44
  • 102