├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── client └── main.go ├── dataservice └── main.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── init.cql └── messages ├── messages.pb.go ├── messages.proto └── messages_grpc.pb.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start with the official Golang image 2 | FROM golang:1.23.1 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy go mod and sum files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed 11 | RUN go mod download 12 | 13 | # Copy the source from the current directory to the Working Directory inside the container 14 | COPY . . 15 | 16 | # Build the Go app 17 | RUN go build -o dataservice ./dataservice 18 | 19 | # Command to run the executable 20 | ENTRYPOINT ["./dataservice/dataservice"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Amr Elmohamady 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Services 2 | 3 | A high-performance, distributed data access layer implementing request coalescing and hash-based routing to reduce database load and prevent hot partitions. 4 | 5 | ## Overview 6 | 7 | Data Services is a middleware layer that sits between API servers and Cassandra clusters, providing request coalescing. It's designed to handle high-traffic scenarios efficiently by reducing duplicate database queries and preventing database overload. 8 | It is inspired by Discord's architecture explained in their blog post: [HOW DISCORD STORES TRILLIONS OF MESSAGES](https://discord.com/blog/how-discord-stores-trillions-of-messages). 9 | 10 | An example of usecase from Discord is when a big announcement is sent on a large server (Discord group) that notifies @everyone: users are going to open the app and read the same message, sending tons of traffic to the database. This is where request coalescing comes in handy, as it can combine all the requests for the same data into a single database query, reducing the load on the database and preventing hot partitions. 11 | 12 | A simpler way of understanding it is: caching with the duration equal to the time spent running the query. No client has to be aware of the coalescing because the max amount of staleness is the same as if each client had run the query themselves. It also doesn't require extra memory, because the query result falls out of scope as soon as it is sent to all waiters. 13 | 14 | ### Key Features 15 | 16 | - **Request Coalescing**: Automatically combines duplicate requests for the same data into a single database query 17 | - **Consistent Hash-based Routing**: Routes related requests to the same service instance for optimal coalescing 18 | - **Distributed Architecture**: Multiple service instances working in parallel 19 | - **High Availability**: Data service nodes are stateless and can be scaled horizontally 20 | - **Monitoring**: Built-in metrics for tracking requests and queries counts 21 | 22 | ## Setup 23 | 24 | ```bash 25 | $ docker-compose up --build 26 | ``` 27 | Wait for the services to start up. The Cassandra cluster will be initialized with the required keyspace. You will see something like this in the logs that shows that the data service instances are ready to accept requests: 28 | ``` 29 | data-service1-1 | 2024/10/26 16:18:45 Connected to cassandra 30 | data-service1-1 | 2024/10/26 16:18:45 Starting server on port 50051 31 | data-service2-1 | 2024/10/26 16:18:45 Connected to cassandra 32 | data-service2-1 | 2024/10/26 16:18:45 Starting server on port 50052 33 | ``` 34 | 35 | Run the client CLI to send test requests to the data service: 36 | ```bash 37 | $ go run ./client -h 38 | -channels int 39 | Number of unique channels to distribute requests across (number of unique requests) (default 20) 40 | -requests int 41 | Total number of requests to send (default 10000) 42 | ``` 43 | 44 | 45 | Example usage: 46 | ``` 47 | $ go run ./client 48 | 2024/10/26 17:08:11 Unique requests: 20, Total requests: 10000, Total queries executed: 184 49 | 2024/10/26 17:08:11 Average queries per request: 0.0184 50 | 2024/10/26 17:08:11 Saved queries by coalescing: 9816 51 | 2024/10/26 17:08:11 Total time taken: 816.727364ms 52 | ``` 53 | 54 | ## Architecture 55 | 56 | ### Components 57 | 58 | 1. **Data Service Nodes**: gRPC servers that handle incoming requests and manage database connections 59 | 2. **Cassandra Cluster**: A 3-node Cassandra cluster for data storage 60 | 3. **Client**: gRPC Test client for simulating high-traffic scenarios 61 | 62 | ```mermaid 63 | %%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '16px', 'fontFamily': 'arial', 'nodeTextSize': '16px', 'labelTextSize': '16px', 'titleTextSize': '20px' }}}%% 64 | 65 | flowchart TB 66 | subgraph Users ["Multiple Users"] 67 | U1[User 1] 68 | U2[User 2] 69 | U3[User 3] 70 | end 71 | 72 | subgraph API ["Hash-based Routing API"] 73 | A1[API Server 1] 74 | A2[API Server 2] 75 | end 76 | 77 | subgraph DS ["Data Services Layer"] 78 | direction TB 79 | subgraph DS1 ["Data Service Instance 1"] 80 | direction TB 81 | subgraph Coalescing1 ["Request Coalescing"] 82 | R1["Request (Channel 1)"] 83 | R2["Request (Channel 1)"] 84 | R3["Request (Channel 1)"] 85 | RC1[Request Coalescer] 86 | DQ1["Single DB Query"] 87 | R1 & R2 & R3 --> RC1 88 | RC1 --> DQ1 89 | end 90 | end 91 | subgraph DS2 ["Data Service Instance 2"] 92 | direction TB 93 | subgraph Coalescing2 ["Request Coalescing"] 94 | R4["Request (Channel 2)"] 95 | R5["Request (Channel 2)"] 96 | R6["Request (Channel 2)"] 97 | RC2[Request Coalescer] 98 | DQ2["Single DB Query"] 99 | R4 & R5 & R6 --> RC2 100 | RC2 --> DQ2 101 | end 102 | end 103 | end 104 | 105 | subgraph DB ["Cassandra Cluster"] 106 | C1[Node 1] <--> C2[Node 2] <--> C3[Node 3] <--> C1 107 | end 108 | 109 | %% Connect users to API 110 | U1 --> A1 111 | U2 --> API 112 | U3 --> A2 113 | 114 | %% Connect API to Data Services 115 | A1 --> DS1 116 | A1 --> DS2 117 | A2 --> DS1 118 | A2 --> DS2 119 | 120 | %% Connect Data Services to Cassandra 121 | DS1 --> DB 122 | DS2 --> DB 123 | 124 | classDef users fill:#B3E5FC,stroke:#0277BD,color:#000000,font-size:16px 125 | classDef api fill:#FFB74D,stroke:#E65100,color:#000000,font-size:16px 126 | classDef dataservice fill:#CE93D8,stroke:#6A1B9A,color:#000000,font-size:16px 127 | classDef cassandra fill:#81C784,stroke:#2E7D32,color:#000000,font-size:16px 128 | classDef component fill:#E0E0E0,stroke:#424242,color:#000000,font-size:16px 129 | 130 | class U1,U2,U3 users 131 | class A1,A2 api 132 | class DS1,DS2,R1,R2,R3,R4,R5,R6,RC1,RC2,DQ1,DQ2 dataservice 133 | class C1,C2,C3 cassandra 134 | ``` 135 | 136 | ## Technical Learnings 137 | 138 | ### Go Concurrency Patterns 139 | 1. **Channels** 140 | - Used for async communication between goroutines 141 | - Each request is in its own goroutine with a channel for response from the query executer goroutine 142 | 143 | 2. **Mutex Operations** 144 | - Implemented thread-safe access to shared resources 145 | 146 | 3. **Atomic Operations** 147 | - Used lock-free atomic counters for metrics tracking 148 | 149 | 4. **WaitGroups** 150 | - Used for waiting on multiple goroutines to complete in the CLI client 151 | 152 | 5. **Context Management** 153 | - Used context for request timeouts and cancellation 154 | 155 | 156 | ### gRPC Implementation 157 | - Defined service interfaces using Protocol Buffers 158 | - Managed timeout handling using context 159 | 160 | ### Docker and Container Orchestration 161 | - Implemented health checks for service readiness 162 | - Managed container dependencies and startup order 163 | - Configured networking between services 164 | - Implemented volume management for data persistence 165 | 166 | ## Future Improvements 167 | 168 | 1. **Monitoring & Observability** 169 | - Add distributed tracing 170 | 171 | 2. **Scalability** 172 | - Implement dynamic service discovery (e.g. Consul or etcd) 173 | 174 | 3. **Resilience** 175 | - Add circuit breakers 176 | - Implement retry policies 177 | - Add rate limiting 178 | 179 | ## License 180 | 181 | This project is licensed under the MIT License. 182 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | pb "github.com/amrdb/data-services/messages" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | ) 15 | 16 | func sendRequest(client pb.MessagesServiceClient, channelId int64, wg *sync.WaitGroup) { 17 | defer wg.Done() 18 | 19 | message := &pb.MessageRequest{ 20 | ChannelId: channelId, 21 | MessageId: channelId * 1000, 22 | } 23 | 24 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 25 | defer cancel() 26 | 27 | _, err := client.GetMessage(ctx, message) 28 | if err != nil { 29 | log.Printf("Failed to get message for channel %d: %v", channelId, err) 30 | return 31 | } 32 | } 33 | 34 | func main() { 35 | requests := flag.Int("requests", 10000, "Total number of requests to send") 36 | numChannels := flag.Int("channels", 20, "Number of unique channels to distribute requests across (number of unique requests)") 37 | flag.Parse() 38 | if (*requests <= 0) || (*numChannels <= 0) { 39 | log.Fatalf("Invalid requests or channels, must be greater than 0") 40 | } 41 | if *numChannels > 20 { 42 | log.Fatalf("Number of channels must be less than or equal to 20 because there are only 20 unique channels in cassandra") 43 | } 44 | if *numChannels > *requests { 45 | log.Fatalf("Number of channels must be less than or equal to number of requests") 46 | } 47 | 48 | dataServices := []string{"localhost:50051", "localhost:50052"} 49 | var clients []pb.MessagesServiceClient 50 | 51 | for _, ds := range dataServices { 52 | conn, err := grpc.NewClient(ds, grpc.WithTransportCredentials(insecure.NewCredentials())) 53 | if err != nil { 54 | log.Fatalf("could not connect to %s: %v", ds, err) 55 | } 56 | defer conn.Close() 57 | clients = append(clients, pb.NewMessagesServiceClient(conn)) 58 | } 59 | 60 | var wg sync.WaitGroup 61 | wg.Add(*requests) 62 | 63 | start := time.Now() 64 | 65 | for i := 0; i < *requests; i++ { 66 | channelId := int64((i % *numChannels) + 1) // Add 1 to start channels from 1 67 | client := clients[int(channelId-1)%len(clients)] // Distribute requests across data services (-1 for 0-based index) 68 | go sendRequest(client, channelId, &wg) 69 | } 70 | 71 | wg.Wait() 72 | elapsed := time.Since(start) 73 | 74 | var metrics []*pb.MetricsReply 75 | for _, client := range clients { 76 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 77 | defer cancel() 78 | 79 | metricsReply, err := client.GetAndResetMetrics(ctx, &pb.Empty{}) 80 | if err != nil { 81 | log.Fatalf("Failed to get metrics: %v", err) 82 | } 83 | metrics = append(metrics, metricsReply) 84 | } 85 | 86 | totalRequests := 0 87 | queriesExecuted := 0 88 | for _, m := range metrics { 89 | totalRequests += int(m.TotalRequests) 90 | queriesExecuted += int(m.QueriesExecuted) 91 | } 92 | avgQueries := strconv.FormatFloat(float64(queriesExecuted)/float64(totalRequests), 'f', -1, 64) 93 | 94 | log.Printf("Unique requests: %d, Total requests: %d, Total queries executed: %d", *numChannels, totalRequests, queriesExecuted) 95 | log.Printf("Average queries per request: %s", avgQueries) 96 | log.Printf("Saved queries by coalescing: %d", totalRequests-queriesExecuted) 97 | log.Printf("Total time taken: %v", elapsed) 98 | } 99 | -------------------------------------------------------------------------------- /dataservice/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "os" 8 | "sync" 9 | "sync/atomic" 10 | 11 | pb "github.com/amrdb/data-services/messages" 12 | "github.com/gocql/gocql" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | var session *gocql.Session 17 | var requestsMap *RequestsMap 18 | 19 | type RequestId struct { 20 | channelId int64 // not go channel, but a channel for chat messages 21 | messageId int64 22 | } 23 | 24 | type Metrics struct { 25 | totalRequests atomic.Int64 26 | queriesExecuted atomic.Int64 27 | } 28 | 29 | // map of request ids to array of channels 30 | type RequestsMap struct { 31 | mu sync.Mutex 32 | // channel returns pointer to avoid a lot of copying and it won't be modified by the caller 33 | requests map[RequestId][]chan *pb.MessageReply 34 | metrics Metrics // atomic counters (not guarded by mutex) 35 | } 36 | 37 | func NewRequestsMap() *RequestsMap { 38 | return &RequestsMap{ 39 | requests: make(map[RequestId][]chan *pb.MessageReply), 40 | metrics: Metrics{}, 41 | } 42 | } 43 | 44 | func (rm *RequestsMap) HandleRequest(requestId RequestId) *pb.MessageReply { 45 | rm.metrics.totalRequests.Add(1) 46 | resultChan := make(chan *pb.MessageReply) 47 | rm.mu.Lock() 48 | 49 | channels, ok := rm.requests[requestId] 50 | if !ok { 51 | channels = make([]chan *pb.MessageReply, 0) 52 | channels = append(channels, resultChan) 53 | rm.requests[requestId] = channels 54 | rm.mu.Unlock() 55 | 56 | rm.metrics.queriesExecuted.Add(1) 57 | go rm.ExecuteQuery(requestId) 58 | } else { 59 | channels = append(channels, resultChan) 60 | rm.requests[requestId] = channels 61 | rm.mu.Unlock() 62 | } 63 | 64 | return <-resultChan 65 | } 66 | 67 | // execute the cassandra query 68 | // send the result to all the channels in the map 69 | // remove the request from the map 70 | func (rm *RequestsMap) ExecuteQuery(requestId RequestId) { 71 | var MessageReply pb.MessageReply 72 | 73 | err := session.Query("SELECT * FROM messages WHERE channel_id = ? AND message_id = ?", requestId.channelId, requestId.messageId). 74 | Scan(&MessageReply.ChannelId, &MessageReply.MessageId, &MessageReply.AuthorId, &MessageReply.Content) 75 | if err != nil { 76 | log.Printf("failed to execute query: %v", err) 77 | } 78 | 79 | rm.mu.Lock() 80 | channels := rm.requests[requestId] 81 | delete(rm.requests, requestId) 82 | rm.mu.Unlock() 83 | 84 | for _, ch := range channels { 85 | ch <- &MessageReply 86 | } 87 | } 88 | 89 | type MessageService struct { 90 | pb.UnimplementedMessagesServiceServer 91 | } 92 | 93 | func (s *MessageService) GetMessage(ctx context.Context, in *pb.MessageRequest) (*pb.MessageReply, error) { 94 | requestId := RequestId{channelId: in.ChannelId, messageId: in.MessageId} 95 | return requestsMap.HandleRequest(requestId), nil 96 | } 97 | 98 | func (s *MessageService) GetAndResetMetrics(ctx context.Context, in *pb.Empty) (*pb.MetricsReply, error) { 99 | metrics := &pb.MetricsReply{ 100 | TotalRequests: requestsMap.metrics.totalRequests.Load(), 101 | QueriesExecuted: requestsMap.metrics.queriesExecuted.Load(), 102 | } 103 | requestsMap.metrics.queriesExecuted.Store(0) 104 | requestsMap.metrics.totalRequests.Store(0) 105 | 106 | return metrics, nil 107 | } 108 | 109 | func main() { 110 | var err error 111 | cluster := gocql.NewCluster("cassandra-node1") 112 | cluster.Keyspace = "dataservices" 113 | session, err = cluster.CreateSession() 114 | if err != nil { 115 | log.Fatalf("failed to connect to cassandra: %v", err) 116 | } 117 | log.Printf("Connected to cassandra") 118 | defer session.Close() 119 | 120 | requestsMap = NewRequestsMap() 121 | 122 | port := os.Getenv("PORT") 123 | if port == "" { 124 | port = "50051" 125 | } 126 | lis, err := net.Listen("tcp", ":"+port) 127 | if err != nil { 128 | log.Fatalf("failed to listen: %v", err) 129 | } 130 | 131 | s := grpc.NewServer() 132 | pb.RegisterMessagesServiceServer(s, &MessageService{}) 133 | log.Printf("Starting server on port %s", port) 134 | if err := s.Serve(lis); err != nil { 135 | log.Fatalf("failed to serve: %v", err) 136 | panic(err) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | cassandra-node1: 5 | image: cassandra:latest 6 | container_name: cassandra-node1 7 | hostname: cassandra-node1 8 | restart: always 9 | environment: 10 | - CASSANDRA_BROADCAST_ADDRESS=cassandra-node1 11 | - CASSANDRA_SEEDS=cassandra-node1 12 | - CASSANDRA_CLUSTER_NAME=data_services 13 | - CASSANDRA_DC=datacenter1 14 | - CASSANDRA_RACK=rack1 15 | - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch 16 | - MAX_HEAP_SIZE=512M 17 | - HEAP_NEWSIZE=100M 18 | networks: 19 | - dataservices-network 20 | volumes: 21 | - cassandra_data1:/var/lib/cassandra 22 | healthcheck: 23 | test: ["CMD", "cqlsh", "-e", "describe keyspaces"] 24 | interval: 30s 25 | timeout: 10s 26 | retries: 10 27 | start_period: 60s 28 | 29 | cassandra-node2: 30 | image: cassandra:latest 31 | container_name: cassandra-node2 32 | hostname: cassandra-node2 33 | restart: always 34 | environment: 35 | - CASSANDRA_BROADCAST_ADDRESS=cassandra-node2 36 | - CASSANDRA_SEEDS=cassandra-node1 37 | - CASSANDRA_CLUSTER_NAME=data_services 38 | - CASSANDRA_DC=datacenter1 39 | - CASSANDRA_RACK=rack1 40 | - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch 41 | - MAX_HEAP_SIZE=512M 42 | - HEAP_NEWSIZE=100M 43 | networks: 44 | - dataservices-network 45 | depends_on: 46 | cassandra-node1: 47 | condition: service_healthy 48 | volumes: 49 | - cassandra_data2:/var/lib/cassandra 50 | healthcheck: 51 | test: ["CMD", "cqlsh", "-e", "describe keyspaces"] 52 | interval: 30s 53 | timeout: 10s 54 | retries: 10 55 | start_period: 120s 56 | 57 | cassandra-node3: 58 | image: cassandra:latest 59 | container_name: cassandra-node3 60 | hostname: cassandra-node3 61 | restart: always 62 | environment: 63 | - CASSANDRA_BROADCAST_ADDRESS=cassandra-node3 64 | - CASSANDRA_SEEDS=cassandra-node1 65 | - CASSANDRA_CLUSTER_NAME=data_services 66 | - CASSANDRA_DC=datacenter1 67 | - CASSANDRA_RACK=rack1 68 | - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch 69 | - MAX_HEAP_SIZE=512M 70 | - HEAP_NEWSIZE=100M 71 | networks: 72 | - dataservices-network 73 | depends_on: 74 | cassandra-node1: 75 | condition: service_healthy 76 | cassandra-node2: 77 | condition: service_healthy 78 | volumes: 79 | - cassandra_data3:/var/lib/cassandra 80 | healthcheck: 81 | test: ["CMD", "cqlsh", "-e", "describe keyspaces"] 82 | interval: 30s 83 | timeout: 10s 84 | retries: 10 85 | start_period: 180s 86 | 87 | cassandra-init: 88 | image: cassandra:latest 89 | depends_on: 90 | cassandra-node1: 91 | condition: service_healthy 92 | cassandra-node2: 93 | condition: service_healthy 94 | cassandra-node3: 95 | condition: service_healthy 96 | networks: 97 | - dataservices-network 98 | volumes: 99 | - ./init.cql:/init.cql 100 | command: > 101 | bash -c ' 102 | while ! cqlsh cassandra-node1 -e "describe cluster" ; do 103 | echo "Waiting for Cassandra cluster to be ready..."; 104 | sleep 10; 105 | done; 106 | echo "Initializing database..."; 107 | cqlsh cassandra-node1 -f /init.cql; 108 | ' 109 | restart: on-failure 110 | 111 | data-service1: 112 | build: 113 | context: . 114 | dockerfile: Dockerfile 115 | ports: 116 | - "50051:50051" 117 | networks: 118 | - dataservices-network 119 | depends_on: 120 | cassandra-node1: 121 | condition: service_healthy 122 | cassandra-node2: 123 | condition: service_healthy 124 | cassandra-node3: 125 | condition: service_healthy 126 | cassandra-init: 127 | condition: service_completed_successfully 128 | environment: 129 | - PORT=50051 130 | - CASSANDRA_CONTACT_POINTS=cassandra-node1,cassandra-node2,cassandra-node3 131 | - CASSANDRA_LOCAL_DC=datacenter1 132 | restart: always 133 | 134 | data-service2: 135 | build: 136 | context: . 137 | dockerfile: Dockerfile 138 | ports: 139 | - "50052:50052" 140 | networks: 141 | - dataservices-network 142 | depends_on: 143 | cassandra-node1: 144 | condition: service_healthy 145 | cassandra-node2: 146 | condition: service_healthy 147 | cassandra-node3: 148 | condition: service_healthy 149 | cassandra-init: 150 | condition: service_completed_successfully 151 | environment: 152 | - PORT=50052 153 | - CASSANDRA_CONTACT_POINTS=cassandra-node1,cassandra-node2,cassandra-node3 154 | - CASSANDRA_LOCAL_DC=datacenter1 155 | restart: always 156 | 157 | volumes: 158 | cassandra_data1: 159 | cassandra_data2: 160 | cassandra_data3: 161 | 162 | networks: 163 | dataservices-network: 164 | driver: bridge -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amrdb/data-services 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/gocql/gocql v1.7.0 7 | google.golang.org/grpc v1.67.1 8 | google.golang.org/protobuf v1.35.1 9 | ) 10 | 11 | require ( 12 | github.com/golang/snappy v0.0.3 // indirect 13 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 14 | golang.org/x/net v0.28.0 // indirect 15 | golang.org/x/sys v0.24.0 // indirect 16 | golang.org/x/text v0.17.0 // indirect 17 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect 18 | gopkg.in/inf.v0 v0.9.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 2 | github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 3 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 4 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= 7 | github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= 8 | github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= 9 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 13 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= 14 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 15 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 16 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 17 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 18 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 22 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 23 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 24 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 25 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 26 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 27 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 28 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= 29 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 30 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 31 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 32 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 33 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 34 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 35 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 36 | -------------------------------------------------------------------------------- /init.cql: -------------------------------------------------------------------------------- 1 | CREATE KEYSPACE IF NOT EXISTS dataservices WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; 2 | 3 | USE dataservices; 4 | 5 | CREATE TABLE IF NOT EXISTS messages ( 6 | channel_id bigint, 7 | message_id bigint, 8 | author_id bigint, 9 | content text, 10 | PRIMARY KEY (channel_id, message_id) 11 | ) WITH CLUSTERING ORDER BY (message_id DESC); 12 | 13 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (1, 1000, 5000, 'Hello, this is the first message.') IF NOT EXISTS; 14 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (2, 2000, 6000, 'Second channel first message.') IF NOT EXISTS; 15 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (3, 3000, 7000, 'Channel 3000') IF NOT EXISTS; 16 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (4, 4000, 8000, 'Channel 4000') IF NOT EXISTS; 17 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (5, 5000, 9000, 'Channel 5000') IF NOT EXISTS; 18 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (6, 6000, 10000, 'Channel 6000') IF NOT EXISTS; 19 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (7, 7000, 11000, 'Channel 7000') IF NOT EXISTS; 20 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (8, 8000, 12000, 'Channel 8000') IF NOT EXISTS; 21 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (9, 9000, 13000, 'Channel 9000') IF NOT EXISTS; 22 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (10, 10000, 14000, 'Channel 10000') IF NOT EXISTS; 23 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (11, 11000, 15000, 'Channel 11000') IF NOT EXISTS; 24 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (12, 12000, 16000, 'Channel 12000') IF NOT EXISTS; 25 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (13, 13000, 17000, 'Channel 13000') IF NOT EXISTS; 26 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (14, 14000, 18000, 'Channel 14000') IF NOT EXISTS; 27 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (15, 15000, 19000, 'Channel 15000') IF NOT EXISTS; 28 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (16, 16000, 20000, 'Channel 16000') IF NOT EXISTS; 29 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (17, 17000, 21000, 'Channel 17000') IF NOT EXISTS; 30 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (18, 18000, 22000, 'Channel 18000') IF NOT EXISTS; 31 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (19, 19000, 23000, 'Channel 19000') IF NOT EXISTS; 32 | INSERT INTO messages (channel_id, message_id, author_id, content) VALUES (20, 20000, 24000, 'Channel 20000') IF NOT EXISTS; -------------------------------------------------------------------------------- /messages/messages.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.35.1 4 | // protoc v3.6.1 5 | // source: messages/messages.proto 6 | 7 | package messages 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type MessageRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | ChannelId int64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` 29 | MessageId int64 `protobuf:"varint,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` 30 | } 31 | 32 | func (x *MessageRequest) Reset() { 33 | *x = MessageRequest{} 34 | mi := &file_messages_messages_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | 39 | func (x *MessageRequest) String() string { 40 | return protoimpl.X.MessageStringOf(x) 41 | } 42 | 43 | func (*MessageRequest) ProtoMessage() {} 44 | 45 | func (x *MessageRequest) ProtoReflect() protoreflect.Message { 46 | mi := &file_messages_messages_proto_msgTypes[0] 47 | if x != nil { 48 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 49 | if ms.LoadMessageInfo() == nil { 50 | ms.StoreMessageInfo(mi) 51 | } 52 | return ms 53 | } 54 | return mi.MessageOf(x) 55 | } 56 | 57 | // Deprecated: Use MessageRequest.ProtoReflect.Descriptor instead. 58 | func (*MessageRequest) Descriptor() ([]byte, []int) { 59 | return file_messages_messages_proto_rawDescGZIP(), []int{0} 60 | } 61 | 62 | func (x *MessageRequest) GetChannelId() int64 { 63 | if x != nil { 64 | return x.ChannelId 65 | } 66 | return 0 67 | } 68 | 69 | func (x *MessageRequest) GetMessageId() int64 { 70 | if x != nil { 71 | return x.MessageId 72 | } 73 | return 0 74 | } 75 | 76 | type MessageReply struct { 77 | state protoimpl.MessageState 78 | sizeCache protoimpl.SizeCache 79 | unknownFields protoimpl.UnknownFields 80 | 81 | ChannelId int64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` 82 | MessageId int64 `protobuf:"varint,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` 83 | AuthorId int64 `protobuf:"varint,3,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` 84 | Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` 85 | } 86 | 87 | func (x *MessageReply) Reset() { 88 | *x = MessageReply{} 89 | mi := &file_messages_messages_proto_msgTypes[1] 90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 91 | ms.StoreMessageInfo(mi) 92 | } 93 | 94 | func (x *MessageReply) String() string { 95 | return protoimpl.X.MessageStringOf(x) 96 | } 97 | 98 | func (*MessageReply) ProtoMessage() {} 99 | 100 | func (x *MessageReply) ProtoReflect() protoreflect.Message { 101 | mi := &file_messages_messages_proto_msgTypes[1] 102 | if x != nil { 103 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 104 | if ms.LoadMessageInfo() == nil { 105 | ms.StoreMessageInfo(mi) 106 | } 107 | return ms 108 | } 109 | return mi.MessageOf(x) 110 | } 111 | 112 | // Deprecated: Use MessageReply.ProtoReflect.Descriptor instead. 113 | func (*MessageReply) Descriptor() ([]byte, []int) { 114 | return file_messages_messages_proto_rawDescGZIP(), []int{1} 115 | } 116 | 117 | func (x *MessageReply) GetChannelId() int64 { 118 | if x != nil { 119 | return x.ChannelId 120 | } 121 | return 0 122 | } 123 | 124 | func (x *MessageReply) GetMessageId() int64 { 125 | if x != nil { 126 | return x.MessageId 127 | } 128 | return 0 129 | } 130 | 131 | func (x *MessageReply) GetAuthorId() int64 { 132 | if x != nil { 133 | return x.AuthorId 134 | } 135 | return 0 136 | } 137 | 138 | func (x *MessageReply) GetContent() string { 139 | if x != nil { 140 | return x.Content 141 | } 142 | return "" 143 | } 144 | 145 | type MetricsReply struct { 146 | state protoimpl.MessageState 147 | sizeCache protoimpl.SizeCache 148 | unknownFields protoimpl.UnknownFields 149 | 150 | TotalRequests int64 `protobuf:"varint,1,opt,name=total_requests,json=totalRequests,proto3" json:"total_requests,omitempty"` 151 | QueriesExecuted int64 `protobuf:"varint,2,opt,name=queries_executed,json=queriesExecuted,proto3" json:"queries_executed,omitempty"` 152 | } 153 | 154 | func (x *MetricsReply) Reset() { 155 | *x = MetricsReply{} 156 | mi := &file_messages_messages_proto_msgTypes[2] 157 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 158 | ms.StoreMessageInfo(mi) 159 | } 160 | 161 | func (x *MetricsReply) String() string { 162 | return protoimpl.X.MessageStringOf(x) 163 | } 164 | 165 | func (*MetricsReply) ProtoMessage() {} 166 | 167 | func (x *MetricsReply) ProtoReflect() protoreflect.Message { 168 | mi := &file_messages_messages_proto_msgTypes[2] 169 | if x != nil { 170 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 171 | if ms.LoadMessageInfo() == nil { 172 | ms.StoreMessageInfo(mi) 173 | } 174 | return ms 175 | } 176 | return mi.MessageOf(x) 177 | } 178 | 179 | // Deprecated: Use MetricsReply.ProtoReflect.Descriptor instead. 180 | func (*MetricsReply) Descriptor() ([]byte, []int) { 181 | return file_messages_messages_proto_rawDescGZIP(), []int{2} 182 | } 183 | 184 | func (x *MetricsReply) GetTotalRequests() int64 { 185 | if x != nil { 186 | return x.TotalRequests 187 | } 188 | return 0 189 | } 190 | 191 | func (x *MetricsReply) GetQueriesExecuted() int64 { 192 | if x != nil { 193 | return x.QueriesExecuted 194 | } 195 | return 0 196 | } 197 | 198 | type Empty struct { 199 | state protoimpl.MessageState 200 | sizeCache protoimpl.SizeCache 201 | unknownFields protoimpl.UnknownFields 202 | } 203 | 204 | func (x *Empty) Reset() { 205 | *x = Empty{} 206 | mi := &file_messages_messages_proto_msgTypes[3] 207 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 208 | ms.StoreMessageInfo(mi) 209 | } 210 | 211 | func (x *Empty) String() string { 212 | return protoimpl.X.MessageStringOf(x) 213 | } 214 | 215 | func (*Empty) ProtoMessage() {} 216 | 217 | func (x *Empty) ProtoReflect() protoreflect.Message { 218 | mi := &file_messages_messages_proto_msgTypes[3] 219 | if x != nil { 220 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 221 | if ms.LoadMessageInfo() == nil { 222 | ms.StoreMessageInfo(mi) 223 | } 224 | return ms 225 | } 226 | return mi.MessageOf(x) 227 | } 228 | 229 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 230 | func (*Empty) Descriptor() ([]byte, []int) { 231 | return file_messages_messages_proto_rawDescGZIP(), []int{3} 232 | } 233 | 234 | var File_messages_messages_proto protoreflect.FileDescriptor 235 | 236 | var file_messages_messages_proto_rawDesc = []byte{ 237 | 0x0a, 0x17, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 238 | 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 239 | 0x67, 0x65, 0x73, 0x22, 0x4e, 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 240 | 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 241 | 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 242 | 0x65, 0x6c, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 243 | 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 244 | 0x65, 0x49, 0x64, 0x22, 0x83, 0x01, 0x0a, 0x0c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 245 | 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 246 | 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 247 | 0x6c, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x69, 248 | 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 249 | 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 250 | 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x49, 0x64, 0x12, 251 | 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 252 | 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x60, 0x0a, 0x0c, 0x4d, 0x65, 0x74, 253 | 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 254 | 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 255 | 0x03, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 256 | 0x12, 0x29, 0x0a, 0x10, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x65, 0x78, 0x65, 0x63, 257 | 0x75, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x71, 0x75, 0x65, 0x72, 258 | 0x69, 0x65, 0x73, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, 0x22, 0x07, 0x0a, 0x05, 0x45, 259 | 0x6d, 0x70, 0x74, 0x79, 0x32, 0x90, 0x01, 0x0a, 0x0f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 260 | 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x67, 0x65, 0x74, 0x4d, 261 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 262 | 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 263 | 0x1a, 0x16, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 264 | 0x61, 0x67, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3d, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x41, 265 | 0x6e, 0x64, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x0f, 266 | 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 267 | 0x16, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 268 | 0x63, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 269 | 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x41, 0x6d, 0x72, 0x32, 0x38, 0x31, 0x32, 0x2f, 0x64, 0x61, 270 | 0x74, 0x61, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 271 | 0x61, 0x67, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 272 | } 273 | 274 | var ( 275 | file_messages_messages_proto_rawDescOnce sync.Once 276 | file_messages_messages_proto_rawDescData = file_messages_messages_proto_rawDesc 277 | ) 278 | 279 | func file_messages_messages_proto_rawDescGZIP() []byte { 280 | file_messages_messages_proto_rawDescOnce.Do(func() { 281 | file_messages_messages_proto_rawDescData = protoimpl.X.CompressGZIP(file_messages_messages_proto_rawDescData) 282 | }) 283 | return file_messages_messages_proto_rawDescData 284 | } 285 | 286 | var file_messages_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 287 | var file_messages_messages_proto_goTypes = []any{ 288 | (*MessageRequest)(nil), // 0: messages.MessageRequest 289 | (*MessageReply)(nil), // 1: messages.MessageReply 290 | (*MetricsReply)(nil), // 2: messages.MetricsReply 291 | (*Empty)(nil), // 3: messages.Empty 292 | } 293 | var file_messages_messages_proto_depIdxs = []int32{ 294 | 0, // 0: messages.MessagesService.getMessage:input_type -> messages.MessageRequest 295 | 3, // 1: messages.MessagesService.getAndResetMetrics:input_type -> messages.Empty 296 | 1, // 2: messages.MessagesService.getMessage:output_type -> messages.MessageReply 297 | 2, // 3: messages.MessagesService.getAndResetMetrics:output_type -> messages.MetricsReply 298 | 2, // [2:4] is the sub-list for method output_type 299 | 0, // [0:2] is the sub-list for method input_type 300 | 0, // [0:0] is the sub-list for extension type_name 301 | 0, // [0:0] is the sub-list for extension extendee 302 | 0, // [0:0] is the sub-list for field type_name 303 | } 304 | 305 | func init() { file_messages_messages_proto_init() } 306 | func file_messages_messages_proto_init() { 307 | if File_messages_messages_proto != nil { 308 | return 309 | } 310 | type x struct{} 311 | out := protoimpl.TypeBuilder{ 312 | File: protoimpl.DescBuilder{ 313 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 314 | RawDescriptor: file_messages_messages_proto_rawDesc, 315 | NumEnums: 0, 316 | NumMessages: 4, 317 | NumExtensions: 0, 318 | NumServices: 1, 319 | }, 320 | GoTypes: file_messages_messages_proto_goTypes, 321 | DependencyIndexes: file_messages_messages_proto_depIdxs, 322 | MessageInfos: file_messages_messages_proto_msgTypes, 323 | }.Build() 324 | File_messages_messages_proto = out.File 325 | file_messages_messages_proto_rawDesc = nil 326 | file_messages_messages_proto_goTypes = nil 327 | file_messages_messages_proto_depIdxs = nil 328 | } 329 | -------------------------------------------------------------------------------- /messages/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/amrdb/data-services/messages"; 4 | 5 | package messages; 6 | 7 | service MessagesService { 8 | rpc getMessage (MessageRequest) returns (MessageReply); 9 | rpc getAndResetMetrics (Empty) returns (MetricsReply); 10 | } 11 | 12 | message MessageRequest { 13 | int64 channel_id = 1; 14 | int64 message_id = 2; 15 | } 16 | 17 | message MessageReply { 18 | int64 channel_id = 1; 19 | int64 message_id = 2; 20 | int64 author_id = 3; 21 | string content = 4; 22 | } 23 | 24 | message MetricsReply { 25 | int64 total_requests = 1; 26 | int64 queries_executed = 2; 27 | } 28 | 29 | message Empty {} 30 | -------------------------------------------------------------------------------- /messages/messages_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v3.6.1 5 | // source: messages/messages.proto 6 | 7 | package messages 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | MessagesService_GetMessage_FullMethodName = "/messages.MessagesService/getMessage" 23 | MessagesService_GetAndResetMetrics_FullMethodName = "/messages.MessagesService/getAndResetMetrics" 24 | ) 25 | 26 | // MessagesServiceClient is the client API for MessagesService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | type MessagesServiceClient interface { 30 | GetMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*MessageReply, error) 31 | GetAndResetMetrics(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MetricsReply, error) 32 | } 33 | 34 | type messagesServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewMessagesServiceClient(cc grpc.ClientConnInterface) MessagesServiceClient { 39 | return &messagesServiceClient{cc} 40 | } 41 | 42 | func (c *messagesServiceClient) GetMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*MessageReply, error) { 43 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 44 | out := new(MessageReply) 45 | err := c.cc.Invoke(ctx, MessagesService_GetMessage_FullMethodName, in, out, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | func (c *messagesServiceClient) GetAndResetMetrics(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MetricsReply, error) { 53 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 54 | out := new(MetricsReply) 55 | err := c.cc.Invoke(ctx, MessagesService_GetAndResetMetrics_FullMethodName, in, out, cOpts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | // MessagesServiceServer is the server API for MessagesService service. 63 | // All implementations must embed UnimplementedMessagesServiceServer 64 | // for forward compatibility. 65 | type MessagesServiceServer interface { 66 | GetMessage(context.Context, *MessageRequest) (*MessageReply, error) 67 | GetAndResetMetrics(context.Context, *Empty) (*MetricsReply, error) 68 | mustEmbedUnimplementedMessagesServiceServer() 69 | } 70 | 71 | // UnimplementedMessagesServiceServer must be embedded to have 72 | // forward compatible implementations. 73 | // 74 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 75 | // pointer dereference when methods are called. 76 | type UnimplementedMessagesServiceServer struct{} 77 | 78 | func (UnimplementedMessagesServiceServer) GetMessage(context.Context, *MessageRequest) (*MessageReply, error) { 79 | return nil, status.Errorf(codes.Unimplemented, "method GetMessage not implemented") 80 | } 81 | func (UnimplementedMessagesServiceServer) GetAndResetMetrics(context.Context, *Empty) (*MetricsReply, error) { 82 | return nil, status.Errorf(codes.Unimplemented, "method GetAndResetMetrics not implemented") 83 | } 84 | func (UnimplementedMessagesServiceServer) mustEmbedUnimplementedMessagesServiceServer() {} 85 | func (UnimplementedMessagesServiceServer) testEmbeddedByValue() {} 86 | 87 | // UnsafeMessagesServiceServer may be embedded to opt out of forward compatibility for this service. 88 | // Use of this interface is not recommended, as added methods to MessagesServiceServer will 89 | // result in compilation errors. 90 | type UnsafeMessagesServiceServer interface { 91 | mustEmbedUnimplementedMessagesServiceServer() 92 | } 93 | 94 | func RegisterMessagesServiceServer(s grpc.ServiceRegistrar, srv MessagesServiceServer) { 95 | // If the following call pancis, it indicates UnimplementedMessagesServiceServer was 96 | // embedded by pointer and is nil. This will cause panics if an 97 | // unimplemented method is ever invoked, so we test this at initialization 98 | // time to prevent it from happening at runtime later due to I/O. 99 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 100 | t.testEmbeddedByValue() 101 | } 102 | s.RegisterService(&MessagesService_ServiceDesc, srv) 103 | } 104 | 105 | func _MessagesService_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(MessageRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(MessagesServiceServer).GetMessage(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: MessagesService_GetMessage_FullMethodName, 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(MessagesServiceServer).GetMessage(ctx, req.(*MessageRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | func _MessagesService_GetAndResetMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 124 | in := new(Empty) 125 | if err := dec(in); err != nil { 126 | return nil, err 127 | } 128 | if interceptor == nil { 129 | return srv.(MessagesServiceServer).GetAndResetMetrics(ctx, in) 130 | } 131 | info := &grpc.UnaryServerInfo{ 132 | Server: srv, 133 | FullMethod: MessagesService_GetAndResetMetrics_FullMethodName, 134 | } 135 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 136 | return srv.(MessagesServiceServer).GetAndResetMetrics(ctx, req.(*Empty)) 137 | } 138 | return interceptor(ctx, in, info, handler) 139 | } 140 | 141 | // MessagesService_ServiceDesc is the grpc.ServiceDesc for MessagesService service. 142 | // It's only intended for direct use with grpc.RegisterService, 143 | // and not to be introspected or modified (even as a copy) 144 | var MessagesService_ServiceDesc = grpc.ServiceDesc{ 145 | ServiceName: "messages.MessagesService", 146 | HandlerType: (*MessagesServiceServer)(nil), 147 | Methods: []grpc.MethodDesc{ 148 | { 149 | MethodName: "getMessage", 150 | Handler: _MessagesService_GetMessage_Handler, 151 | }, 152 | { 153 | MethodName: "getAndResetMetrics", 154 | Handler: _MessagesService_GetAndResetMetrics_Handler, 155 | }, 156 | }, 157 | Streams: []grpc.StreamDesc{}, 158 | Metadata: "messages/messages.proto", 159 | } 160 | --------------------------------------------------------------------------------