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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dump.rdb
261 changes: 226 additions & 35 deletions cmd/student-manager/main.go
Original file line number Diff line number Diff line change
@@ -1,68 +1,259 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"math"
"net/http"
"os"
"strconv"
"time"

"github.com/EngineerProOrg/BE-K01/configs"
"github.com/EngineerProOrg/BE-K01/pkg/controller"
"github.com/EngineerProOrg/BE-K01/pkg/service"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"gorm.io/driver/mysql"
"gorm.io/gorm"

"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v8"
)

const (
RedisTopPingKey = "top_pings"
RedisHyperLogLogKey = "hyperloglog"
RedisExpirationTime = 300 * time.Second
CookieExpirationTime = 300
MaxPingPerUser = 2
PingRateLimit = 60
TopPingCount = 10
MutexName = "pingLock"

DbServerAddress = "192.168.0.103:3306"
DbServerUser = "quangmx"
DbServerPassword = "2511"
DbName = "engineerpro"

RedisServerAddress = "192.168.0.103:6379"
RedisServerPassword = "2511"
)

var (
confPath = flag.String("conf", "files/live.json", "path to config file")
db *gorm.DB
router *gin.Engine
redisClient *redis.Client
mu *redsync.Mutex
)

func init() {
initDatabase()
initRedis()
initRouter()
}

// initDatabase initializes the database
func initDatabase() {
var err error
db, err = gorm.Open(mysql.New(mysql.Config{
DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", DbServerUser, DbServerPassword, DbServerAddress, DbName),
DefaultStringSize: 256,
DisableDatetimePrecision: true,
DontSupportRenameIndex: true,
SkipInitializeWithVersion: false,
}), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
fmt.Println("Can not connect to db:", err)
return
}
}

// initRedis initializes the Redis instance
func initRedis() {
redisClient = redis.NewClient(&redis.Options{Addr: RedisServerAddress, Password: RedisServerPassword})
if redisClient == nil {
fmt.Println("Can not initialize redis")
return
}

// Create a pool with go-redis which is the pool redsync will use while
// communicating with Redis
redisPool := goredis.NewPool(redisClient)

// Create an instance of redsync to be used to obtain a mutual exclusion lock
rs := redsync.New(redisPool)

// Obtain a new mutex by using the same name for all instances wanting the same lock
mu = rs.NewMutex(MutexName)
}

// initRouter initializes the gin router
func initRouter() {
router = gin.Default()
}

func main() {
config := &configs.StudentManagerConfig{}
jsonFile, err := os.Open("users.json")
// if we os.Open returns an error then handle it
// Declare /login API
router.POST("/login", handleLogin)

// Declare /ping API
router.GET("/ping", handlePing)

// Delcare /top API
router.GET("/top", handleTop)

// Declare /count API
router.GET("/count", handleCount)

// Start the web server
err := router.Run(":8080")
if err != nil {
fmt.Println(err)
}
}

type Auth struct {
ID int `gorm:"primaryKey" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}

// handleLogin logs user in if valid and save sessionID in redis
func handleLogin(c *gin.Context) {
// Get username and password
username := c.PostForm("username")
password := c.PostForm("password")

// Check validity of username and password
var auth Auth
db.Raw("SELECT id from User where username = ? and password = ?", username, password).Scan(&auth)
if auth.ID == 0 {
c.IndentedJSON(http.StatusUnauthorized, gin.H{"message": "Wrong username or password"})
return
}
defer jsonFile.Close()
bt, err := io.ReadAll(jsonFile)

// If logged in, set a sessionID for this session
sessionID := uuid.New().String()

// Save current sessionID and username in Redis
err := redisClient.Set(redisClient.Context(), sessionID, username, RedisExpirationTime).Err()
if err != nil {
fmt.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
err = json.Unmarshal(bt, config)

// Set sessionID cookie
c.SetCookie("sessionID", sessionID, CookieExpirationTime, "/", c.Request.Host, false, true)

c.IndentedJSON(http.StatusOK, gin.H{"message": "Log in successfully!", "sessionID": sessionID})
}

// handlePing allows just one user calls at a time
func handlePing(c *gin.Context) {
// Acquire the distributed lock
mu.Lock()
defer mu.Unlock()

sessionID, err := c.Cookie("sessionID")
if err != nil {
fmt.Println(err)
c.IndentedJSON(http.StatusUnauthorized, gin.H{
"message": err.Error(),
})
return
}

r := gin.Default()
r.GET("/ping", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "pong"})
})
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "root:123456@tcp(127.0.0.1:3306)/engineerpro?charset=utf8mb4&parseTime=True&loc=Local",
DefaultStringSize: 256,
DisableDatetimePrecision: true,
DontSupportRenameIndex: true,
SkipInitializeWithVersion: false,
}), &gorm.Config{
SkipDefaultTransaction: true,
})
username, err := redisClient.Get(redisClient.Context(), sessionID).Result()
if err != nil {
fmt.Println("can not connect to db ", err)
c.IndentedJSON(http.StatusUnauthorized, gin.H{
"message": err.Error(),
})
return
}

// Return if can not find sessionID or username
if sessionID == "" || username == "" {
c.IndentedJSON(http.StatusUnauthorized, gin.H{"status": "Unauthorized"})
return
}
rd := redis.NewClient(&redis.Options{})
if rd == nil {

// Check if the user has exceeded the rate limit for /ping API
if !canMakePing(username) {
c.IndentedJSON(http.StatusTooManyRequests, gin.H{"message": "Rate limit exceeded"})
return
}

// Increase the counter for the user's /ping calls
increaseCounter(username)

// Simulate work inside /ping API
time.Sleep(3 * time.Second)

c.IndentedJSON(http.StatusOK, gin.H{"message": "Ping succeeded."})
}

// đếm số lượng lần 1 người gọi api /ping
func increaseCounter(username string) {
redisClient.ZIncrBy(redisClient.Context(), RedisTopPingKey, float64(1), username)
redisClient.PFAdd(redisClient.Context(), RedisHyperLogLogKey, username)
}

func canMakePing(username string) bool {
// Create a map to save ping of each user -> this map is on redis -> can scale up
pingID := "ping-" + username
pingInfo, _ := redisClient.HGetAll(redisClient.Context(), pingID).Result()

// If pingInfo is empty then create new pingInfo
if len(pingInfo) == 0 {
err := setPingInfo(pingID, 0, int(time.Now().Unix()))
if err != nil {
panic(err)
}
return true
}

currPingTime := time.Now().Unix()
blockTime, _ := strconv.ParseInt(pingInfo["blockTime"], 10, 32)
lastPingTime, _ := strconv.ParseInt(pingInfo["lastPingTime"], 10, 32)

if int(currPingTime)-int(lastPingTime) > int(blockTime) {
newBlockTime := math.Max(float64(0), float64(int(lastPingTime)+int(PingRateLimit)-int(currPingTime)))
err := setPingInfo(pingID, int(newBlockTime), int(currPingTime))
if err != nil {
panic(err)
}
return true
}
return false
}

func setPingInfo(pingID string, blockTime, currPingTime int) error {
pingRecord := map[string]int{"blockTime": blockTime, "lastPingTime": currPingTime}
for k, v := range pingRecord {
err := redisClient.HSet(redisClient.Context(), pingID, k, v).Err()
if err != nil {
return err
}
}
return nil
}

// handleTop retrieves the top 10 callers of /ping API
func handleTop(c *gin.Context) {
topUsers, err := redisClient.ZRevRangeWithScores(redisClient.Context(), RedisTopPingKey, 0, TopPingCount-1).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve top users"})
return
}
service := service.NewService(db, rd)
controller.MappingService(r, service)
r.Run()

c.IndentedJSON(http.StatusOK, gin.H{"topUsers": topUsers})
}

// handleCount retrieves number of users called /ping
func handleCount(c *gin.Context) {
count, err := redisClient.PFCount(redisClient.Context(), RedisHyperLogLogKey).Result()
if err != nil {
panic(err)
}
c.IndentedJSON(http.StatusOK, gin.H{"Number of /ping users": count})
}
43 changes: 26 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,41 @@ require (
)

require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/go-redsync/redsync/v4 v4.8.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/redis/go-redis/v9 v9.0.2 // indirect
)

require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading