diff --git a/README.md b/README.md index e1b058d..080fa78 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,23 @@ You can set environment variables to configure the proxy before running it. |-----------------|-------------------------------------|------------------------| | `ORIGIN_URL` | The origin server URL to proxy | `http://dummyjson.com` | | `PROXY_PORT` | Port for the proxy server to listen | `1337` | +| `PROXY_CACHE` | Cache storage (redis/memory) | `memory` | +| `PROXY_TTL` | Cache TTL (seconds) | `60` | | `ZAP_ENV` | Logging environment (dev/prod) | `dev` | -| `REDIS_HOST` | Redis server host | `redis_proxio` | +| `REDIS_HOST` | Redis server host (used only if cache = redis) | `redis_proxio` | | `REDIS_PORT` | Redis server port | `6379` | | `REDIS_USERNAME` | Redis username | `proxio` | | `REDIS_PASSWORD` | Redis password | *(empty)* | | `REDIS_DATABASE` | Redis database number | `0` | +### Notes on Caching +- **Default cache storage is `memory`** +- When using memory cache, **you do not need a Redis instance** +- Switch to Redis by setting: +```env +PROXY_CACHE="redis" +``` + ## Run Proxio ```bash @@ -40,7 +50,11 @@ You can set environment variables to configure the proxy before running it. ./proxio # Or set custom variables inline -ORIGIN_URL="http://example.com" PROXY_PORT="8080" ./proxio +ORIGIN_URL="http://example.com" \ +PROXY_PORT="8080" \ +PROXY_CACHE="redis" \ +PROXY_TTL="120" \ +./proxio ``` Cached responses will be stored in Redis and served quickly on repeated requests. @@ -56,12 +70,20 @@ docker build -t proxio . docker run -d \ -e ORIGIN_URL="http://example.com" \ -e PROXY_PORT="8080" \ + -p 8080:8080 \ + proxio +``` + +If using redis: +```bash +docker run -d \ + -e PROXY_CACHE="redis" \ -e REDIS_HOST="redis" \ -p 8080:8080 \ proxio ``` -## Using Docker +## Using Container Registries ```bash # Pull from Docker Hub docker pull me3di/proxio:latest @@ -71,8 +93,6 @@ docker pull ghcr.io/pyr33x/proxio:latest # Or build locally docker build -t proxio . - -# Run with Docker docker run -d -p 8080:8080 ghcr.io/pyr33x/proxio:latest ``` @@ -82,6 +102,7 @@ Run with: ```bash docker compose up -d --build ``` +If `PROXY_CACHE=memory`, Redis won't be used even if it’s running. ## Contributing Feel free to open issues or submit pull requests. Any help is appreciated! diff --git a/internal/cache/cache.go b/internal/cache/cache.go index af6aad1..a483439 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -8,18 +8,17 @@ import ( "time" "github.com/pyr33x/proxio/pkg/err" - "github.com/redis/go-redis/v9" "go.uber.org/zap" ) -type Caching interface { - Get(ctx context.Context, key string) string - Put(ctx context.Context, key string, value any) error +type Store interface { + Get(ctx context.Context, key string) ([]byte, error) + Set(ctx context.Context, key string, value []byte, ttl time.Duration) error Clear(ctx context.Context) error } type Cache struct { - rdb *redis.Client + store Store logger *zap.Logger expiration time.Duration } @@ -30,11 +29,11 @@ type CacheValue struct { Body []byte } -func NewCacheRepository(rdb *redis.Client, logger *zap.Logger) *Cache { +func NewCacheRepository(store Store, logger *zap.Logger, ttl time.Duration) *Cache { return &Cache{ - rdb: rdb, + store: store, logger: logger, - expiration: 60 * time.Second, + expiration: ttl, } } @@ -44,7 +43,7 @@ func (c *Cache) Get(ctx context.Context, key string) (*CacheValue, bool) { return nil, false } - raw, err := c.rdb.Get(ctx, key).Bytes() + raw, err := c.store.Get(ctx, key) if err != nil { c.logger.Info("cache miss", zap.String("key", key), @@ -53,6 +52,12 @@ func (c *Cache) Get(ctx context.Context, key string) (*CacheValue, bool) { return nil, false } + // raw is nil (key doesn't exist) + if raw == nil { + c.logger.Info("cache miss", zap.String("key", key), zap.String("state", "MISS")) + return nil, false + } + var val CacheValue if err := json.Unmarshal(raw, &val); err != nil { return nil, false @@ -76,8 +81,7 @@ func (c *Cache) Put(ctx context.Context, key string, value CacheValue) error { return err } - err = c.rdb.Set(ctx, key, b, c.expiration).Err() - if err != nil { + if err := c.store.Set(ctx, key, b, c.expiration); err != nil { c.logger.Error("failed to write to cache", zap.String("key", key), zap.Duration("expiration", c.expiration), @@ -90,5 +94,5 @@ func (c *Cache) Put(ctx context.Context, key string, value CacheValue) error { } func (c *Cache) Clear(ctx context.Context) error { - return c.rdb.FlushAll(ctx).Err() + return c.store.Clear(ctx) } diff --git a/internal/cache/memory.go b/internal/cache/memory.go new file mode 100644 index 0000000..91ce45a --- /dev/null +++ b/internal/cache/memory.go @@ -0,0 +1,58 @@ +package cache + +import ( + "context" + "sync" + "time" +) + +type memoryEntry struct { + value []byte + expiration time.Time +} + +type memoryStore struct { + data sync.Map +} + +func NewMemoryStore() *memoryStore { + return &memoryStore{} +} + +func (m *memoryStore) Get(ctx context.Context, key string) ([]byte, error) { + raw, ok := m.data.Load(key) + if !ok { + return nil, nil + } + + entry := raw.(memoryEntry) + + if !entry.expiration.IsZero() && time.Now().After(entry.expiration) { + m.data.Delete(key) + return nil, nil + } + + return entry.value, nil +} + +func (m *memoryStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { + var exp time.Time + if ttl > 0 { + exp = time.Now().Add(ttl) + } + + m.data.Store(key, memoryEntry{ + value: value, + expiration: exp, + }) + + return nil +} + +func (m *memoryStore) Clear(ctx context.Context) error { + m.data.Range(func(key, _ any) bool { + m.data.Delete(key) + return true + }) + return nil +} diff --git a/internal/cache/redis.go b/internal/cache/redis.go new file mode 100644 index 0000000..e6b06ae --- /dev/null +++ b/internal/cache/redis.go @@ -0,0 +1,28 @@ +package cache + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +type redisStore struct { + client *redis.Client +} + +func NewRedisStore(client *redis.Client) *redisStore { + return &redisStore{client: client} +} + +func (r *redisStore) Get(ctx context.Context, key string) ([]byte, error) { + return r.client.Get(ctx, key).Bytes() +} + +func (r *redisStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { + return r.client.Set(ctx, key, value, ttl).Err() +} + +func (r *redisStore) Clear(ctx context.Context) error { + return r.client.FlushDB(ctx).Err() +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index aa0c804..13ea02a 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -27,7 +27,19 @@ type OriginServer struct { } func NewProxyServer(ctx context.Context, cfg *config.Config, logger *zap.Logger) *http.Server { - rdb := redis.New(ctx, &cfg.Redis, logger).GetClient() + var cacheStorage cache.Store + + if cfg.Server.Proxy.Cache == "redis" { + adapter := redis.New(ctx, &cfg.Redis, logger) + if adapter == nil { + logger.Warn("failed to connect to redis, falling back to memory store") + cacheStorage = cache.NewMemoryStore() + } else { + cacheStorage = cache.NewRedisStore(adapter.GetClient()) + } + } else { + cacheStorage = cache.NewMemoryStore() + } srv := &Server{ Proxy: ProxyServer{ @@ -36,7 +48,7 @@ func NewProxyServer(ctx context.Context, cfg *config.Config, logger *zap.Logger) Origin: OriginServer{ URL: cfg.Server.Origin.URL, }, - Cache: cache.NewCacheRepository(rdb, logger), + Cache: cache.NewCacheRepository(cacheStorage, logger, time.Duration(cfg.Server.Proxy.TTL)*time.Second), logger: logger, } diff --git a/pkg/config/config.go b/pkg/config/config.go index ad4250f..e8fabdd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -18,7 +18,9 @@ type Origin struct { } type Proxy struct { - Port string + Port string + Cache string + TTL int64 } type Zap struct { @@ -44,7 +46,9 @@ func New() *Config { URL: envy.GetString("ORIGIN_URL", "http://dummyjson.com"), }, Proxy: Proxy{ - Port: envy.GetString("PROXY_PORT", "1337"), + Port: envy.GetString("PROXY_PORT", "1337"), + Cache: envy.GetString("PROXY_CACHE", "memory"), + TTL: envy.GetInt64("PROXY_TTL", 60), // in seconds }, }, Zap: Zap{