···1212// Must be called with the slurper lock held
1313func (r *Relay) canSlurpHost(hostname string) bool {
1414 // Check if we're over the limit for new hosts today
1515- if !r.Slurper.NewHostPerDayLimiter.Allow() {
1515+ if !r.HostPerDayLimiter.Allow() {
1616 return false
1717 }
1818
+16-8
cmd/relay/relay/models/models.go
···2222)
23232424type Host struct {
2525- ID uint64 `gorm:"column:id;primarykey"`
2525+ ID uint64 `gorm:"column:id;primarykey"`
2626+2727+ // these fields are automatically managed by gorm (by convention)
2628 CreatedAt time.Time
2729 UpdatedAt time.Time
2830···37393840 // TODO: ThrottleUntil time.Time
39414040- // indicates this is a highly trusted host (PDS) and different limits apply
4242+ // indicates this is a highly trusted host (PDS), and different rate limits apply
4143 Trusted bool `gorm:"column:trusted;default:false"`
42444343- // enum of account status
4445 Status HostStatus `gorm:"column:status;default:active"`
45464646- // negative number indicates no sequence recorded
4747- LastSeq int64 `gorm:"column:last_seq"`
4848- AccountCount int64 `gorm:"column:account_count"`
4747+ // the last sequence number persisted for this host. updated periodically, and at shutdown. negative number indicates no sequence recorded
4848+ LastSeq int64 `gorm:"column:last_seq;default:-1"`
4949+5050+ // represents the number of accounts on the host, minus any in "deleted" state
5151+ AccountCount int64 `gorm:"column:account_count;default:0"`
4952}
50535154func (Host) TableName() string {
···7174)
72757376type Account struct {
7474- UID uint64 `gorm:"column:uid;primarykey"`
7575- DID string `gorm:"column:did;uniqueIndex;not null"`
7777+ UID uint64 `gorm:"column:uid;primarykey"`
7878+ DID string `gorm:"column:did;uniqueIndex;not null"`
7979+8080+ // this is a reference to the ID field on Host; but it is not an explicit foreign key
7681 HostID uint64 `gorm:"column:host_id;not null"`
7782 Status AccountStatus `gorm:"column:status;default:active"`
7883 UpstreamStatus AccountStatus `gorm:"column:upstream_status;default:active"`
···85908691// This is a small extension table to `Account`, which holds fast-changing fields updated on every firehose event.
8792type AccountRepo struct {
9393+ // references Account.UID, but not set up as a foreign key
8894 UID uint64 `gorm:"column:uid;primarykey"`
8995 Rev string `gorm:"column:rev;not null"`
9696+9097 // The CID of the entire signed commit block. Sometimes called the "head"
9198 CommitCID string `gorm:"column:commit_cid;not null"`
9999+92100 // The CID of the top of the repo MST, which is the 'data' field within the commit block. This becomes 'prevData'
93101 CommitData string `gorm:"column:commit_data;not null"`
94102}
+12-6
cmd/relay/relay/relay.go
···88 "github.com/bluesky-social/indigo/cmd/relay/relay/models"
99 "github.com/bluesky-social/indigo/cmd/relay/stream/eventmgr"
10101111+ "github.com/RussellLuo/slidingwindow"
1112 "github.com/hashicorp/golang-lru/v2"
1213 "go.opentelemetry.io/otel"
1314 "gorm.io/gorm"
···2425 HostChecker HostChecker
2526 Config RelayConfig
26272727- // accountLk serializes a section of syncHostAccount()
2828- // TODO: at some point we will want to lock specific DIDs, this lock as is
2929- // is overly broad, but i dont expect it to be a bottleneck for now
3030- accountLk sync.Mutex
3131-3228 // Management of Socket Consumers
3329 consumersLk sync.RWMutex
3430 nextConsumerID uint64
···36323733 // Account cache
3834 accountCache *lru.Cache[string, *models.Account]
3535+3636+ HostPerDayLimiter *slidingwindow.Limiter
3937}
40384139type RelayConfig struct {
4040+ UserAgent string
4241 DefaultRepoLimit int64
4342 ConcurrencyPerHost int
4443 QueueDepthPerHost int
4544 LenientSyncValidation bool
4645 TrustedDomains []string
4646+ HostPerDayLimit int64
47474848 // If true, skip validation that messages for a given account (DID) are coming from the expected upstream host (PDS). Currently only used in tests; might be used for intermediate relays in the future.
4949 SkipAccountHostCheck bool
···5252func DefaultRelayConfig() *RelayConfig {
5353 // NOTE: many of these defaults are clobbered by CLI arguments
5454 return &RelayConfig{
5555+ UserAgent: "indigo-relay",
5556 DefaultRepoLimit: 100,
5657 ConcurrencyPerHost: 40,
5758 QueueDepthPerHost: 1000,
5959+ HostPerDayLimit: 50,
5860 }
5961}
6062···66686769 uc, _ := lru.New[string, *models.Account](2_000_000)
68706969- hc := NewHostClient("relay") // TODO: pass-through a user-agent from config?
7171+ hc := NewHostClient(config.UserAgent)
7272+7373+ // NOTE: discarded second argument is not an `error` type
70747175 r := &Relay{
7276 db: db,
···8084 consumers: make(map[uint64]*SocketConsumer),
81858286 accountCache: uc,
8787+8888+ HostPerDayLimiter: perDayLimiter(config.HostPerDayLimit),
8389 }
84908591 if err := r.MigrateDatabase(); err != nil {
+13-16
cmd/relay/relay/slurper.go
···66 "fmt"
77 "log/slog"
88 "math/rand"
99+ "net/http"
910 "sync"
1011 "sync/atomic"
1112 "time"
···3940 LimitMtx sync.RWMutex
4041 Limiters map[uint64]*Limiters
41424242- NewHostPerDayLimiter *slidingwindow.Limiter
4343-4443 shutdownChan chan bool
4544 shutdownResult chan error
4645···5453}
55545655type SlurperConfig struct {
5656+ UserAgent string
5757 DefaultPerSecondLimit int64
5858 DefaultPerHourLimit int64
5959 DefaultPerDayLimit int64
6060 DefaultRepoLimit int64
6161 ConcurrencyPerHost int
6262 QueueDepthPerHost int
6363- NewHostPerDayLimit int64
6463 PersistCursorPeriod time.Duration
6564 PersistCursorCallback PersistCursorFunc
6665 PersistHostStatusCallback PersistHostStatusFunc
···6968func DefaultSlurperConfig() *SlurperConfig {
7069 // NOTE: many of these defaults are overruled by DefaultRelayConfig, or even process CLI arg defaults
7170 return &SlurperConfig{
7272- NewHostPerDayLimit: 50,
7171+ UserAgent: "indigo-relay",
7372 DefaultPerSecondLimit: 50,
7473 DefaultPerHourLimit: 2500,
7574 DefaultPerDayLimit: 20_000,
···113112 logger = slog.Default()
114113 }
115114116116- // NOTE: discarded second argument is not an `error` type
117117- newHostPerDayLimiter, _ := slidingwindow.NewLimiter(time.Hour*24, config.NewHostPerDayLimit, windowFunc)
118118-119115 s := &Slurper{
120120- processCallback: processCallback,
121121- Config: config,
122122- subs: make(map[string]*Subscription),
123123- Limiters: make(map[uint64]*Limiters),
124124- shutdownChan: make(chan bool),
125125- shutdownResult: make(chan error),
126126- NewHostPerDayLimiter: newHostPerDayLimiter,
127127- logger: logger,
116116+ processCallback: processCallback,
117117+ Config: config,
118118+ subs: make(map[string]*Subscription),
119119+ Limiters: make(map[uint64]*Limiters),
120120+ shutdownChan: make(chan bool),
121121+ shutdownResult: make(chan error),
122122+ logger: logger,
128123 }
129124130125 // Start a goroutine to persist cursors (both periodically and and on shutdown)
···274269 if newHost {
275270 u = fmt.Sprintf("%s?cursor=%d", u, cursor)
276271 }
277277- conn, res, err := d.DialContext(ctx, u, nil)
272272+ hdr := make(http.Header)
273273+ hdr.Add("User-Agent", s.Config.UserAgent)
274274+ conn, res, err := d.DialContext(ctx, u, hdr)
278275 if err != nil {
279276 s.logger.Warn("dialing failed", "host", host.Hostname, "err", err, "backoff", backoff)
280277 time.Sleep(sleepForBackoff(backoff))