-
Notifications
You must be signed in to change notification settings - Fork 0
Anonymous: Add configurable device limit #2
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,14 @@ import ( | |
| ) | ||
|
|
||
| const cacheKeyPrefix = "anon-device" | ||
| const anonymousDeviceExpiration = 30 * 24 * time.Hour | ||
|
|
||
| var ErrDeviceLimitReached = fmt.Errorf("device limit reached") | ||
|
|
||
| type AnonDBStore struct { | ||
| sqlStore db.DB | ||
| log log.Logger | ||
| sqlStore db.DB | ||
| log log.Logger | ||
| deviceLimit int64 | ||
| } | ||
|
|
||
| type Device struct { | ||
|
|
@@ -45,8 +49,8 @@ type AnonStore interface { | |
| DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error | ||
| } | ||
|
|
||
| func ProvideAnonDBStore(sqlStore db.DB) *AnonDBStore { | ||
| return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore")} | ||
| func ProvideAnonDBStore(sqlStore db.DB, deviceLimit int64) *AnonDBStore { | ||
| return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore"), deviceLimit: deviceLimit} | ||
| } | ||
|
|
||
| func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error) { | ||
|
|
@@ -65,9 +69,54 @@ func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time | |
| return devices, err | ||
| } | ||
|
|
||
| // updateDevice updates a device if it exists and has been updated between the given times. | ||
| func (s *AnonDBStore) updateDevice(ctx context.Context, device *Device) error { | ||
| const query = `UPDATE anon_device SET | ||
| client_ip = ?, | ||
| user_agent = ?, | ||
| updated_at = ? | ||
| WHERE device_id = ? AND updated_at BETWEEN ? AND ?` | ||
|
|
||
| args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID, | ||
| device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute), | ||
| } | ||
| err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { | ||
| args = append([]interface{}{query}, args...) | ||
| result, err := dbSession.Exec(args...) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| rowsAffected, err := result.RowsAffected() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if rowsAffected == 0 { | ||
| return ErrDeviceLimitReached | ||
| } | ||
|
|
||
| return nil | ||
| }) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error { | ||
| var query string | ||
|
|
||
| // if device limit is reached, only update devices | ||
| if s.deviceLimit > 0 { | ||
| count, err := s.CountDevices(ctx, time.Now().UTC().Add(-anonymousDeviceExpiration), time.Now().UTC().Add(time.Minute)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if count >= s.deviceLimit { | ||
| return s.updateDevice(ctx, device) | ||
| } | ||
| } | ||
|
Comment on lines
+108
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the full function containing lines 108-118
cd /root/repo && sed -n '80,130p' pkg/services/anonymous/anonimpl/anonstore/database.goRepository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 143 🏁 Script executed: # Let's also check the file size to ensure we can read it safely
wc -l pkg/services/anonymous/anonimpl/anonstore/database.goRepository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 140 🏁 Script executed: # Search for related comments about device limit enforcement
rg -i "device.*limit" pkg/services/anonymous/ -A 2 -B 2Repository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 4253 🏁 Script executed: # Look for transaction usage in this file
rg -i "transaction|begin|commit|rollback" pkg/services/anonymous/anonimpl/anonstore/database.goRepository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 65 🏁 Script executed: cat -n pkg/services/anonymous/anonimpl/anonstore/database.goRepository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 7419 🏁 Script executed: # Also check the test to understand expected behavior
cat -n pkg/services/anonymous/anonimpl/anonstore/database_test.goRepository: AI-Code-Review-Evals/coderabbit-grafana Length of output: 3519 TOCTOU race condition between count check and insert—device limit can be exceeded under concurrent load. The count check (line 110) and subsequent insert/update operations (lines 115-155) are not atomic. Multiple concurrent requests can both pass the count check simultaneously and then both proceed to insert, exceeding the device limit. For example, with a limit of 1: Request A counts 0 devices, Request B counts 0 devices, then both insert, resulting in 2 devices. Since |
||
|
|
||
| args := []any{device.DeviceID, device.ClientIP, device.UserAgent, | ||
| device.CreatedAt.UTC(), device.UpdatedAt.UTC()} | ||
| switch s.sqlStore.GetDBType() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify error semantics:
rowsAffected == 0may not always indicate limit reached.The
updateDevicemethod returnsErrDeviceLimitReachedwhen no rows are affected, but this could also occur if the device doesn't exist or ifupdated_atfalls outside the time window for reasons other than the limit being reached. This conflates different failure modes.Consider returning a more specific error or adding a check to distinguish between "device not found" and "device limit reached".
Suggested approach
if rowsAffected == 0 { - return ErrDeviceLimitReached + return ErrDeviceLimitReached // Note: This also triggers if device doesn't exist within the expiration window }Alternatively, you could query for the device's existence first to provide a more accurate error, though this adds an extra query.