-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: implemented caching and etag support for Go #6985
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: main
Are you sure you want to change the base?
Changes from all commits
375f4c5
49decc1
24b8e70
2d19d0d
3843857
f02ee47
febd719
336c925
ae5f61c
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 |
|---|---|---|
|
|
@@ -5,11 +5,15 @@ import ( | |
| "errors" | ||
| "time" | ||
|
|
||
| "github.com/jackc/pgx/v5" | ||
| "github.com/redis/go-redis/v9" | ||
|
|
||
| "github.com/infisical/api/internal/database/pg" | ||
| ) | ||
|
|
||
| // KeyStore provides key-value operations backed by Redis. | ||
| // KeyStore provides key-value operations backed by Redis and PostgreSQL. | ||
| type KeyStore interface { | ||
| // Redis operations | ||
| SetItem(ctx context.Context, key string, value string) error | ||
| GetItem(ctx context.Context, key string) (string, error) | ||
| SetExpiry(ctx context.Context, key string, expiry time.Duration) (bool, error) | ||
|
|
@@ -19,62 +23,123 @@ type KeyStore interface { | |
| DeleteItems(ctx context.Context, keys []string) (int64, error) | ||
| IncrementBy(ctx context.Context, key string, value int64) (int64, error) | ||
|
|
||
| // IncrementByWithExpiry atomically increments a key and sets its expiry. | ||
| // If the key doesn't exist, it's created with value 0 before incrementing. | ||
| IncrementByWithExpiry(ctx context.Context, key string, value int64, expiry time.Duration) (int64, error) | ||
|
|
||
| // HashGet returns the value of a field in a hash (HGET). | ||
| // Returns empty string if key or field doesn't exist. | ||
| HashGet(ctx context.Context, key, field string) (string, error) | ||
|
|
||
| // HashSet sets a field in a hash (HSET). | ||
| HashSet(ctx context.Context, key, field, value string) error | ||
|
|
||
| // StreamAdd adds an entry to a Redis stream (XADD). | ||
| // Pass "*" as id to auto-generate the entry ID. | ||
| StreamAdd(ctx context.Context, stream string, id string, values map[string]string) (string, error) | ||
|
|
||
| // PostgreSQL key_value_store operations | ||
|
|
||
| // PgGetIntItem returns the integer value for a key from the PostgreSQL key_value_store table. | ||
| // Returns 0 if key doesn't exist or is expired. | ||
| PgGetIntItem(ctx context.Context, key string) (int64, error) | ||
| } | ||
|
|
||
| type redisKeyStore struct { | ||
| client redis.UniversalClient | ||
| type keyStore struct { | ||
| redis redis.UniversalClient | ||
| db pg.DB | ||
| } | ||
|
|
||
| func NewKeyStore(client redis.UniversalClient) KeyStore { | ||
| return &redisKeyStore{client: client} | ||
| func NewKeyStore(redisClient redis.UniversalClient, db pg.DB) KeyStore { | ||
| return &keyStore{redis: redisClient, db: db} | ||
| } | ||
|
|
||
| func (k *redisKeyStore) SetItem(ctx context.Context, key, value string) error { | ||
| return k.client.Set(ctx, key, value, 0).Err() | ||
| func (k *keyStore) SetItem(ctx context.Context, key, value string) error { | ||
| return k.redis.Set(ctx, key, value, 0).Err() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) GetItem(ctx context.Context, key string) (string, error) { | ||
| val, err := k.client.Get(ctx, key).Result() | ||
| func (k *keyStore) GetItem(ctx context.Context, key string) (string, error) { | ||
| val, err := k.redis.Get(ctx, key).Result() | ||
| if errors.Is(err, redis.Nil) { | ||
| return "", nil | ||
| } | ||
| return val, err | ||
| } | ||
|
|
||
| func (k *redisKeyStore) SetExpiry(ctx context.Context, key string, expiry time.Duration) (bool, error) { | ||
| return k.client.Expire(ctx, key, expiry).Result() | ||
| func (k *keyStore) SetExpiry(ctx context.Context, key string, expiry time.Duration) (bool, error) { | ||
| return k.redis.Expire(ctx, key, expiry).Result() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) SetItemWithExpiry(ctx context.Context, key string, expiry time.Duration, value string) error { | ||
| return k.client.Set(ctx, key, value, expiry).Err() | ||
| func (k *keyStore) SetItemWithExpiry(ctx context.Context, key string, expiry time.Duration, value string) error { | ||
| return k.redis.Set(ctx, key, value, expiry).Err() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) SetItemWithExpiryNX(ctx context.Context, key string, expiry time.Duration, value string) (bool, error) { | ||
| return k.client.SetNX(ctx, key, value, expiry).Result() | ||
| func (k *keyStore) SetItemWithExpiryNX(ctx context.Context, key string, expiry time.Duration, value string) (bool, error) { | ||
| return k.redis.SetNX(ctx, key, value, expiry).Result() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) DeleteItem(ctx context.Context, key string) (int64, error) { | ||
| return k.client.Del(ctx, key).Result() | ||
| func (k *keyStore) DeleteItem(ctx context.Context, key string) (int64, error) { | ||
| return k.redis.Del(ctx, key).Result() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) DeleteItems(ctx context.Context, keys []string) (int64, error) { | ||
| func (k *keyStore) DeleteItems(ctx context.Context, keys []string) (int64, error) { | ||
| if len(keys) == 0 { | ||
| return 0, nil | ||
| } | ||
| return k.client.Del(ctx, keys...).Result() | ||
| return k.redis.Del(ctx, keys...).Result() | ||
| } | ||
|
|
||
| func (k *keyStore) IncrementBy(ctx context.Context, key string, value int64) (int64, error) { | ||
| return k.redis.IncrBy(ctx, key, value).Result() | ||
| } | ||
|
|
||
| func (k *keyStore) IncrementByWithExpiry(ctx context.Context, key string, value int64, expiry time.Duration) (int64, error) { | ||
| pipe := k.redis.TxPipeline() | ||
| incrCmd := pipe.IncrBy(ctx, key, value) | ||
| pipe.Expire(ctx, key, expiry) | ||
| _, err := pipe.Exec(ctx) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return incrCmd.Val(), nil | ||
| } | ||
|
|
||
| func (k *redisKeyStore) IncrementBy(ctx context.Context, key string, value int64) (int64, error) { | ||
| return k.client.IncrBy(ctx, key, value).Result() | ||
| func (k *keyStore) HashGet(ctx context.Context, key, field string) (string, error) { | ||
| val, err := k.redis.HGet(ctx, key, field).Result() | ||
| if errors.Is(err, redis.Nil) { | ||
| return "", nil | ||
| } | ||
| return val, err | ||
| } | ||
|
|
||
| func (k *keyStore) HashSet(ctx context.Context, key, field, value string) error { | ||
| return k.redis.HSet(ctx, key, field, value).Err() | ||
| } | ||
|
|
||
| func (k *redisKeyStore) StreamAdd(ctx context.Context, stream, id string, values map[string]string) (string, error) { | ||
| return k.client.XAdd(ctx, &redis.XAddArgs{ | ||
| func (k *keyStore) StreamAdd(ctx context.Context, stream, id string, values map[string]string) (string, error) { | ||
| return k.redis.XAdd(ctx, &redis.XAddArgs{ | ||
| Stream: stream, | ||
| ID: id, | ||
| Values: values, | ||
| }).Result() | ||
| } | ||
|
|
||
| func (k *keyStore) PgGetIntItem(ctx context.Context, key string) (int64, error) { | ||
| var integerValue *int64 | ||
| err := k.db.Replica().QueryRow(ctx, ` | ||
| SELECT "integerValue" | ||
| FROM key_value_store | ||
| WHERE key = @key | ||
| AND ("expiresAt" IS NULL OR "expiresAt" > NOW()) | ||
|
Comment on lines
+129
to
+133
Contributor
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. question: one pattern that I see a lot on our knex queries is that if we receive a transaction we hit the primary database instead of the replica. I saw the usage of this and it seems taht we are always hitting the replica ( |
||
| `, pgx.NamedArgs{"key": key}).Scan(&integerValue) | ||
| if err != nil { | ||
| if errors.Is(err, pgx.ErrNoRows) { | ||
| return 0, nil | ||
| } | ||
| return 0, err | ||
| } | ||
| if integerValue == nil { | ||
| return 0, nil | ||
| } | ||
| return *integerValue, nil | ||
| } | ||
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.
nit: Here we define db, but this is actually Postgres right? Maybe we could have it as
postgresinstead ofdb, since the interface supports any db, so if in the future we need another db I believe it is more straightforwardThere 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.
Not sure about another database. I used
pg.DBbecause of two reasons.pg.Postgres