this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

automod: extracting some packages, and concurrency safety (#464)

A few minor hopefully-advancements to the automod/hepa packages:

- `countstore` is now extracted as a package.
- Synchronization primitives are added to MemCountStore. This fixes
crashes when running `hepa` without the use of Redis.
- Additional testing, to make sure that MemCountStore gets enough
exercise that the race detector has something to work on in tests.
- Attempted to sneak in some docs along the way!

authored by

Eric Myhre and committed by
GitHub
c54aff40 1c058927

+265 -156
+1
.gitignore
··· 33 33 /sonar-cli 34 34 /stress 35 35 /supercollider 36 + /hepa 36 37 37 38 # Don't ignore this file itself, or other specific dotfiles 38 39 !.gitignore
-93
automod/countstore.go
··· 1 - package automod 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "log/slog" 7 - "time" 8 - ) 9 - 10 - const ( 11 - PeriodTotal = "total" 12 - PeriodDay = "day" 13 - PeriodHour = "hour" 14 - ) 15 - 16 - type CountStore interface { 17 - GetCount(ctx context.Context, name, val, period string) (int, error) 18 - Increment(ctx context.Context, name, val string) error 19 - // TODO: batch increment method 20 - GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) 21 - IncrementDistinct(ctx context.Context, name, bucket, val string) error 22 - } 23 - 24 - // TODO: this implementation isn't race-safe (yet)! 25 - type MemCountStore struct { 26 - Counts map[string]int 27 - DistinctCounts map[string]map[string]bool 28 - } 29 - 30 - func NewMemCountStore() MemCountStore { 31 - return MemCountStore{ 32 - Counts: make(map[string]int), 33 - DistinctCounts: make(map[string]map[string]bool), 34 - } 35 - } 36 - 37 - func PeriodBucket(name, val, period string) string { 38 - switch period { 39 - case PeriodTotal: 40 - return fmt.Sprintf("%s/%s", name, val) 41 - case PeriodDay: 42 - t := time.Now().UTC().Format(time.DateOnly) 43 - return fmt.Sprintf("%s/%s/%s", name, val, t) 44 - case PeriodHour: 45 - t := time.Now().UTC().Format(time.RFC3339)[0:13] 46 - return fmt.Sprintf("%s/%s/%s", name, val, t) 47 - default: 48 - slog.Warn("unhandled counter period", "period", period) 49 - return fmt.Sprintf("%s/%s", name, val) 50 - } 51 - } 52 - 53 - func (s MemCountStore) GetCount(ctx context.Context, name, val, period string) (int, error) { 54 - v, ok := s.Counts[PeriodBucket(name, val, period)] 55 - if !ok { 56 - return 0, nil 57 - } 58 - return v, nil 59 - } 60 - 61 - func (s MemCountStore) Increment(ctx context.Context, name, val string) error { 62 - for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { 63 - k := PeriodBucket(name, val, p) 64 - v, ok := s.Counts[k] 65 - if !ok { 66 - v = 0 67 - } 68 - v = v + 1 69 - s.Counts[k] = v 70 - } 71 - return nil 72 - } 73 - 74 - func (s MemCountStore) GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) { 75 - v, ok := s.DistinctCounts[PeriodBucket(name, bucket, period)] 76 - if !ok { 77 - return 0, nil 78 - } 79 - return len(v), nil 80 - } 81 - 82 - func (s MemCountStore) IncrementDistinct(ctx context.Context, name, bucket, val string) error { 83 - for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { 84 - k := PeriodBucket(name, bucket, p) 85 - m, ok := s.DistinctCounts[k] 86 - if !ok { 87 - m = make(map[string]bool) 88 - } 89 - m[val] = true 90 - s.DistinctCounts[k] = m 91 - } 92 - return nil 93 - }
+66
automod/countstore/countstore.go
··· 1 + package countstore 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "time" 8 + ) 9 + 10 + const ( 11 + PeriodTotal = "total" 12 + PeriodDay = "day" 13 + PeriodHour = "hour" 14 + ) 15 + 16 + // CountStore is an interface for storing incrementing event counts, bucketed into periods. 17 + // It is implemented by MemCountStore and by RedisCountStore. 18 + // 19 + // Period bucketing works on the basis of the current date (as determined mid-call). 20 + // See the `Period*` consts for the available period types. 21 + // 22 + // The "GetCount" and "Increment" methods perform actual counting. 23 + // The "*Distinct" methods have a different behavior: 24 + // "IncrementDistinct" marks a value as seen at least once, 25 + // and "GetCountDistinct" asks how _many_ values have been seen at least once. 26 + // 27 + // Incrementing -- both the "Increment" and "IncrementDistinct" variants -- increases 28 + // a count in each supported period bucket size. 29 + // In other words, one call to CountStore.Increment causes three increments internally: 30 + // one to the count for the hour, one to the count for the day, and one to thte all-time count. 31 + // 32 + // The exact implementation and precision of the "*Distinct" methods may vary: 33 + // in the MemCountStore implementation, it is precise (it's based on large maps); 34 + // in the RedisCountStore implementation, it uses the Redis "pfcount" feature, 35 + // which is based on a HyperLogLog datastructure which has probablistic properties 36 + // (see https://redis.io/commands/pfcount/ ). 37 + // 38 + // Memory growth and availablity of information over time also varies by implementation. 39 + // The RedisCountStore implementation uses Redis's key expiration primitives; 40 + // only the all-time counts go without expiration. 41 + // The MemCountStore grows without bound (it's intended to be used in testing 42 + // and other non-production operations). 43 + // 44 + type CountStore interface { 45 + GetCount(ctx context.Context, name, val, period string) (int, error) 46 + Increment(ctx context.Context, name, val string) error 47 + // TODO: batch increment method 48 + GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) 49 + IncrementDistinct(ctx context.Context, name, bucket, val string) error 50 + } 51 + 52 + func periodBucket(name, val, period string) string { 53 + switch period { 54 + case PeriodTotal: 55 + return fmt.Sprintf("%s/%s", name, val) 56 + case PeriodDay: 57 + t := time.Now().UTC().Format(time.DateOnly) 58 + return fmt.Sprintf("%s/%s/%s", name, val, t) 59 + case PeriodHour: 60 + t := time.Now().UTC().Format(time.RFC3339)[0:13] 61 + return fmt.Sprintf("%s/%s/%s", name, val, t) 62 + default: 63 + slog.Warn("unhandled counter period", "period", period) 64 + return fmt.Sprintf("%s/%s", name, val) 65 + } 66 + }
+64
automod/countstore/countstore_mem.go
··· 1 + package countstore 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/puzpuzpuz/xsync/v3" 7 + ) 8 + 9 + type MemCountStore struct { 10 + // Counts is keyed by a string that is a munge of "{name}/{val}[/{period}]", 11 + // where period is either absent (meaning all-time total) 12 + // or a string describing that timeperiod (either "YYYY-MM-DD" or that plus a literal "T" and "HH"). 13 + // 14 + // (Using a values for `name` and `val` with slashes in them is perhaps inadvisable, as it may be ambiguous.) 15 + Counts *xsync.MapOf[string, int] 16 + DistinctCounts *xsync.MapOf[string, *xsync.MapOf[string, bool]] 17 + } 18 + 19 + func NewMemCountStore() MemCountStore { 20 + return MemCountStore{ 21 + Counts: xsync.NewMapOf[string, int](), 22 + DistinctCounts: xsync.NewMapOf[string, *xsync.MapOf[string, bool]](), 23 + } 24 + } 25 + 26 + func (s MemCountStore) GetCount(ctx context.Context, name, val, period string) (int, error) { 27 + v, ok := s.Counts.Load(periodBucket(name, val, period)) 28 + if !ok { 29 + return 0, nil 30 + } 31 + return v, nil 32 + } 33 + 34 + func (s MemCountStore) Increment(ctx context.Context, name, val string) error { 35 + for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { 36 + k := periodBucket(name, val, p) 37 + s.Counts.Compute(k, func(oldVal int, _ bool) (int, bool) { 38 + return oldVal+1, false 39 + }) 40 + } 41 + return nil 42 + } 43 + 44 + func (s MemCountStore) GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) { 45 + v, ok := s.DistinctCounts.Load(periodBucket(name, bucket, period)) 46 + if !ok { 47 + return 0, nil 48 + } 49 + return v.Size(), nil 50 + } 51 + 52 + func (s MemCountStore) IncrementDistinct(ctx context.Context, name, bucket, val string) error { 53 + for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { 54 + k := periodBucket(name, bucket, p) 55 + s.DistinctCounts.Compute(k,func(nested *xsync.MapOf[string, bool], _ bool) (*xsync.MapOf[string, bool], bool) { 56 + if nested == nil { 57 + nested = xsync.NewMapOf[string, bool]() 58 + } 59 + nested.Store(val, true) 60 + return nested, false 61 + }) 62 + } 63 + return nil 64 + }
+100
automod/countstore/countstore_test.go
··· 1 + package countstore 2 + 3 + import ( 4 + "context" 5 + "sync" 6 + "testing" 7 + "time" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestMemCountStoreBasics(t *testing.T) { 13 + assert := assert.New(t) 14 + ctx := context.Background() 15 + 16 + cs := NewMemCountStore() 17 + 18 + c, err := cs.GetCount(ctx, "test1", "val1", PeriodTotal) 19 + assert.NoError(err) 20 + assert.Equal(0, c) 21 + assert.NoError(cs.Increment(ctx, "test1", "val1")) 22 + assert.NoError(cs.Increment(ctx, "test1", "val1")) 23 + c, err = cs.GetCount(ctx, "test1", "val1", PeriodTotal) 24 + assert.NoError(err) 25 + assert.Equal(2, c) 26 + 27 + c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 28 + assert.NoError(err) 29 + assert.Equal(0, c) 30 + assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 31 + assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 32 + assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 33 + c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 34 + assert.NoError(err) 35 + assert.Equal(1, c) 36 + 37 + assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "two")) 38 + assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "three")) 39 + c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 40 + assert.NoError(err) 41 + assert.Equal(3, c) 42 + } 43 + 44 + func TestMemCountStoreConcurrent(t *testing.T) { 45 + assert := assert.New(t) 46 + ctx := context.Background() 47 + 48 + cs := NewMemCountStore() 49 + 50 + c, err := cs.GetCount(ctx, "test1", "val1", PeriodTotal) 51 + assert.NoError(err) 52 + assert.Equal(0, c) 53 + 54 + // Increment two different values from four different goroutines. 55 + // Read from two more (don't assert values; just that there's no error, 56 + // and no race (run this with `-race`!). 57 + // A short sleep ensures the scheduler is yielded to, so that order is decently random, 58 + // and reads are interleaved with writes. 59 + var wg sync.WaitGroup 60 + fnInc := func(name, val string, times int) { 61 + for i := 0; i < times; i++ { 62 + assert.NoError(cs.Increment(ctx, name, val)) 63 + assert.NoError(cs.IncrementDistinct(ctx, name, name, val)) 64 + time.Sleep(time.Nanosecond) 65 + } 66 + wg.Done() 67 + } 68 + fnRead := func(name, val string, times int) { 69 + for i := 0; i < times; i++ { 70 + _, err := cs.GetCount(ctx, name, val, PeriodTotal) 71 + assert.NoError(err) 72 + time.Sleep(time.Nanosecond) 73 + } 74 + } 75 + wg.Add(4) 76 + go fnInc("test1", "val1", 10) 77 + go fnInc("test1", "val1", 10) 78 + go fnRead("test1", "val1", 10) 79 + go fnInc("test2", "val2", 6) 80 + go fnInc("test2", "val2", 6) 81 + go fnRead("test2", "val2", 6) 82 + wg.Wait() 83 + 84 + // One final read for each value after all writer routines are collected. 85 + // This one should match a fixed value of the sum of all writes. 86 + c, err = cs.GetCount(ctx, "test1", "val1", PeriodTotal) 87 + assert.NoError(err) 88 + assert.Equal(20, c) 89 + c, err = cs.GetCount(ctx, "test2", "val2", PeriodTotal) 90 + assert.NoError(err) 91 + assert.Equal(12, c) 92 + 93 + // And what of distinct counts? Those should be 1. 94 + c, err = cs.GetCountDistinct(ctx, "test1", "test1", PeriodTotal) 95 + assert.NoError(err) 96 + assert.Equal(1, c) 97 + c, err = cs.GetCountDistinct(ctx, "test2", "test2", PeriodTotal) 98 + assert.NoError(err) 99 + assert.Equal(1, c) 100 + }
-40
automod/countstore_test.go
··· 1 - package automod 2 - 3 - import ( 4 - "context" 5 - "testing" 6 - 7 - "github.com/stretchr/testify/assert" 8 - ) 9 - 10 - func TestMemCountStoreBasics(t *testing.T) { 11 - assert := assert.New(t) 12 - ctx := context.Background() 13 - 14 - cs := NewMemCountStore() 15 - 16 - c, err := cs.GetCount(ctx, "test1", "val1", PeriodTotal) 17 - assert.NoError(err) 18 - assert.Equal(0, c) 19 - assert.NoError(cs.Increment(ctx, "test1", "val1")) 20 - assert.NoError(cs.Increment(ctx, "test1", "val1")) 21 - c, err = cs.GetCount(ctx, "test1", "val1", PeriodTotal) 22 - assert.NoError(err) 23 - assert.Equal(2, c) 24 - 25 - c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 26 - assert.NoError(err) 27 - assert.Equal(0, c) 28 - assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 29 - assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 30 - assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "one")) 31 - c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 32 - assert.NoError(err) 33 - assert.Equal(1, c) 34 - 35 - assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "two")) 36 - assert.NoError(cs.IncrementDistinct(ctx, "test2", "val2", "three")) 37 - c, err = cs.GetCountDistinct(ctx, "test2", "val2", PeriodTotal) 38 - assert.NoError(err) 39 - assert.Equal(3, c) 40 - }
+2 -1
automod/engine.go
··· 9 9 comatproto "github.com/bluesky-social/indigo/api/atproto" 10 10 "github.com/bluesky-social/indigo/atproto/identity" 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/bluesky-social/indigo/automod/countstore" 12 13 "github.com/bluesky-social/indigo/xrpc" 13 14 ) 14 15 ··· 19 20 Logger *slog.Logger 20 21 Directory identity.Directory 21 22 Rules RuleSet 22 - Counters CountStore 23 + Counters countstore.CountStore 23 24 Sets SetStore 24 25 Cache CacheStore 25 26 Flags FlagStore
+2 -1
automod/engine_test.go
··· 9 9 appbsky "github.com/bluesky-social/indigo/api/bsky" 10 10 "github.com/bluesky-social/indigo/atproto/identity" 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/bluesky-social/indigo/automod/countstore" 12 13 13 14 "github.com/stretchr/testify/assert" 14 15 ) ··· 54 55 engine := Engine{ 55 56 Logger: slog.Default(), 56 57 Directory: &dir, 57 - Counters: NewMemCountStore(), 58 + Counters: countstore.NewMemCountStore(), 58 59 Sets: sets, 59 60 Flags: flags, 60 61 Cache: cache,
+9 -9
automod/redis_counters.go automod/countstore/countstore_redis.go
··· 1 - package automod 1 + package countstore 2 2 3 3 import ( 4 4 "context" ··· 32 32 } 33 33 34 34 func (s *RedisCountStore) GetCount(ctx context.Context, name, val, period string) (int, error) { 35 - key := redisCountPrefix + PeriodBucket(name, val, period) 35 + key := redisCountPrefix + periodBucket(name, val, period) 36 36 c, err := s.Client.Get(ctx, key).Int() 37 37 if err == redis.Nil { 38 38 return 0, nil ··· 49 49 // increment multiple counters in a single redis round-trip 50 50 multi := s.Client.Pipeline() 51 51 52 - key = redisCountPrefix + PeriodBucket(name, val, PeriodHour) 52 + key = redisCountPrefix + periodBucket(name, val, PeriodHour) 53 53 multi.Incr(ctx, key) 54 54 multi.Expire(ctx, key, 2*time.Hour) 55 55 56 - key = redisCountPrefix + PeriodBucket(name, val, PeriodDay) 56 + key = redisCountPrefix + periodBucket(name, val, PeriodDay) 57 57 multi.Incr(ctx, key) 58 58 multi.Expire(ctx, key, 48*time.Hour) 59 59 60 - key = redisCountPrefix + PeriodBucket(name, val, PeriodTotal) 60 + key = redisCountPrefix + periodBucket(name, val, PeriodTotal) 61 61 multi.Incr(ctx, key) 62 62 // no expiration for total 63 63 ··· 66 66 } 67 67 68 68 func (s *RedisCountStore) GetCountDistinct(ctx context.Context, name, val, period string) (int, error) { 69 - key := redisDistinctPrefix + PeriodBucket(name, val, period) 69 + key := redisDistinctPrefix + periodBucket(name, val, period) 70 70 c, err := s.Client.PFCount(ctx, key).Result() 71 71 if err == redis.Nil { 72 72 return 0, nil ··· 83 83 // increment multiple counters in a single redis round-trip 84 84 multi := s.Client.Pipeline() 85 85 86 - key = redisDistinctPrefix + PeriodBucket(name, bucket, PeriodHour) 86 + key = redisDistinctPrefix + periodBucket(name, bucket, PeriodHour) 87 87 multi.PFAdd(ctx, key, val) 88 88 multi.Expire(ctx, key, 2*time.Hour) 89 89 90 - key = redisDistinctPrefix + PeriodBucket(name, bucket, PeriodDay) 90 + key = redisDistinctPrefix + periodBucket(name, bucket, PeriodDay) 91 91 multi.PFAdd(ctx, key, val) 92 92 multi.Expire(ctx, key, 48*time.Hour) 93 93 94 - key = redisDistinctPrefix + PeriodBucket(name, bucket, PeriodTotal) 94 + key = redisDistinctPrefix + periodBucket(name, bucket, PeriodTotal) 95 95 multi.PFAdd(ctx, key, val) 96 96 // no expiration for total 97 97
+2 -1
automod/rules/fixture_test.go
··· 7 7 "github.com/bluesky-social/indigo/atproto/identity" 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "github.com/bluesky-social/indigo/automod" 10 + "github.com/bluesky-social/indigo/automod/countstore" 10 11 "github.com/bluesky-social/indigo/xrpc" 11 12 ) 12 13 ··· 38 39 engine := automod.Engine{ 39 40 Logger: slog.Default(), 40 41 Directory: &dir, 41 - Counters: automod.NewMemCountStore(), 42 + Counters: countstore.NewMemCountStore(), 42 43 Sets: sets, 43 44 Flags: flags, 44 45 Cache: cache,
+3 -2
automod/rules/identity.go
··· 6 6 "time" 7 7 8 8 "github.com/bluesky-social/indigo/automod" 9 + "github.com/bluesky-social/indigo/automod/countstore" 9 10 ) 10 11 11 12 // triggers on first identity event for an account (DID) ··· 20 21 if age > 2*time.Hour { 21 22 return nil 22 23 } 23 - exists := evt.GetCount("acct/exists", did, automod.PeriodTotal) 24 + exists := evt.GetCount("acct/exists", did, countstore.PeriodTotal) 24 25 if exists == 0 { 25 26 evt.Logger.Info("new account") 26 27 evt.Increment("acct/exists", did) ··· 31 32 return nil 32 33 } 33 34 pdsHost := strings.ToLower(pdsURL.Host) 34 - existingAccounts := evt.GetCount("host/newacct", pdsHost, automod.PeriodTotal) 35 + existingAccounts := evt.GetCount("host/newacct", pdsHost, countstore.PeriodTotal) 35 36 evt.Increment("host/newacct", pdsHost) 36 37 37 38 // new PDS host
+5 -4
automod/rules/interaction.go
··· 2 2 3 3 import ( 4 4 "github.com/bluesky-social/indigo/automod" 5 + "github.com/bluesky-social/indigo/automod/countstore" 5 6 ) 6 7 7 8 var interactionDailyThreshold = 500 ··· 12 13 switch evt.Collection { 13 14 case "app.bsky.feed.like": 14 15 evt.Increment("like", did) 15 - created := evt.GetCount("like", did, automod.PeriodDay) 16 - deleted := evt.GetCount("unlike", did, automod.PeriodDay) 16 + created := evt.GetCount("like", did, countstore.PeriodDay) 17 + deleted := evt.GetCount("unlike", did, countstore.PeriodDay) 17 18 ratio := float64(deleted) / float64(created) 18 19 if created > interactionDailyThreshold && deleted > interactionDailyThreshold && ratio > 0.5 { 19 20 evt.Logger.Info("high-like-churn", "created-today", created, "deleted-today", deleted) ··· 21 22 } 22 23 case "app.bsky.graph.follow": 23 24 evt.Increment("follow", did) 24 - created := evt.GetCount("follow", did, automod.PeriodDay) 25 - deleted := evt.GetCount("unfollow", did, automod.PeriodDay) 25 + created := evt.GetCount("follow", did, countstore.PeriodDay) 26 + deleted := evt.GetCount("unfollow", did, countstore.PeriodDay) 26 27 ratio := float64(deleted) / float64(created) 27 28 if created > interactionDailyThreshold && deleted > interactionDailyThreshold && ratio > 0.5 { 28 29 evt.Logger.Info("high-follow-churn", "created-today", created, "deleted-today", deleted)
+2 -1
automod/rules/promo.go
··· 7 7 8 8 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 9 "github.com/bluesky-social/indigo/automod" 10 + "github.com/bluesky-social/indigo/automod/countstore" 10 11 ) 11 12 12 13 // looks for new accounts, with a commercial or donation link in profile, which directly reply to several accounts ··· 51 52 } 52 53 53 54 did := evt.Account.Identity.DID.String() 54 - uniqueReplies := evt.GetCountDistinct("reply-to", did, automod.PeriodDay) 55 + uniqueReplies := evt.GetCountDistinct("reply-to", did, countstore.PeriodDay) 55 56 if uniqueReplies >= 5 { 56 57 evt.AddAccountFlag("promo-multi-reply") 57 58 }
+2 -1
automod/rules/replies.go
··· 4 4 appbsky "github.com/bluesky-social/indigo/api/bsky" 5 5 "github.com/bluesky-social/indigo/atproto/syntax" 6 6 "github.com/bluesky-social/indigo/automod" 7 + "github.com/bluesky-social/indigo/automod/countstore" 7 8 ) 8 9 9 10 // does not count "self-replies" (direct to self, or in own post thread) ··· 13 14 } 14 15 15 16 did := evt.Account.Identity.DID.String() 16 - if evt.GetCount("reply", did, automod.PeriodDay) > 3 { 17 + if evt.GetCount("reply", did, countstore.PeriodDay) > 3 { 17 18 // TODO: disabled, too noisy for prod 18 19 //evt.AddAccountFlag("frequent-replier") 19 20 }
+4 -3
cmd/hepa/server.go
··· 12 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 13 "github.com/bluesky-social/indigo/atproto/identity" 14 14 "github.com/bluesky-social/indigo/automod" 15 + "github.com/bluesky-social/indigo/automod/countstore" 15 16 "github.com/bluesky-social/indigo/automod/rules" 16 17 "github.com/bluesky-social/indigo/util" 17 18 "github.com/bluesky-social/indigo/xrpc" ··· 86 87 } 87 88 } 88 89 89 - var counters automod.CountStore 90 + var counters countstore.CountStore 90 91 var cache automod.CacheStore 91 92 var flags automod.FlagStore 92 93 var rdb *redis.Client ··· 103 104 return nil, err 104 105 } 105 106 106 - cnt, err := automod.NewRedisCountStore(config.RedisURL) 107 + cnt, err := countstore.NewRedisCountStore(config.RedisURL) 107 108 if err != nil { 108 109 return nil, err 109 110 } ··· 121 122 } 122 123 flags = flg 123 124 } else { 124 - counters = automod.NewMemCountStore() 125 + counters = countstore.NewMemCountStore() 125 126 cache = automod.NewMemCacheStore(5_000, 30*time.Minute) 126 127 flags = automod.NewMemFlagStore() 127 128 }
+1
go.mod
··· 48 48 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f 49 49 github.com/prometheus/client_golang v1.17.0 50 50 github.com/prometheus/client_model v0.5.0 51 + github.com/puzpuzpuz/xsync/v3 v3.0.2 51 52 github.com/redis/go-redis/v9 v9.3.0 52 53 github.com/rivo/uniseg v0.1.0 53 54 github.com/samber/slog-echo v1.8.0
+2
go.sum
··· 563 563 github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= 564 564 github.com/prometheus/statsd_exporter v0.25.0 h1:gpVF1TMf1UqMJmBDpzBYrEaGOFMpbMBYYYUDwM38Y/I= 565 565 github.com/prometheus/statsd_exporter v0.25.0/go.mod h1:HwzfSvg6ehmb0Qg71ZuFrlgj5XQt9C+MGVLz5Gt5lqc= 566 + github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= 567 + github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 566 568 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 567 569 github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= 568 570 github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=