-
Notifications
You must be signed in to change notification settings - Fork 30
[Quang Ma] Week 2 - APIs Assignment #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maxuanquang
wants to merge
5
commits into
EngineerProOrg:scott/sample-gorm
Choose a base branch
from
maxuanquang:scott/sample-gorm-quangma
base: scott/sample-gorm
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
482204e
Fix bug to run
maxuanquang 27f2140
Small fixes
maxuanquang bd1524d
Do APIs assignment
maxuanquang a52659e
Use distributed lock and handle potential race condition
maxuanquang 5e61489
Add database server and redis server configs
maxuanquang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dump.rdb |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
maxuanquang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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}) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.