Skip to content

Commit

Permalink
refactor cache to use an interface
Browse files Browse the repository at this point in the history
  • Loading branch information
circa10a committed Jul 13, 2024
1 parent 21c6d94 commit 7cec550
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 71 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ By default, the library will use an in-memory cache that will be used to reduce

### Persistent

If you need a persistent cache to live outside of your application, [Redis](https://redis.io/) is supported by this library. To have the library cache address proximity using a Redis instance, simply provide a `redis.RedisOptions` struct to `geofence.Config.RedisOptions`. If `RedisOptions` is configured, the in-memory cache will not be used.
If you need a persistent cache to live outside of your application, [Redis](https://redis.io/) is supported by this library. To have the library cache address proximity using a Redis instance, simply provide a `geofence.RedisOptions` struct to `geofence.Config.RedisOptions`. If `RedisOptions` is configured, the in-memory cache will not be used.

> Note: Only Redis 7 is currently supported at the time of this writing.
Expand All @@ -87,7 +87,7 @@ func main() {
AllowPrivateIPAddresses: true,
CacheTTL: 7 * (24 * time.Hour), // 1 week
// Use Redis for caching
RedisOptions: &redis.Options{
RedisOptions: &geofence.RedisOptions{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
Expand Down
11 changes: 11 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cache

import (
"context"
)

// Cache is an interface for caching ip addresses
type Cache interface {
Get(context.Context, string) (bool, bool, error)
Set(context.Context, string, bool) error
}
42 changes: 42 additions & 0 deletions cache/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cache

import (
"context"
"time"

gocache "github.com/patrickmn/go-cache"
)

const (
deleteExpiredCacheItemsInternal = 10 * time.Minute
)

type MemoryCache struct {
memoryClient *gocache.Cache
memoryOptions *MemoryOptions
}

// MemoryOptions holds in memory cache configuration parameters.
type MemoryOptions struct {
TTL time.Duration
}

func NewMemoryCache(memoryOptions *MemoryOptions) *MemoryCache {
return &MemoryCache{
memoryClient: gocache.New(memoryOptions.TTL, deleteExpiredCacheItemsInternal),
memoryOptions: memoryOptions,
}
}

func (m *MemoryCache) Get(ctx context.Context, key string) (bool, bool, error) {
if isIPAddressNear, found := m.memoryClient.Get(key); found {
return isIPAddressNear.(bool), found, nil
} else {
return false, false, nil
}
}

func (m *MemoryCache) Set(ctx context.Context, key string, value bool) error {
m.memoryClient.Set(key, value, m.memoryOptions.TTL)
return nil
}
55 changes: 55 additions & 0 deletions cache/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cache

import (
"context"
"strconv"
"time"

"github.com/go-redis/redis/v9"
)

type RedisCache struct {
redisClient *redis.Client
redisOptions *RedisOptions
}

// RedisOptions holds redis configuration parameters.
type RedisOptions struct {
Addr string
Password string
DB int
TTL time.Duration
}

func NewRedisCache(redisOpts *RedisOptions) *RedisCache {
return &RedisCache{
redisClient: redis.NewClient(&redis.Options{
Addr: redisOpts.Addr,
Password: redisOpts.Password,
DB: redisOpts.DB,
}),
redisOptions: redisOpts,
}
}

func (r *RedisCache) Get(ctx context.Context, key string) (bool, bool, error) {
val, err := r.redisClient.Get(ctx, key).Result()
if err != nil {
// If key is not in redis
if err == redis.Nil {
return false, false, nil
}
return false, false, err
}
isIPAddressNear, err := strconv.ParseBool(val)
if err != nil {
return false, false, err
}

return isIPAddressNear, true, nil
}

func (r *RedisCache) Set(ctx context.Context, key string, value bool) error {
// Redis stores false as 0 for whatever reason, so we'll store as a string and parse out in cacheGet
return r.redisClient.Set(ctx, key, strconv.FormatBool(value), r.redisOptions.TTL).Err()
}
88 changes: 19 additions & 69 deletions geofence.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package geofence

import (
"fmt"
"errors"
"net"
"strconv"
"time"

"github.com/EpicStep/go-simple-geo/v2/geo"
"github.com/go-redis/redis/v9"
"github.com/circa10a/go-geofence/cache"
"github.com/go-resty/resty/v2"
"github.com/patrickmn/go-cache"
"golang.org/x/net/context"
)

Expand All @@ -21,7 +19,7 @@ const (

// Config holds the user configuration to setup a new geofence
type Config struct {
RedisOptions *redis.Options
RedisOptions *cache.RedisOptions
IPAddress string
Token string
Radius float64
Expand All @@ -31,13 +29,12 @@ type Config struct {

// Geofence holds a ipbase.com client, redis client, in-memory cache and user supplied config
type Geofence struct {
cache *cache.Cache
cache cache.Cache
ipbaseClient *resty.Client
redisClient *redis.Client
ctx context.Context
Config
Latitude float64
Longitude float64
Config Config
Latitude float64
Longitude float64
}

// ipBaseResponse is the json response from ipbase.com
Expand Down Expand Up @@ -79,6 +76,7 @@ type languages struct {
Name string `json:"name"`
NameNative string `json:"name_native"`
}

type country struct {
Alpha2 string `json:"alpha2"`
Alpha3 string `json:"alpha3"`
Expand Down Expand Up @@ -134,10 +132,7 @@ func (e *IPBaseError) Error() string {
}

// ErrInvalidIPAddress is the error raised when an invalid IP address is provided
var ErrInvalidIPAddress = fmt.Errorf("invalid IP address provided")

// ErrCacheNotConfigured is the error raised when the cache was not set up correctly
var ErrCacheNotConfigured = fmt.Errorf("cache no configured")
var ErrInvalidIPAddress = errors.New("invalid IP address provided")

// validateIPAddress ensures valid ip address
func validateIPAddress(ipAddress string) error {
Expand All @@ -154,7 +149,7 @@ func (g *Geofence) getIPGeoData(ipAddress string) (*ipbaseResponse, error) {

resp, err := g.ipbaseClient.R().
SetHeader("Accept", "application/json").
SetQueryParam("apikey", g.Token).
SetQueryParam("apikey", g.Config.Token).
SetQueryParam("ip", ipAddress).
SetResult(response).
SetError(ipbaseError).
Expand Down Expand Up @@ -188,9 +183,12 @@ func New(c *Config) (*Geofence, error) {
// Set up redis client if options are provided
// Else we create a local in-memory cache
if c.RedisOptions != nil {
geofence.redisClient = redis.NewClient(c.RedisOptions)
c.RedisOptions.TTL = c.CacheTTL
geofence.cache = cache.NewRedisCache(c.RedisOptions)
} else {
geofence.cache = cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal)
geofence.cache = cache.NewMemoryCache(&cache.MemoryOptions{
TTL: c.CacheTTL,
})
}

// Get current location of specified IP address
Expand All @@ -216,15 +214,15 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) {
return false, err
}

if g.AllowPrivateIPAddresses {
if g.Config.AllowPrivateIPAddresses {
ip := net.ParseIP(ipAddress)
if ip.IsPrivate() || ip.IsLoopback() {
return true, nil
}
}

// Check if ipaddress has been looked up before and is in cache
isIPAddressNear, found, err := g.cacheGet(ipAddress)
isIPAddressNear, found, err := g.cache.Get(g.ctx, ipAddress)
if err != nil {
return false, err
}
Expand All @@ -247,60 +245,12 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) {

// Compare coordinates
// Distance must be less than or equal to the configured radius to be near
isNear := distance <= g.Radius
isNear := distance <= g.Config.Radius

err = g.cacheSet(ipAddress, isNear)
err = g.cache.Set(g.ctx, ipAddress, isNear)
if err != nil {
return false, err
}

return isNear, nil
}

func (g *Geofence) cacheGet(ipAddress string) (bool, bool, error) {
// Use redis if configured
if g.redisClient != nil {
val, err := g.redisClient.Get(g.ctx, ipAddress).Result()
if err != nil {
// If key is not in redis
if err == redis.Nil {
return false, false, nil
}
return false, false, err
}
isIPAddressNear, err := strconv.ParseBool(val)
if err != nil {
return false, false, err
}
return isIPAddressNear, true, nil
}

// Use in memory cache if configured
if g.cache != nil {
if isIPAddressNear, found := g.cache.Get(ipAddress); found {
return isIPAddressNear.(bool), found, nil
} else {
return false, false, nil
}
}

return false, false, ErrCacheNotConfigured
}

func (g *Geofence) cacheSet(ipAddress string, isNear bool) error {
// Use redis if configured
if g.redisClient != nil {
// Redis stores false as 0 for whatever reason, so we'll store as a string and parse out in cacheGet
err := g.redisClient.Set(g.ctx, ipAddress, strconv.FormatBool(isNear), g.Config.CacheTTL).Err()
if err != nil {
return err
}
}

// Use in memory cache if configured
if g.cache != nil {
g.cache.Set(ipAddress, isNear, g.Config.CacheTTL)
}

return nil
}

0 comments on commit 7cec550

Please sign in to comment.