{"id":22850169,"url":"https://github.com/ramadani/andromeda","last_synced_at":"2025-03-31T06:28:25.258Z","repository":{"id":47995772,"uuid":"369526359","full_name":"ramadani/andromeda","owner":"ramadani","description":"An extensible quota keeper using redis","archived":false,"fork":false,"pushed_at":"2021-08-11T05:35:06.000Z","size":97,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-06T10:53:22.364Z","etag":null,"topics":["limiter","lock-free","quota","race-condition-prevention","race-conditions"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ramadani.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-05-21T12:21:16.000Z","updated_at":"2022-07-11T18:36:36.000Z","dependencies_parsed_at":"2022-08-12T16:10:55.143Z","dependency_job_id":null,"html_url":"https://github.com/ramadani/andromeda","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ramadani%2Fandromeda","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ramadani%2Fandromeda/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ramadani%2Fandromeda/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ramadani%2Fandromeda/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ramadani","download_url":"https://codeload.github.com/ramadani/andromeda/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246427529,"owners_count":20775602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["limiter","lock-free","quota","race-condition-prevention","race-conditions"],"created_at":"2024-12-13T05:06:44.384Z","updated_at":"2025-03-31T06:28:25.238Z","avatar_url":"https://github.com/ramadani.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# andromeda\n\nandromeda is a golang package that can be implemented on an object that has quotas.\n\n## Feature Overview\n\n- Add and reduce quota usage\n- Handle race conditions\n- Can be applied to multiple quotas\n- Reversible if an error occurs\n- Handling if quota usage is not exists in redis\n- Can be implemented according to your needs\n\n### Use cases\n\n- Flash sale\n- Claim vouchers\n- etc\n\n## Getting Started\n\n### Prerequisites\n\nandromeda requires [redis](https://redis.io/) to store quota data, make sure your machine has it.\n\n### Installation\n\n```\ngo get github.com/ramadani/andromeda\n```\n\n## Guide\n\nA quota consists of limits and usage. so the entity that you have must have these 2 attributes.\n\nThere are 4 functions that you need to prepare for each of your quota.\n\n1. Get quota limit  \nGet the limit of a quota. Limit is used to keep usage from exceeding the limit.\n\n2. Get quota usage  \nGet updated usage of a quota. This function will be called if the quota usage is not in Redis. After the value of the quota usage is obtained, the value will be saved to redis according to its key and expiration.\n\n3. Get quota usage key  \nGenerate key that will be used to store and get quota usage stored in redis.\n\n4. Get quota usage expiration  \nExpiration is used to set how long the duration of a quota usage is stored in Redis.\n\n### Example\n\nFor example, we will use claim voucher use case.\n\n```go\ntype Voucher struct {\n\tID    string\n\tCode  string\n\tLimit int64\n\tUsage int64\n}\n```\n\nTo get quota limit and usage, we create code using `GetQuota` interface\n\n**Get quota limit**\n\n```go\ntype getVoucherQuotaLimit struct {\n\tvoucherRepo VoucherRepository\n}\n\nfunc (v *getVoucherQuotaLimit) Do(ctx context.Context, req *andromeda.QuotaRequest) (int64, error) {\n\tvoucher, err := v.voucherRepo.FindByID(ctx, req.QuotaID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn voucher.Limit, nil\n}\n\nfunc NewGetVoucherQuotaLimit(voucherRepo VoucherRepository) andromeda.GetQuota {\n\treturn \u0026getVoucherQuotaLimit{voucherRepo: voucherRepo}\n}\n```\n\n**Get quota usage**\n\n```go\ntype getVoucherQuotaUsage struct {\n\tvoucherRepo VoucherRepository\n}\n\nfunc (v *getVoucherQuotaUsage) Do(ctx context.Context, req *andromeda.QuotaRequest) (int64, error) {\n\tvoucher, err := v.voucherRepo.FindByID(ctx, req.QuotaID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn voucher.Usage, nil\n}\n\nfunc NewGetVoucherQuotaUsage(voucherRepo VoucherRepository) andromeda.GetQuota {\n\treturn \u0026getVoucherQuotaUsage{voucherRepo: voucherRepo}\n}\n```\n\nTo get quota usage key, we create code using `GetQuotaKey` interface\n\n**Get quota usage key**\n\n```go\ntype getVoucherQuotaUsageKey struct {\n\tkeyFormat string\n}\n\nfunc (v *getVoucherQuotaUsageKey) Do(_ context.Context, req *andromeda.QuotaRequest) (string, error) {\n\treturn fmt.Sprintf(v.keyFormat, req.QuotaID), nil\n}\n\nfunc NewGetVoucherQuotaUsageKey(keyFormat string) andromeda.GetQuotaKey {\n\treturn \u0026getVoucherQuotaUsageKey{keyFormat: keyFormat}\n}\n```\n\nTo get quota usage expiration, we create code using the `GetQuotaExpiration` interface\n\n**Get quota usage expiration**\n\n```go\ntype getVoucherQuotaUsageExpiration struct{}\n\nfunc (v *getVoucherQuotaUsageExpiration) Do(ctx context.Context, req *andromeda.QuotaRequest) (time.Duration, error) {\n\treturn time.Hour * 5, nil\n}\n\nfunc NewGetVoucherQuotaUsageExpiration() andromeda.GetQuotaExpiration {\n\treturn \u0026getVoucherQuotaUsageExpiration{}\n}\n```\n\nThen use these 4 code implementations to create add quota usage and reduce quota usage functions\n\n```go\nkeyUsageFormat := \"voucher-quota-usage-%s\"\ngetVoucherQuotaLimit := internal.NewGetVoucherQuotaLimit(voucherRepo)\ngetVoucherQuotaUsage := internal.NewGetVoucherQuotaUsage(voucherRepo)\ngetVoucherQuotaUsageKey := internal.NewGetVoucherQuotaUsageKey(keyUsageFormat)\ngetVoucherQuotaUsageExpiration := internal.NewGetVoucherQuotaUsageExpiration()\ngetVoucherQuotaUsageConf := andromeda.GetQuotaUsageConfig{\n\tLockIn:   time.Second * 3,\n\tMaxRetry: 10,\n\tRetryIn:  time.Millisecond * 100,\n}\n\naddVoucherUsage := andromeda.AddQuotaUsage(andromeda.AddQuotaUsageConfig{\n\tCache:                   cacheRedis,\n\tGetQuotaLimit:           getVoucherQuotaLimit,\n\tGetQuotaUsage:           getVoucherQuotaUsage,\n\tGetQuotaUsageKey:        getVoucherQuotaUsageKey,\n\tGetQuotaUsageExpiration: getVoucherQuotaUsageExpiration,\n\tGetQuotaUsageConfig:     getVoucherQuotaUsageConf,\n})\n\nreduceVoucherUsage := andromeda.ReduceQuotaUsage(andromeda.ReduceQuotaUsageConfig{\n\tCache:                   cacheRedis,\n\tGetQuotaUsage:           getVoucherQuotaUsage,\n\tGetQuotaUsageKey:        getVoucherQuotaUsageKey,\n\tGetQuotaUsageExpiration: getVoucherQuotaUsageExpiration,\n\tGetQuotaUsageConfig:     getVoucherQuotaUsageConf,\n})\n```\n\nuse these functions in your logic code\n\n```go\nfunc (c *claimVoucher) Do(ctx context.Context, code, userID string) (*History, error) {\n\tvoucher, err := c.voucherRepo.FindByCode(ctx, code)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thistory := \u0026History{\n\t\tID:        uuid.NewV4().String(),\n\t\tVoucherID: voucher.ID,\n\t\tUserID:    userID,\n\t\tCreatedAt: time.Now(),\n\t}\n\n\tusageReq := andromeda.QuotaUsageRequest{\n\t\tQuotaID: voucher.ID,\n\t\tUsage:   1,\n\t}\n\n\tif _, err = c.addVoucherUsage.Do(ctx, \u0026usageReq); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = c.historyRepo.Create(ctx, history); err == nil {\n\t\treturn history, nil\n\t}\n\n\t// has error, do reverse usage quota\n\tif _, er := c.reduceVoucherUsage.Do(ctx, \u0026usageReq); er != nil {\n\t\terr = er\n\t}\n\n\treturn nil, err\n}\n```\n\n#### Listener\n\nYou can implement a listener to know the events on success or error using the `UpdateQuotaUsageListener` interface.\n\n```go\ntype updateVoucherQuotaUsageListener struct{}\n\nfunc (v *updateVoucherQuotaUsageListener) OnSuccess(ctx context.Context, req *andromeda.QuotaUsageRequest, updatedUsage int64) {\n\tlog.Println(\"updated quota\", updatedUsage)\n}\n\nfunc (v *updateVoucherQuotaUsageListener) OnError(ctx context.Context, req *andromeda.QuotaUsageRequest, err error) {\n\tlog.Println(\"err\", err)\n}\n\nfunc NewUpdateVoucherQuotaUsageListener() andromeda.UpdateQuotaUsageListener {\n\treturn \u0026updateVoucherQuotaUsageListener{}\n}\n```\n\nAdd the listener to the option\n\n```go\nupdateVoucherQuotaUsageListener := internal.NewUpdateVoucherQuotaUsageListener()\n\naddVoucherUsage := andromeda.AddQuotaUsage(andromeda.AddQuotaUsageConfig{\n\tCache:                   cacheRedis,\n\tGetQuotaLimit:           getVoucherQuotaLimit,\n\tGetQuotaUsage:           getVoucherQuotaUsage,\n\tGetQuotaUsageKey:        getVoucherQuotaUsageKey,\n\tGetQuotaUsageExpiration: getVoucherQuotaUsageExpiration,\n\tGetQuotaUsageConfig:     getVoucherQuotaUsageConf,\n\tOption: andromeda.AddUsageOption{\n\t\tListener: updateVoucherQuotaUsageListener,\n\t},\n})\n\nreduceVoucherUsage := andromeda.ReduceQuotaUsage(andromeda.ReduceQuotaUsageConfig{\n\tCache:                   cacheRedis,\n\tGetQuotaUsage:           getVoucherQuotaUsage,\n\tGetQuotaUsageKey:        getVoucherQuotaUsageKey,\n\tGetQuotaUsageExpiration: getVoucherQuotaUsageExpiration,\n\tGetQuotaUsageConfig:     getVoucherQuotaUsageConf,\n\tOption: andromeda.ReduceUsageOption{\n\t\tListener: updateVoucherQuotaUsageListener,\n\t},\n})\n```\n\nCheck out the [examples](example) to find out more\n\n### Tips\n\n1. Use job scheduling to update usage from redis to database\n2. Set expiration is longer than the original quota time. For example, the quota period is only 3 days, the set expiration is more than 3 days so that the value in redis will still be there when the job scheduling period is still running\n3. To get the quota limit, you can save it to redis so that it doesn't always get it from the database\n\n## Contributing\n\n1. Fork the Project\n2. Create your Feature Branch (git checkout -b feature/AmazingFeature)\n3. Commit your Changes (git commit -m 'Add some AmazingFeature')\n4. Push to the Branch (git push origin feature/AmazingFeature)\n5. Open a Pull Request\n\n## License\n\nDistributed under the MIT License. See `LICENSE` for more information.\n\n## References\n\n- [Race condition](https://en.wikipedia.org/wiki/Race_condition)\n- [Redis incr/decr](https://redis.io/)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Framadani%2Fandromeda","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Framadani%2Fandromeda","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Framadani%2Fandromeda/lists"}