Search code examples
dockerdocker-composedockerfilegrpcgrpc-node

gRPC-node: When *Dockerizing* Service, request doesn't go through service's server? [Screenshots included]


I created a really simple bookstore with a Books, Customer, and a main service. This particular problem involves the main and books service.

I'm currently making a gRPC request called: "createBook", which creates a book in our DB, and also console logs.

When running the gRPC server (booksServer) without docker, the process runs smoothly.

But as soon as I use docker, it seems as if a gRPC request doesn't go into the gRPC server...

By "using docker" I mean using docker to run the booksServer. (As shown below)

Result: Without Docker

As you can see, without docker, the request is fulfilled, and everything works as it should. 
Our gRPC client makes a call to the gRPC server (in which metadata is created) and the metadata is also sent back to the client. 
(Scroll down to see the gRPC server file with the method called "getBooks".)

enter image description here

booksServer (without docker) enter image description here

*** Notice the console logs in the booksServer!!! ***

Let me run the booksServer (with docker)

(Dockerfile below)

FROM node:12.14.0
WORKDIR /usr/src/app
COPY package*.json ./
COPY . /usr/src/app
RUN npm install
RUN npm install nodemon -g
EXPOSE 30043
CMD ["nodemon", "booksServer.js"



Here's my main service docker file too which initiates the request:

FROM node:12.14.0
WORKDIR /usr/src/app
COPY package*.json ./
COPY . /usr/src/app
# COPY wait-for-it.sh . 
# RUN chmod +x /wait-for-it.sh
RUN npm install
EXPOSE 4555
CMD ["node", "main.js"]

enter image description here enter image description here

^^^ Notice how when dockerfile is used to run booksServer
it doesn't go/run inside the booksServer file 
***It does NOT produce any console.logs when I fire off a gRPC requesst***

This is what the booksServer.js file looks like enter image description here

Heres the Books Stub

//use this for bookInitiator
const path = require('path');
const PROTO_PATH = path.join(__dirname, "../protos/books.proto");

const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  arrays: true
});

const BooksService = grpc.loadPackageDefinition(packageDefinition).BooksService;

// potential issues to fix 1) making localhost port dynamic 2) docker containerization may cause conflict

const client = new BooksService (
  "172.17.0.2:30043",
  grpc.credentials.createInsecure()
);

console.log("Creating stub inside booksStub");
module.exports = client;

Here's the gRPC Server file (with the binded ports).

// const PROTO_PATH = "../protos/books.proto";
const path = require('path');
const PROTO_PATH = path.join(__dirname, './protos/books.proto');
const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");
const express = require("express");
const controller = require("./booksController.js");
const app = express();
app.use(express.json());

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  arrays: true,
});

const booksProto = grpc.loadPackageDefinition(packageDefinition);

const { v4: uuidv4 } = require("uuid");

const server = new grpc.Server();

server.addService(booksProto.BooksService.service, {
  CreateBook: (call, callback) => {
    console.log("call to CreateBook");

    //sample will take the call information from the client(stub)
    const book = {
      title: call.request.title,
      author: call.request.author,
      numberOfPages: call.request.numberOfPages,
      publisher: call.request.publisher,
      
      id: call.request.id,
    };

    controller.createBook(book);

    let meta = new grpc.Metadata();
    meta.add("response", "none");
    console.log("metadata in createBook...: ", meta);
    call.sendMetadata(meta);

    callback(
      null,
      //bookmodel.create
      {
        title: `completed for: ${call.request.title}`,
        author: `completed for: ${call.request.author}`,
        numberOfPages: `completed for: ${call.request.numberOfPages}`,
        publisher: `completed for: ${call.request.publisher}`,
        id: `completed for: ${call.request.id}`,
      }
    );
  },
  GetBooks: (call, callback) => {
    console.log("call to GetBooks");
    // read from database
    let meta = new grpc.Metadata();
    meta.add('response', 'none');
    call.sendMetadata(meta);

    controller.getBooks(callback);
  }
});

server.bind("0.0.0.0:30043", grpc.ServerCredentials.createInsecure());
console.log("booksServer.js running at 0.0.0.0:30043");
console.log("Inside Books Server!");
console.log("call from books server");

server.start();

horus.js (custom made simple tracing tool), grab trace grabs the journey of a certain request and sends it back to the gRPC client as metadata

const fs = require("fs");
const grpc = require("grpc");
const path = require("path");

class horus {
  constructor(name) {
    this.serviceName = name; // represents the name of the microservices
    this.startTime = null;
    this.endTime = null;
    this.request = {};
    this.targetService = null; // represents the location to which the request was made
    this.allRequests = []; // array which stores all requests
    this.timeCompleted = null;
    this.call;
  }

  static getReqId() {
    // primitive value - number of millisecond since midnight January 1, 1970 UTC
    // add service name/ initials to the beginning of reqId?
    return new Date().valueOf();
  }

  // start should be invoked before the request is made
  // start begins the timer and initializes the request as pending
  start(targetService, call) {
    this.startTime = Number(process.hrtime.bigint());
    this.request[targetService] = "pending"; // {books: 'pending', responseTime: 'pending'}
    this.request.responseTime = "pending";
    this.targetService = targetService;
    this.call = call;
    this.request.requestId = horus.getReqId();
  }
  // end should be invoked when the request has returned
  end() {
    this.endTime = Number(process.hrtime.bigint());
    this.request.responseTime = (
      (this.endTime - this.startTime) /
      1000000
    ).toFixed(3); //converting into ms.
    this.sendResponse();
    this.request.timeCompleted = this.getCurrentTime();
  }
  // grabTrace accepts inserts trace into request
  // trace represents the "journey" of the request
  // trace expects metaData to be 'none when the server made no additional requests
  // trace expects metaData to be the request object generated by the server otherwise
  // in gRPC, the trace must be sent back as meta data. objects should be converted with JSON.parse
  grabTrace(metaData) {
    //console.log("incoming meta data ", metaData);
    console.log("Inside Grab Trace Method.");
    console.log("Metadata inside grabTrace: ", metaData);
    if (metaData === "none" || metaData === undefined) this.request[this.targetService] = "none";
    else {
      metaData = JSON.parse(metaData);
      this.request[this.targetService] = metaData;
    }
    this.allRequests.push(this.request);
    this.sendResponse();
  }
  // displayRequests logs to the console all stored requests
  // setTimeout builds in deliberate latency since metadata may be sent before or after a request is done processing
  displayRequests() {
    console.log("\n\n");
    console.log("Logging all requests from : ", this.serviceName);
    this.allRequests.forEach((request) => {
      console.log("\n");
      console.log(request);
    });
    console.log("\n\n");
  }
  // sends response via metadata if service is in the middle of a chain
  sendResponse() {
    if (
      this.request.responseTime === "pending" ||
      this.request[this.targetService] === "pending" ||
      this.call === undefined
    )
      return;
    console.log("Inside send response");
    let meta = new grpc.Metadata();
    meta.add("response", JSON.stringify(this.request));
    console.log('meta in send response: ', meta)
    this.call.sendMetadata(meta);
  }
  writeToFile() {
    console.log("call to writeToFile");
    console.log("logging request obj ", this.request);
    let strRequests = "";
    for (let req of this.allRequests) {
      // First write to file - contains Total
      // subsequent - chained requests
      strRequests += `Request ID: ${req.requestId}\n`;
      strRequests += `"${
        Object.keys(req)[0]
      }" service -> Response received in ${Object.values(req)[1]} ms (Total)\n`;
      strRequests += `Timestamp: ${req.timeCompleted}\n`;
      // while we don't hit an empty object on the 1st key, go inside
      // add numbering in order for nested requests inside original?!
      let innerObj = Object.values(req)[0];
      while (innerObj !== "none") {
        strRequests += `"${
          Object.keys(innerObj)[0]
        }" service -> Response received in ${Object.values(innerObj)[1]} ms\n`;
        strRequests += `Timestamp: ${innerObj.timeCompleted}\n`;
        innerObj = Object.values(innerObj)[0];
      }
      strRequests +=
        "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n";
    }
    console.log('strRequests', strRequests)
    fs.writeFile(this.serviceName + 'data' + '.txt', strRequests, { flag: "a+" }, (err) => {
      if (err) {
        console.error(err);
      }
    }); //'a+' is append mode
  }

module.exports = horus;


main.js (initiates gRPC client request)

const path = require('path');
// const grpc = require("grpc");
const customersStub = require("./stubs/customersStub.js");
const booksStub = require("./stubs/booksStub.js");
const horusTracer = require(path.join(__dirname, "./horus/horus.js"));

//In master branch
console.log("Stub is Inside main service!!!");

const book = {
  title: "ITttttt",
  author: "Stephen King",
  numberOfPages: 666,
  publisher: "Random House",
  id: 200,
};

const bookId = {
  id: 200
}

const customer = {
  id: 123,
  name: "Lily",
  age: 23,
  address: "Blablabla",
  favBookId: 100
};

const customerId = {
  id: 123
}

let ht = new horusTracer("main");


function CreateBook () {
  ht.start('books')
  booksStub.CreateBook(book, (error, response) => {
    if (error) console.log("there was an error ", error);
    ht.end();
    ht.displayRequests();
    ht.writeToFile();
  }).on('metadata', (metadata) => {
    console.log("Before grab trace is invoked!");
    ht.grabTrace(metadata.get('response')[0]);
  });
}

}


CreateBook(); //Works

What I think is the issue.


Edit: murgatroid99 mentioned that it was a networking issue with docker!


~~~~~~~~~
I initially thought this was a networking issue, but I don't think it is 
because all my docker files are running on the default bridge network. 
So they all technically can communicate with one another...

Is it something wrong with nodemon interacting with Docker? 
Does the server not output the console logs...? 
Is the server actually running and working...?

Do I need a reverse proxy like nginx?

``

Solution

  • The problem is that your server is binding to "127.0.0.1:30043". You say that you are running the docker images using the default bridge network. In that mode the docker image has a different (virtual) network than the host machine has, so its loopback address is different from the host machine's loopback address. To fix that, you can instead bind the server to 0.0.0.0:30043 or [::]:30043to bind to other network interfaces that the client can connect to from outside of the docker container.

    For the same reason, connecting the client to localhost:30043 will not work: its "localhost" address also refers to the loopback interface within the docker container. You should instead replace "localhost" with the IP address of the server container.

    Alternatively, as described in this question, you can network the docker containers in "host" mode so that they share the same network with the host machine.