├── k8s ├── namespace.yaml ├── service.yaml ├── ingress.yaml └── deployment.yaml ├── scripts ├── build-docker.sh ├── build.sh ├── protoc-gen.sh ├── run-terraform.sh ├── run-k8-server.sh └── run-helm.sh ├── .gitignore ├── terraform ├── outputs.tf ├── variables.tf └── main.tf ├── helm-charts ├── Chart.yaml ├── templates │ ├── service.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── ingress.yaml │ ├── deployment.yaml │ └── _helpers.tpl ├── .helmignore └── values.yaml ├── api ├── server │ ├── error.go │ ├── cache_service.go │ ├── api.go │ └── api_test.go ├── Dockerfile └── main.go ├── go.mod ├── .travis.yml ├── Makefile ├── proto ├── cache-service.proto └── cache-service.pb.go ├── license ├── examples └── client.go ├── go.sum └── readme.md /k8s/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: grpc-cache 5 | -------------------------------------------------------------------------------- /scripts/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t knrt10/grpc-cache -f api/Dockerfile . 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | server-cache 4 | client-cache 5 | .terraform/ 6 | terraform.* 7 | .terraform* 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go build -o server-cache api/main.go && go build -o client-cache examples/client.go 3 | -------------------------------------------------------------------------------- /scripts/protoc-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | protoc --proto_path=proto --go_out=plugins=grpc:proto cache-service.proto 4 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | output "ingress_host_ip" { 3 | value = kubernetes_ingress.grpc-cache.load_balancer_ingress.0.ip 4 | description = "IP mapping of ingress to host" 5 | } 6 | -------------------------------------------------------------------------------- /helm-charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: grpc-cache 3 | description: A Helm chart for Kubernetes 4 | 5 | type: application 6 | 7 | version: 0.1.0 8 | 9 | appVersion: 1.16.0 10 | -------------------------------------------------------------------------------- /api/server/error.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNoKey for key not found 7 | ErrNoKey = errors.New("No key found") 8 | // ErrKeyExpired for keys expired 9 | ErrKeyExpired = errors.New("Key expired") 10 | ) 11 | -------------------------------------------------------------------------------- /k8s/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: grpc-cache 5 | namespace: grpc-cache 6 | spec: 7 | selector: 8 | name: grpc-cache 9 | ports: 10 | - name: grpc 11 | port: 5001 12 | targetPort: 5001 13 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | ADD . /go/src/github.com/knrt10/gRPC-cache/ 4 | 5 | WORKDIR /go/src/github.com/knrt10/gRPC-cache/ 6 | 7 | RUN chmod 777 scripts/protoc-gen.sh && chmod 777 scripts/build.sh && scripts/build.sh 8 | 9 | ENTRYPOINT [ "./server-cache" ] 10 | 11 | EXPOSE 5001 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knrt10/grpc-cache 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/golang/protobuf v1.4.1 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect 8 | golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect 9 | google.golang.org/grpc v1.29.1 10 | ) 11 | -------------------------------------------------------------------------------- /helm-charts/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.service.name }} 5 | namespace: {{ .Values.namespace }} 6 | spec: 7 | selector: 8 | name: {{ .Values.service.name }} 9 | ports: 10 | - name: grpc 11 | port: {{ .Values.service.port }} 12 | targetPort: {{ .Values.service.targetPort }} 13 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "image_name" { 2 | type = string 3 | default = "grpc-cache" 4 | } 5 | 6 | variable "image_repository" { 7 | type = string 8 | default = "knrt10/grpc-cache" 9 | } 10 | 11 | variable "kube_defaultspace" { 12 | type = string 13 | default = "grpc-cache" 14 | } 15 | 16 | variable "kube_deployment_replica" { 17 | type = number 18 | default = 1 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /helm-charts/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm-charts/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "helm-charts.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "helm-charts.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "helm-charts.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /helm-charts/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helm-charts. 2 | 3 | image: 4 | name: grpc-cache 5 | repository: knrt10/grpc-cache 6 | 7 | ingress: 8 | annotations: 9 | kubernetes.io/ingress.class: nginx 10 | nginx.ingress.kubernetes.io/backend-protocol: GRPC 11 | nginx.ingress.kubernetes.io/ssl-redirect: "true" 12 | host: grpc-cache.example.com 13 | 14 | namespace: grpc-cache 15 | 16 | replicaCount: 1 17 | 18 | service: 19 | name: grpc-cache 20 | port: 5001 21 | targetPort: 5001 22 | -------------------------------------------------------------------------------- /k8s/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: grpc-cache 5 | namespace: grpc-cache 6 | annotations: 7 | "kubernetes.io/ingress.class": "nginx" 8 | "nginx.ingress.kubernetes.io/backend-protocol": "GRPC" 9 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 10 | spec: 11 | rules: 12 | - host: grpc-cache.example.com 13 | http: 14 | paths: 15 | - backend: 16 | serviceName: grpc-cache 17 | servicePort: grpc 18 | -------------------------------------------------------------------------------- /helm-charts/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Values.namespace }} 6 | annotations: 7 | {{- range $key, $val := .Values.ingress.annotations }} 8 | {{ $key }}: {{ $val | quote }} 9 | {{- end }} 10 | spec: 11 | rules: 12 | - host: {{ .Values.ingress.host }} 13 | http: 14 | paths: 15 | - backend: 16 | serviceName: {{ .Values.service.name }} 17 | servicePort: grpc 18 | -------------------------------------------------------------------------------- /k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: grpc-cache 5 | namespace: grpc-cache 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: grpc-cache 11 | template: 12 | metadata: 13 | labels: 14 | name: grpc-cache 15 | namespace: grpc-cache 16 | spec: 17 | containers: 18 | - name: grpc-cache 19 | image: knrt10/grpc-cache 20 | ports: 21 | - containerPort: 5001 22 | name: grpc 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/run-terraform.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | terraform init terraform/ 4 | 5 | terraform apply -auto-approve terraform 6 | 7 | HOST_INGRESS=$(terraform output ingress_host_ip) 8 | 9 | ADDRESS_MAP_EXIST=$(grep "$HOST_INGRESS grpc-cache.example.com" /etc/hosts) 10 | 11 | if [ "$ADDRESS_MAP_EXIST" == "" ]; then 12 | echo "Adding ingress host mapping to /etc/hosts" 13 | sudo -- sh -c -e "echo '$HOST_INGRESS grpc-cache.example.com' >> /etc/hosts"; 14 | fi 15 | 16 | echo "Started all resources successfully, you can use the application now." 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: go 3 | 4 | go: 5 | - 1.11.x 6 | - 1.12.x 7 | - 1.13.x 8 | - tip 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | 14 | env: 15 | - GO111MODULE=on 16 | 17 | before_install: 18 | - go get github.com/axw/gocov/gocov 19 | - go get github.com/mattn/goveralls 20 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 21 | 22 | script: 23 | - go test api/server/* -v -cover -race 24 | - $HOME/gopath/bin/goveralls -ignore "proto/*" -service=travis-ci 25 | -------------------------------------------------------------------------------- /helm-charts/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Values.namespace }} 6 | spec: 7 | replicas: {{ .Values.replicaCount }} 8 | selector: 9 | matchLabels: 10 | name: {{ .Chart.Name }} 11 | template: 12 | metadata: 13 | labels: 14 | name: {{ .Chart.Name }} 15 | namespace: {{ .Values.namespace }} 16 | spec: 17 | containers: 18 | - name: {{ .Values.image.name | quote}} 19 | image: {{ .Values.image.repository | quote}} 20 | ports: 21 | - containerPort: 5001 22 | name: grpc 23 | 24 | 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | - chmod 777 scripts/protoc-gen.sh && chmod 777 scripts/build.sh 3 | - scripts/build.sh 4 | 5 | docker: 6 | - chmod 777 scripts/build-docker.sh 7 | - scripts/build-docker.sh 8 | 9 | protoc: 10 | - scripts/protoc-gen.sh 11 | 12 | server: 13 | - go run api/main.go 14 | 15 | dockerServer: 16 | - docker run -it -p 5001:5001 knrt10/grpc-cache 17 | 18 | client: 19 | - go run examples/client.go 20 | 21 | test: 22 | - go test api/server/* -v -cover -race 23 | 24 | run-k8s-server: 25 | - chmod 777 scripts/run-k8-server.sh 26 | - scripts/run-k8-server.sh 27 | 28 | run-helm-server: 29 | - chmod 777 scripts/run-helm.sh 30 | - scripts/run-helm.sh 31 | 32 | run-terraform-server: 33 | - chmod 777 scripts/run-terraform.sh 34 | - scripts/run-terraform.sh 35 | -------------------------------------------------------------------------------- /proto/cache-service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package cacheService; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | // Item is what is stored in the cache 8 | message Item { 9 | string key = 1; 10 | string value = 2; 11 | string expiration = 3; 12 | } 13 | 14 | message GetKey { 15 | string key = 1; 16 | } 17 | 18 | message AllItems { 19 | repeated Item items = 1; 20 | } 21 | 22 | message Success { 23 | bool success = 1; 24 | } 25 | 26 | service CacheService { 27 | rpc Add (Item) returns (Item); 28 | rpc Get (GetKey) returns (Item); 29 | rpc GetByPrefix(GetKey) returns (AllItems); 30 | rpc GetAllItems(google.protobuf.Empty) returns (AllItems); 31 | rpc DeleteKey(GetKey) returns (Success); 32 | rpc DeleteAll(google.protobuf.Empty) returns (Success); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kautilya Tripathi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/run-k8-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # create resources for k8s 4 | kubectl apply -f ./k8s/namespace.yaml 5 | kubectl apply -f ./k8s/service.yaml 6 | kubectl apply -f ./k8s/deployment.yaml 7 | kubectl apply -f ./k8s/ingress.yaml 8 | 9 | # set current context namespace 10 | kubectl config set-context $(kubectl config current-context) --namespace=grpc-cache 11 | 12 | HOST_INGRESS=$(kubectl get ingress grpc-cache -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 13 | 14 | # setup host 15 | while true; do 16 | if [ "$HOST_INGRESS" != "" ]; then 17 | echo "Ingress address configured to: $HOST_INGRESS" 18 | break 19 | fi 20 | echo "Waiting for ingress to configure, sleeping for 10 seconds" 21 | HOST_INGRESS=$(kubectl get ingress grpc-cache -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 22 | sleep 10 23 | done 24 | 25 | ADDRESS_MAP_EXIST=$(grep "$HOST_INGRESS grpc-cache.example.com" /etc/hosts) 26 | 27 | if [ "$ADDRESS_MAP_EXIST" == "" ]; then 28 | echo "Adding ingress host mapping to /etc/hosts" 29 | sudo -- sh -c -e "echo '$HOST_INGRESS grpc-cache.example.com' >> /etc/hosts"; 30 | fi 31 | 32 | echo "Started all resources successfully, you can use the application now." 33 | -------------------------------------------------------------------------------- /scripts/run-helm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # check namespace if present 4 | { 5 | NAMESPACE_EXIST=$(kubectl get ns grpc-cache -o jsonpath='{.metadata.name}') 6 | } &> /dev/null 7 | 8 | if [ "$NAMESPACE_EXIST" != "grpc-cache" ]; then 9 | kubectl create ns grpc-cache 10 | fi 11 | 12 | # set current context namespace 13 | kubectl config set-context $(kubectl config current-context) --namespace=grpc-cache 14 | 15 | # create resources for k8s 16 | helm install grpc-cache ./helm-charts 17 | 18 | HOST_INGRESS=$(kubectl get ingress grpc-cache -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 19 | 20 | # setup host 21 | while true; do 22 | if [ "$HOST_INGRESS" != "" ]; then 23 | echo "Ingress address configured to: $HOST_INGRESS" 24 | break 25 | fi 26 | echo "Waiting for ingress to configure, sleeping for 10 seconds" 27 | HOST_INGRESS=$(kubectl get ingress grpc-cache -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 28 | sleep 10 29 | done 30 | 31 | ADDRESS_MAP_EXIST=$(grep "$HOST_INGRESS grpc-cache.example.com" /etc/hosts) 32 | 33 | if [ "$ADDRESS_MAP_EXIST" == "" ]; then 34 | echo "Adding ingress host mapping to /etc/hosts" 35 | sudo -- sh -c -e "echo '$HOST_INGRESS grpc-cache.example.com' >> /etc/hosts"; 36 | fi 37 | 38 | echo "Started all resources successfully, you can use the application now." 39 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "time" 9 | 10 | cache "github.com/knrt10/grpc-cache/api/server" 11 | api "github.com/knrt10/grpc-cache/proto" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/reflection" 14 | ) 15 | 16 | var ( 17 | address string 18 | expire int 19 | cleanup int 20 | ) 21 | 22 | func main() { 23 | // Get address from flag 24 | flag.StringVar(&address, "addr", ":5001", "Address on which you want to run server") 25 | flag.IntVar(&expire, "exp", 10, "Default expiration duration of cache is 10 min") 26 | flag.IntVar(&cleanup, "cln", 5, "Cleanup interval duration of expired cache is 5 min") 27 | flag.Parse() 28 | 29 | opts := []grpc.ServerOption{ 30 | grpc.MaxConcurrentStreams(200), 31 | } 32 | 33 | // create a gRPC server object 34 | grpcServer := grpc.NewServer(opts...) 35 | // Default expiration of cache is 10 minutes and default purge time for expired items is 5 minutes 36 | api.RegisterCacheServiceServer(grpcServer, cache.NewCacheService(time.Duration(expire)*time.Minute, time.Duration(cleanup)*time.Minute)) 37 | 38 | reflection.Register(grpcServer) 39 | 40 | lis, err := net.Listen("tcp", address) 41 | if err != nil { 42 | log.Fatalf("Error in starting server %v", err) 43 | } 44 | fmt.Println("Started the server on:", address) 45 | if err := grpcServer.Serve(lis); err != nil { 46 | log.Fatalf("err in serving gRPC %v\n", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /helm-charts/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "helm-charts.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "helm-charts.fullname" -}} 15 | {{- if .Values.fullnameOverride }} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 17 | {{- else }} 18 | {{- $name := default .Chart.Name .Values.nameOverride }} 19 | {{- if contains $name .Release.Name }} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 21 | {{- else }} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "helm-charts.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "helm-charts.labels" -}} 38 | helm.sh/chart: {{ include "helm-charts.chart" . }} 39 | {{ include "helm-charts.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end }} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "helm-charts.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "helm-charts.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end }} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "helm-charts.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create }} 59 | {{- default (include "helm-charts.fullname" .) .Values.serviceAccount.name }} 60 | {{- else }} 61 | {{- default "default" .Values.serviceAccount.name }} 62 | {{- end }} 63 | {{- end }} 64 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | provider "kubernetes" {} 2 | 3 | # create namespace 4 | resource "kubernetes_namespace" "grpc-cache" { 5 | metadata { 6 | name = var.kube_defaultspace 7 | } 8 | } 9 | 10 | # create deployment 11 | resource "kubernetes_deployment" "grpc-cache" { 12 | metadata { 13 | name = "grpc-cache" 14 | namespace = var.kube_defaultspace 15 | } 16 | 17 | spec { 18 | replicas = var.kube_deployment_replica 19 | 20 | selector { 21 | match_labels = { 22 | name = "grpc-cache" 23 | } 24 | } 25 | 26 | template { 27 | metadata { 28 | labels = { 29 | name = "grpc-cache" 30 | } 31 | namespace = var.kube_defaultspace 32 | } 33 | 34 | spec { 35 | container { 36 | name = var.image_name 37 | image = var.image_repository 38 | port { 39 | container_port = 5001 40 | name = "grpc" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | # create service 49 | 50 | resource "kubernetes_service" "grpc-cache" { 51 | metadata { 52 | name = "grpc-cache" 53 | namespace = var.kube_defaultspace 54 | } 55 | 56 | spec { 57 | selector = { 58 | name = "grpc-cache" 59 | } 60 | 61 | port { 62 | port = 5001 63 | target_port = 5001 64 | name = "grpc" 65 | } 66 | } 67 | } 68 | 69 | # create ingress 70 | 71 | resource "kubernetes_ingress" "grpc-cache" { 72 | metadata { 73 | name = "grpc-cache" 74 | namespace = var.kube_defaultspace 75 | annotations = { 76 | "kubernetes.io/ingress.class" = "nginx" 77 | "nginx.ingress.kubernetes.io/backend-protocol" = "GRPC" 78 | "nginx.ingress.kubernetes.io/ssl-redirect" = "true" 79 | } 80 | } 81 | 82 | wait_for_load_balancer = true 83 | 84 | spec { 85 | rule { 86 | host = "grpc-cache.example.com" 87 | http { 88 | path { 89 | backend { 90 | service_name = "grpc-cache" 91 | service_port = 5001 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /api/server/cache_service.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Item items value of key and expiration 10 | type Item struct { 11 | Object interface{} 12 | Expiration int64 13 | } 14 | 15 | // Cache is a struct in which the cache's methods synchronize access to this map, so it is not 16 | // recommended to keep any references to the map around after creating a cache. 17 | type Cache struct { 18 | *cache 19 | } 20 | 21 | type worker struct { 22 | Interval time.Duration 23 | stop chan bool 24 | } 25 | 26 | type cache struct { 27 | defaultExpiration time.Duration 28 | mu sync.RWMutex 29 | items map[interface{}]interface{} 30 | worker *worker 31 | } 32 | 33 | // NewCacheService is used to initialize a new cache. 34 | // Return a new cache with a given default expiration duration and cleanup 35 | // interval. If the expiration duration is less than one, 36 | // the items in the cache never expire (by default), and must be deleted 37 | // manually. If the cleanup interval is less than one, expired items are not 38 | // deleted from the cache before calling c.deleteExpired(). 39 | func NewCacheService(defaultExpiration, cleanupInterval time.Duration) *Cache { 40 | items := make(map[interface{}]interface{}) 41 | return newCacheWithWorker(defaultExpiration, cleanupInterval, items) 42 | } 43 | 44 | // This ensures Worker goroutine (which granted it 45 | // was enabled is running deleteExpired on c forever) does not keep 46 | // the returned C object from being garbage collected. When it is 47 | // garbage collected, the finalizer stops the Worker goroutine, after 48 | // which c can be collected. 49 | func newCacheWithWorker(defaultExpiration time.Duration, cleanupInterval time.Duration, items map[interface{}]interface{}) *Cache { 50 | c := newCache(defaultExpiration, items) 51 | C := &Cache{c} 52 | if cleanupInterval > 0 { 53 | runWorker(c, cleanupInterval) 54 | runtime.SetFinalizer(C, stopWorker) 55 | } 56 | return C 57 | } 58 | 59 | func newCache(defaultExpiration time.Duration, items map[interface{}]interface{}) *cache { 60 | c := &cache{ 61 | defaultExpiration: defaultExpiration, 62 | items: items, 63 | } 64 | return c 65 | } 66 | 67 | // This stops the Worker 68 | func stopWorker(c *Cache) { 69 | c.worker.stop <- true 70 | } 71 | 72 | func runWorker(c *cache, cleanupInterval time.Duration) { 73 | w := &worker{ 74 | Interval: cleanupInterval, 75 | stop: make(chan bool), 76 | } 77 | c.worker = w 78 | // This starts the new ticker and checks for expiration keys in cache 79 | go w.Run(c) 80 | } 81 | 82 | // Run deletes keys that get expired 83 | func (w *worker) Run(c *cache) { 84 | ticker := time.NewTicker(w.Interval) 85 | for { 86 | select { 87 | case <-ticker.C: 88 | // This means key has expired 89 | c.deleteExpired() 90 | case <-w.stop: 91 | ticker.Stop() 92 | return 93 | } 94 | } 95 | } 96 | 97 | // delete is used to delete key from the cache 98 | func (c *cache) delete(k interface{}) (interface{}, bool) { 99 | delete(c.items, k) 100 | return nil, false 101 | } 102 | 103 | // deleteExpired is used to delete items from the cache when key time is expired 104 | func (c *cache) deleteExpired() { 105 | now := time.Now().UnixNano() 106 | c.mu.Lock() 107 | for k, v := range c.items { 108 | if v.(Item).Expiration > 0 && now > v.(Item).Expiration { 109 | c.delete(k) 110 | } 111 | } 112 | 113 | c.mu.Unlock() 114 | } 115 | -------------------------------------------------------------------------------- /examples/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "strconv" 9 | 10 | "google.golang.org/grpc" 11 | 12 | "github.com/golang/protobuf/ptypes/empty" 13 | api "github.com/knrt10/grpc-cache/proto" 14 | ) 15 | 16 | var ( 17 | address string 18 | conn *grpc.ClientConn 19 | err error 20 | ) 21 | 22 | func main() { 23 | 24 | // Get address from flag 25 | flag.StringVar(&address, "addr", "127.0.0.1:5001", "Address on which you want to run server") 26 | flag.Parse() 27 | conn, err = grpc.Dial(address, grpc.WithInsecure()) 28 | if err != nil { 29 | log.Fatalf("did not connect: %s", err) 30 | } 31 | defer conn.Close() 32 | 33 | c := api.NewCacheServiceClient(conn) 34 | 35 | // Add key 36 | keyVal1 := &api.Item{ 37 | Key: "22", 38 | Value: "knrt10", 39 | Expiration: "-1m", 40 | } 41 | 42 | keyVal2 := &api.Item{ 43 | Key: "distributed", 44 | Value: "systems", 45 | Expiration: "20s", 46 | } 47 | 48 | keyVal3 := &api.Item{ 49 | Key: "24", 50 | Value: "Palash", 51 | Expiration: "2min10s", 52 | } 53 | 54 | keyVal4 := &api.Item{ 55 | Key: "prefixTest", 56 | Value: "val1", 57 | Expiration: "10s", 58 | } 59 | 60 | keyVal5 := &api.Item{ 61 | Key: "prefixTest1", 62 | Value: "val2", 63 | Expiration: "10s", 64 | } 65 | 66 | keyVal6 := &api.Item{ 67 | Key: "prefixTest2", 68 | Value: "val3", 69 | Expiration: "10s", 70 | } 71 | 72 | c.Add(context.Background(), keyVal1) 73 | c.Add(context.Background(), keyVal2) 74 | c.Add(context.Background(), keyVal4) 75 | c.Add(context.Background(), keyVal5) 76 | c.Add(context.Background(), keyVal6) 77 | 78 | addKeyRes, err := c.Add(context.Background(), keyVal3) 79 | if err != nil { 80 | log.Fatalf("Error when calling Add: %s", err) 81 | } 82 | fmt.Println("Response from server for adding a key", addKeyRes) 83 | 84 | // Checking for race condition 85 | for i := 0; i < 50; i++ { 86 | go c.Add(context.Background(), &api.Item{ 87 | Key: strconv.Itoa(i), 88 | Value: "Value of i is ", 89 | Expiration: strconv.Itoa(i), 90 | }) 91 | } 92 | 93 | // Get key 94 | keyGet := &api.GetKey{ 95 | Key: "distributed", 96 | } 97 | 98 | getKeyRes, err := c.Get(context.Background(), keyGet) 99 | if err != nil { 100 | log.Fatalf("Error when calling Get: %s", err) 101 | } 102 | fmt.Println("Response from server for getting a key", getKeyRes) 103 | 104 | // Get keys by prefix 105 | keyGetPrefix := &api.GetKey{ 106 | Key: "prefixTest", 107 | } 108 | 109 | getKeyPrefixRes, err := c.GetByPrefix(context.Background(), keyGetPrefix) 110 | if err != nil { 111 | log.Fatalf("Error when calling Get: %s", err) 112 | } 113 | fmt.Println("Response from server for getting a keys by prefix", getKeyPrefixRes) 114 | 115 | // GetAllItems 116 | getAllKeysRes, err := c.GetAllItems(context.Background(), &empty.Empty{}) 117 | if err != nil { 118 | log.Fatalf("Error when calling GetAllItems: %s", err) 119 | } 120 | fmt.Println("Response from server for getting all keys", getAllKeysRes) 121 | 122 | // Delete Key 123 | deleteKeyRes, err := c.DeleteKey(context.Background(), keyGet) 124 | if err != nil { 125 | log.Fatalf("Error when calling DeleteKey: %s", err) 126 | } 127 | fmt.Println("Response from server after deleting a key", deleteKeyRes) 128 | 129 | // DeleteAll key 130 | deleteAllKeysResp, err := c.DeleteAll(context.Background(), &empty.Empty{}) 131 | if err != nil { 132 | log.Fatalf("Error when calling DeleteAll: %s", err) 133 | } 134 | fmt.Println("Response from server after deleting all keys", deleteAllKeysResp) 135 | 136 | // GetAllItems after deleting key 137 | _, err = c.GetAllItems(context.Background(), &empty.Empty{}) 138 | if err != nil { 139 | fmt.Println("Response from server after no key found", err.Error()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /api/server/api.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/golang/protobuf/ptypes/empty" 9 | api "github.com/knrt10/grpc-cache/proto" 10 | ) 11 | 12 | // Add is used to add key/value pair to the cache. 13 | func (c *cache) Add(ctx context.Context, item *api.Item) (*api.Item, error) { 14 | var expiration int64 15 | duration, _ := time.ParseDuration(item.Expiration) 16 | // Meaning d is of form "2m30s" 17 | if duration > 0 { 18 | expiration = time.Now().Add(duration).UnixNano() 19 | } 20 | c.mu.Lock() 21 | c.items[item.Key] = Item{ 22 | Object: item.Value, 23 | Expiration: expiration, 24 | } 25 | c.mu.Unlock() 26 | return item, nil 27 | } 28 | 29 | // Get method is used to key/value pair while providing key as args 30 | func (c *cache) Get(ctx context.Context, args *api.GetKey) (*api.Item, error) { 31 | key := args.Key 32 | // Locking so that other goroutines cannot access this at the same time 33 | c.mu.RLock() 34 | value, exists := c.items[key] 35 | // No key found 36 | if !exists { 37 | c.mu.RUnlock() 38 | return nil, ErrNoKey 39 | } 40 | 41 | // This means key has some expiration 42 | if value.(Item).Expiration > 0 { 43 | if time.Now().UnixNano() > value.(Item).Expiration { 44 | c.mu.RUnlock() 45 | return nil, ErrKeyExpired 46 | } 47 | } 48 | c.mu.RUnlock() 49 | return &api.Item{ 50 | Key: key, 51 | Value: value.(Item).Object.(string), 52 | Expiration: time.Unix(0, value.(Item).Expiration).String(), 53 | }, nil 54 | } 55 | 56 | // GetByPrefix method is all keys that match the prefix while providing key as args 57 | func (c *cache) GetByPrefix(ctx context.Context, args *api.GetKey) (*api.AllItems, error) { 58 | key := args.Key 59 | c.mu.RLock() 60 | defer c.mu.RUnlock() 61 | var items []*api.Item 62 | now := time.Now().UnixNano() 63 | for k, v := range c.items { 64 | if v.(Item).Expiration > 0 { 65 | if now > v.(Item).Expiration { 66 | continue 67 | } 68 | } 69 | 70 | if strings.Contains(k.(string), key) { 71 | items = append(items, &api.Item{ 72 | Key: k.(string), 73 | Value: v.(Item).Object.(string), 74 | Expiration: time.Unix(0, v.(Item).Expiration).String(), 75 | }) 76 | } 77 | } 78 | // This means no keys were found, or all were expired 79 | if len(items) < 1 { 80 | return nil, ErrNoKey 81 | } 82 | 83 | return &api.AllItems{ 84 | Items: items, 85 | }, nil 86 | } 87 | 88 | // GetAllItems method get all unexpired keys from the cache 89 | func (c *cache) GetAllItems(ctx context.Context, in *empty.Empty) (*api.AllItems, error) { 90 | c.mu.RLock() 91 | defer c.mu.RUnlock() 92 | var items []*api.Item 93 | now := time.Now().UnixNano() 94 | for k, v := range c.items { 95 | if v.(Item).Expiration > 0 { 96 | if now > v.(Item).Expiration { 97 | continue 98 | } 99 | } 100 | 101 | items = append(items, &api.Item{ 102 | Key: k.(string), 103 | Value: v.(Item).Object.(string), 104 | Expiration: time.Unix(0, v.(Item).Expiration).String(), 105 | }) 106 | } 107 | 108 | // This means no keys were found, or all were expired 109 | if len(items) < 1 { 110 | return nil, ErrNoKey 111 | } 112 | 113 | return &api.AllItems{ 114 | Items: items, 115 | }, nil 116 | } 117 | 118 | // DeleteKey deletes an item from the cache. Does nothing if the key is not in the cache. 119 | func (c *cache) DeleteKey(ctx context.Context, args *api.GetKey) (*api.Success, error) { 120 | c.mu.Lock() 121 | c.delete(args.Key) 122 | c.mu.Unlock() 123 | return &api.Success{ 124 | Success: true, 125 | }, nil 126 | } 127 | 128 | // Delete all items from the cache. 129 | func (c *cache) DeleteAll(ctx context.Context, in *empty.Empty) (*api.Success, error) { 130 | c.mu.Lock() 131 | c.items = map[interface{}]interface{}{} 132 | c.mu.Unlock() 133 | return &api.Success{ 134 | Success: true, 135 | }, nil 136 | } 137 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 6 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 7 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 8 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 10 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 14 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 15 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 16 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 17 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 18 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 19 | github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= 20 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 21 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 22 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 24 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 25 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 27 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 28 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 29 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 30 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 31 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 32 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 33 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 35 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 36 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 37 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 38 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 40 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 52 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 53 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 54 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 55 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 57 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 58 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 59 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 60 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 61 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 62 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 63 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 64 | google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= 65 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 66 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 67 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 68 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 69 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 70 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 71 | google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= 72 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 73 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 74 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 75 | -------------------------------------------------------------------------------- /api/server/api_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/test/bufconn" 13 | 14 | "github.com/golang/protobuf/ptypes/empty" 15 | apis "github.com/knrt10/grpc-cache/proto" 16 | ) 17 | 18 | const ( 19 | bufSize = 1024 * 1024 20 | expire = 10 21 | cleanup = 1 22 | ) 23 | 24 | var lis *bufconn.Listener 25 | 26 | func init() { 27 | lis = bufconn.Listen(bufSize) 28 | s := grpc.NewServer() 29 | apis.RegisterCacheServiceServer(s, NewCacheService(time.Duration(expire)*time.Minute, time.Duration(cleanup)*time.Second)) 30 | go func() { 31 | if err := s.Serve(lis); err != nil { 32 | log.Fatalf("Server exited with error: %v", err) 33 | } 34 | }() 35 | } 36 | 37 | func bufDialer(context.Context, string) (net.Conn, error) { 38 | return lis.Dial() 39 | } 40 | 41 | func TestAdd(t *testing.T) { 42 | ctx := context.Background() 43 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 44 | if err != nil { 45 | t.Fatalf("Failed to dial bufnet: %v", err) 46 | } 47 | defer conn.Close() 48 | c := apis.NewCacheServiceClient(conn) 49 | keyVal1 := &apis.Item{ 50 | Key: "kautilya", 51 | Value: "knrt10", 52 | Expiration: "1m", 53 | } 54 | 55 | keyVal2 := &apis.Item{ 56 | Key: "24", 57 | Value: "palash", 58 | Expiration: "1m", 59 | } 60 | 61 | keyVal3 := &apis.Item{ 62 | Key: "foo", 63 | Value: "bar", 64 | Expiration: "1m", 65 | } 66 | 67 | keyVal4 := &apis.Item{ 68 | Key: "temp", 69 | Value: "bar", 70 | Expiration: "1µs", 71 | } 72 | 73 | c.Add(context.Background(), keyVal2) 74 | c.Add(context.Background(), keyVal3) 75 | c.Add(context.Background(), keyVal4) 76 | 77 | resp, err := c.Add(context.Background(), keyVal1) 78 | if err != nil { 79 | t.Fatalf("Adding key Failed: %v", err) 80 | } 81 | if resp.Key != "kautilya" { 82 | t.Errorf("handler returned unexpected body: got %v want %v", 83 | resp.Key, "kautilya") 84 | } 85 | if resp.Value != "knrt10" { 86 | t.Errorf("handler returned unexpected body: got %v want %v", 87 | resp.Key, "knrt10") 88 | } 89 | 90 | // Save keys 91 | // Checking for race condition 92 | for i := 0; i < 100; i++ { 93 | go c.Add(context.Background(), &apis.Item{ 94 | Key: strconv.Itoa(i), 95 | Value: "Value of i is ", 96 | Expiration: strconv.Itoa(i), 97 | }) 98 | } 99 | 100 | } 101 | 102 | func TestGet(t *testing.T) { 103 | ctx := context.Background() 104 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 105 | if err != nil { 106 | t.Fatalf("Failed to dial bufnet: %v", err) 107 | } 108 | defer conn.Close() 109 | c := apis.NewCacheServiceClient(conn) 110 | 111 | keyGet := &apis.GetKey{ 112 | Key: "kautilya", 113 | } 114 | resp, err := c.Get(context.Background(), keyGet) 115 | if err != nil { 116 | t.Fatalf("Getting key Failed: %v", err) 117 | } 118 | if resp.Key != "kautilya" { 119 | t.Errorf("handler returned unexpected body: got %v want %v", 120 | resp.Key, "kautilya") 121 | } 122 | if resp.Value != "knrt10" { 123 | t.Errorf("handler returned unexpected body: got %v want %v", 124 | resp.Key, "knrt10") 125 | } 126 | } 127 | 128 | func TestGetByPrefix(t *testing.T) { 129 | ctx := context.Background() 130 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 131 | if err != nil { 132 | t.Fatalf("Failed to dial bufnet: %v", err) 133 | } 134 | defer conn.Close() 135 | c := apis.NewCacheServiceClient(conn) 136 | 137 | keyVal1 := &apis.Item{ 138 | Key: "prefixTest", 139 | Value: "val1", 140 | Expiration: "10s", 141 | } 142 | 143 | keyVal2 := &apis.Item{ 144 | Key: "prefixTest1", 145 | Value: "val2", 146 | Expiration: "10s", 147 | } 148 | 149 | keyVal3 := &apis.Item{ 150 | Key: "prefixTest2", 151 | Value: "val3", 152 | Expiration: "10s", 153 | } 154 | 155 | c.Add(context.Background(), keyVal1) 156 | c.Add(context.Background(), keyVal2) 157 | c.Add(context.Background(), keyVal3) 158 | 159 | keyWrongPrefix := &apis.GetKey{ 160 | Key: "wrongPrefix", 161 | } 162 | _, err = c.GetByPrefix(context.Background(), keyWrongPrefix) 163 | if err.Error() != "rpc error: code = Unknown desc = No key found" { 164 | t.Errorf("No key found") 165 | } 166 | 167 | keyRightPrefix := &apis.GetKey{ 168 | Key: "prefixTest", 169 | } 170 | 171 | resp, err := c.GetByPrefix(context.Background(), keyRightPrefix) 172 | if err != nil { 173 | t.Fatalf("Getting key by prefix Failed: %v", err) 174 | } 175 | if len(resp.Items) != 3 { 176 | t.Errorf("handler returned unexpected body: got %v want %v", 177 | len(resp.Items), 3) 178 | } 179 | } 180 | 181 | func TestGetAllItems(t *testing.T) { 182 | ctx := context.Background() 183 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 184 | if err != nil { 185 | t.Fatalf("Failed to dial bufnet: %v", err) 186 | } 187 | defer conn.Close() 188 | c := apis.NewCacheServiceClient(conn) 189 | 190 | _, err = c.GetAllItems(context.Background(), &empty.Empty{}) 191 | if err != nil { 192 | t.Fatalf("Getting all keys Failed: %v", err) 193 | } 194 | } 195 | 196 | func TestDeleteKey(t *testing.T) { 197 | ctx := context.Background() 198 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 199 | if err != nil { 200 | t.Fatalf("Failed to dial bufnet: %v", err) 201 | } 202 | defer conn.Close() 203 | c := apis.NewCacheServiceClient(conn) 204 | 205 | keyGet := &apis.GetKey{ 206 | Key: "22", 207 | } 208 | resp, err := c.DeleteKey(context.Background(), keyGet) 209 | if err != nil { 210 | t.Fatalf("Deleting key Failed: %v", err) 211 | } 212 | if resp.Success != true { 213 | t.Errorf("handler returned unexpected body: got %v want %v", 214 | resp.Success, true) 215 | } 216 | } 217 | 218 | func TestDeleteAll(t *testing.T) { 219 | ctx := context.Background() 220 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 221 | if err != nil { 222 | t.Fatalf("Failed to dial bufnet: %v", err) 223 | } 224 | defer conn.Close() 225 | c := apis.NewCacheServiceClient(conn) 226 | 227 | resp, err := c.DeleteAll(context.Background(), &empty.Empty{}) 228 | if err != nil { 229 | t.Fatalf("Deleting key Failed: %v", err) 230 | } 231 | if resp.Success != true { 232 | t.Errorf("handler returned unexpected body: got %v want %v", 233 | resp.Success, true) 234 | } 235 | } 236 | 237 | // Testing deleted Key 238 | func TestGetDeletedKey(t *testing.T) { 239 | ctx := context.Background() 240 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 241 | if err != nil { 242 | t.Fatalf("Failed to dial bufnet: %v", err) 243 | } 244 | defer conn.Close() 245 | c := apis.NewCacheServiceClient(conn) 246 | 247 | // Geting expired key 248 | keyGet := &apis.GetKey{ 249 | Key: "temp", 250 | } 251 | _, err = c.Get(context.Background(), keyGet) 252 | if err.Error() != "rpc error: code = Unknown desc = No key found" { 253 | t.Errorf("Key not deleted") 254 | } 255 | } 256 | 257 | func TestDeleteKeyByExpiration(t *testing.T) { 258 | ctx := context.Background() 259 | conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) 260 | if err != nil { 261 | t.Fatalf("Failed to dial bufnet: %v", err) 262 | } 263 | defer conn.Close() 264 | c := apis.NewCacheServiceClient(conn) 265 | keyVal1 := &apis.Item{ 266 | Key: "expired", 267 | Value: "knrt10", 268 | Expiration: "1s", 269 | } 270 | 271 | resp, err := c.Add(context.Background(), keyVal1) 272 | if err != nil { 273 | t.Fatalf("Adding key Failed: %v", err) 274 | } 275 | if resp.Key != "expired" { 276 | t.Errorf("handler returned unexpected body: got %v want %v", 277 | resp.Key, "expired") 278 | } 279 | if resp.Value != "knrt10" { 280 | t.Errorf("handler returned unexpected body: got %v want %v", 281 | resp.Key, "knrt10") 282 | } 283 | 284 | time.Sleep(2 * time.Second) 285 | 286 | keyGet := &apis.GetKey{ 287 | Key: "expired", 288 | } 289 | _, err = c.Get(context.Background(), keyGet) 290 | if err.Error() != "rpc error: code = Unknown desc = No key found" { 291 | t.Errorf("Key not deleted") 292 | } 293 | 294 | } 295 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |