Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/common/grpc_client_manager.py b/common/grpc_client_manager.py
new file mode 100644
index 0000000..dbaff76
--- /dev/null
+++ b/common/grpc_client_manager.py
@@ -0,0 +1,77 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+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.
+"""
+
+import logging
+import sys
+
+import grpc
+import psutil
+from common.service_registry import ServiceRegistry
+
+
+class GRPCClientManager:
+    def __init__(
+        self, service_name: str, service_stub,
+        max_client_reuse: int = 60,
+    ):
+        self._client = None
+        self._service_stub = service_stub
+        self._service_name = service_name
+        self._num_client_use = 0
+        self._max_client_reuse = max_client_reuse
+
+    def get_client(self):
+        """
+        get_client returns a grpc client of the specified service in the cloud.
+        it will return a recycled client until the client fails or the number
+        of recycling reaches the max_client_use.
+        """
+        if self._client is None or \
+                self._num_client_use > self._max_client_reuse:
+            chan = ServiceRegistry.get_rpc_channel(
+                self._service_name,
+                ServiceRegistry.CLOUD,
+            )
+            self._client = self._service_stub(chan)
+            self._num_client_use = 0
+
+        self._num_client_use += 1
+        return self._client
+
+    def on_grpc_fail(self, err_code):
+        """
+        Try to reuse the grpc client if possible. We are yet to fix a
+        grpc behavior, where if DNS request blackholes then the DNS request
+        is retried infinitely even after the channel is deleted. To prevent
+        running out of fds, we try to reuse the channel during such failures
+        as much as possible.
+        """
+        if err_code != grpc.StatusCode.DEADLINE_EXCEEDED:
+            # Not related to the DNS issue
+            self._reset_client()
+        if self._num_client_use >= self._max_client_reuse:
+            logging.info('Max client reuse reached. Cleaning up client')
+            self._reset_client()
+
+            # Sanity check if we are not leaking fds
+            proc = psutil.Process()
+            max_fds, _ = proc.rlimit(psutil.RLIMIT_NOFILE)
+            open_fds = proc.num_fds()
+            logging.info('Num open fds: %d', open_fds)
+            if open_fds >= (max_fds * 0.8):
+                logging.error("Reached 80% of allowed fds. Restarting process")
+                sys.exit(1)
+
+    def _reset_client(self):
+        self._client = None
+        self._num_client_use = 0