-
Notifications
You must be signed in to change notification settings - Fork 0
Anonymous: Add configurable device limit #10
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
base: enhance-anonymous-access
Are you sure you want to change the base?
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. Race condition in device limit enforcement. The device limit check has a race condition:
Between the count check and the insert/update, another concurrent request could create a device, causing the actual count to exceed the limit. This race condition could allow the device limit to be exceeded under concurrent load. Consider one of these approaches:
// Pseudo-code
s.lock.Lock()
defer s.lock.Unlock()
count, err := s.CountDevices(...)
if count >= s.deviceLimit {
return s.updateDevice(ctx, device)
}
// proceed with insert
Which approach is best depends on how strict the limit enforcement needs to be. 🤖 Prompt for AI Agents |
||
|
|
||
| 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 the time window logic and error semantics.
The
updateDevicefunction has several concerns:Ambiguous error: Returns
ErrDeviceLimitReachedwhen no rows are affected (line 95-96), but zero rows could mean:This makes the error potentially misleading.
Unusual upper time bound: The WHERE clause checks
updated_at BETWEEN (now - 30 days) AND (now + 1 minute)(line 81). The upper bound ofnow + 1 minuteseems intended to handle clock skew, but:updated_at >= ?with a single lower boundConsider refactoring to make the intent clearer:
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 ?` +WHERE device_id = ? AND updated_at >= ?` - args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID, - device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute), - } + args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID, + device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), + }And potentially return a different error or add a comment explaining that
ErrDeviceLimitReachedin this context means "device not found or expired, cannot update."🤖 Prompt for AI Agents