Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Sử dụng ảnh golang:1.16.0-alpine làm base image
FROM golang:latest

# Sao chép mã nguồn vào thư mục /app trong container
COPY Dockerfile docker-compose.yaml go.mod main.go /app/

# Đặt thư mục làm việc mặc định trong container
WORKDIR /app

RUN go get github.com/EngineerProOrg/BE-K01
# Cài đặt dependencies và biên dịch ứng dụng
RUN go mod download && go build -o main .

# Chạy ứng dụng
CMD ["./main"]
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'
services:
redis:
image: redis:latest
ports:
- 6379:6379
app:
build: .
ports:
- 8080:8080
depends_on:
- redis
145 changes: 145 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"log"
"time"
"net/http"
"fmt"

"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)

var client *redis.Client
var hllKey = "ping_callers"


func init() {
// Initialize Redis client
client = redis.NewClient(&redis.Options{
Addr: "redis:6379",
})
}

func main() {

// Check to connect Redis
_, err := client.Ping(client.Context()).Result()
if err != nil {
log.Fatal(err)
}

router := gin.Default()

router.POST("/login", loginHandler)
router.GET("/ping", pingHandler)
router.GET("/top", topHandler)
router.GET("/count", countHandler)

router.Run(":8080")
}

func loginHandler(c *gin.Context) {
sessionID := c.GetHeader("session_id")
if sessionID != "" {
// Session ID already exists, no need to create a new session
c.JSON(http.StatusOK, gin.H{"message": "Already logged in"})
return
}

username := c.PostForm("username")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should check if this req have session id or not first, if already have then its mean he already logged in, no need to do anything

sessionID = generateSessionID()

// Store the session ID and user name in Redis
err := client.Set(c, sessionID, username, 0).Err()
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
return
}

// Set sessionID cookie
c.SetCookie("session_id", sessionID, 300, "/", "localhost", false, true)

c.JSON(http.StatusOK, gin.H{"session_id": sessionID, "username": username, "message": "Session created successfully"})
}

func pingHandler(c *gin.Context) {
sessionID, err := c.Cookie("session_id")
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "session ID not found"})
return
}

// Check if the session ID is valid
userName, err := client.Get(c, sessionID).Result()
if err == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session ID"})
return
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve session"})
return
}

// Acquire distributed lock to ensure only one person can call /ping at a time
lockKey := fmt.Sprintf("lock:%s", userName)
ok, err := client.SetNX(c, lockKey, "locked", 5*time.Second).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to obtain lock"})
return
}
if !ok {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "failed to obtain lock"})
return
}

// Implement Redis-based rate limiting
rateLimitKey := fmt.Sprintf("ratelimit:%s", userName)
remaining, err := client.Incr(c, rateLimitKey).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to increment rate limit"})
return
}

if remaining > 2 {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
return
}

// Sleep for 5 seconds to simulate processing time
time.Sleep(5 * time.Second)

c.JSON(http.StatusOK, gin.H{"message": "pong"})
}

func topHandler(c *gin.Context) {
// Retrieve the top 10 most-called /ping APIs
result, err := client.ZRevRangeWithScores(c, hllKey, 0, 9).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve top APIs"})
return
}

var topAPIs []string
for _, z := range result {
topAPIs = append(topAPIs, z.Member.(string))
}

c.JSON(http.StatusOK, gin.H{"top_apis": topAPIs})
}

func countHandler(c *gin.Context) {
// Retrieve the approximate number of callers to /ping API using HyperLogLog
count, err := client.PFCount(c, hllKey).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve count"})
return
}

c.JSON(http.StatusOK, gin.H{"count": count})
}

func generateSessionID() string {
return uuid.New().String()
}