Skip to content
This repository has been archived by the owner on Jul 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #40 from jamesstocktonj1/feature/look-aside-cache
Browse files Browse the repository at this point in the history
Adding a Cache Driver
  • Loading branch information
madflojo authored Apr 2, 2024
2 parents aaba081 + e33c8ce commit 30f6009
Show file tree
Hide file tree
Showing 7 changed files with 1,507 additions and 4 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,59 @@ jobs:
run: /usr/local/go/bin/go test -v -race -covermode=atomic -coverprofile=coverage.out ./drivers/bbolt
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3

cache:
runs-on: ubuntu-latest
container: madflojo/ubuntu-build
services:
cassandra-primary:
image: madflojo/cassandra:latest
env:
CASSANDRA_KEYSPACE: hord
ports:
- 7000
- 7001
- 7199
- 9042
- 9160

cassandra:
image: madflojo/cassandra:latest
env:
CASSANDRA_SEEDS: cassandra-primary
CASSANDRA_KEYSPACE: hord
SLEEP_TIMER: 15
ports:
- 7000
- 7001
- 7199
- 9042
- 9160

redis:
image: bitnami/redis:latest
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
ALLOW_EMPTY_PASSWORD: yes

redis-sentinel:
image: bitnami/redis-sentinel:latest
env:
REDIS_URL: redis://redis:6379

steps:
- uses: actions/checkout@v3
# Using this instead of actions/setup-go to get around an issue with act
- name: Install Go
run: |
sleep 120
curl -L https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /usr/local -xzf -
- name: Execute Tests
run: /usr/local/go/bin/go test -v -race -covermode=atomic -coverprofile=coverage.out ./drivers/cache
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
162 changes: 162 additions & 0 deletions drivers/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
Package cache provides a Hord database driver for a variety of caching strategies. To use this driver, import it as follows:
import (
"github.com/madflojo/hord"
"github.com/madflojo/hord/cache"
)
# Connecting to the Database
Use the Dial() function to create a new client for interacting with the cache.
// Handle database connection
var database hord.Database
...
// Handle cache connection
var cache hord.Database
...
var db hord.Database
db, err := cache.Dial(cache.Config{
Database: database,
Cache: cache,
Type: cache.Lookaside,
})
if err != nil {
// Handle connection error
}
# Initialize database
Hord provides a Setup() function for preparing a database. This function is safe to execute after every Dial().
err := db.Setup()
if err != nil {
// Handle setup error
}
# Database Operations
Hord provides a simple abstraction for working with the cache, with easy-to-use methods such as Get() and Set() to read and write values.
// Handle database connection
var database hord.Database
database, err := cassandra.Dial(cassandra.Config{})
if err != nil {
// Handle connection error
}
// Handle cache connection
var cache hord.Database
cache, err := redis.Dial(redis.Config{})
if err != nil {
// Handle connection error
}
// Connect to the Cache database
db, err := cache.Dial(cache.Config{
Database: database,
Cache: cache,
Type: cache.Lookaside,
})
if err != nil {
// Handle connection error
}
err := db.Setup()
if err != nil {
// Handle setup error
}
// Set a value
err = db.Set("key", []byte("value"))
if err != nil {
// Handle error
}
// Retrieve a value
value, err := db.Get("key")
if err != nil {
// Handle error
}
*/
package cache

import (
"errors"

"github.com/madflojo/hord"
"github.com/madflojo/hord/drivers/cache/lookaside"
)

// CacheType is the type of cache to use.
type Type string

const (
Lookaside Type = "lookaside"
None Type = "none"
)

// Config provides the configuration options for the Cache driver.
type Config struct {
Type Type
Database hord.Database
Cache hord.Database
}

// NilCache is a nil cache driver that returns dial errors. It fixes the issue when the Dial function returns a nil hord.Database this prevents nil pointer errors.
type NilCache struct{}

var (
// ErrNoType is returned when the CacheType is invalid.
ErrNoType = errors.New("invalid CacheType")
)

// Dial will create a new Cache driver using the provided Config. It will return an error if either the Database or Cache values in Config are nil or if a CacheType is not specified.
func Dial(cfg Config) (hord.Database, error) {
if (cfg.Database == nil) || (cfg.Cache == nil) {
return &NilCache{}, hord.ErrInvalidDatabase
}

switch cfg.Type {
case Lookaside:
return lookaside.Dial(lookaside.Config{
Database: cfg.Database,
Cache: cfg.Cache,
})
case None:
return cfg.Database, nil
default:
return &NilCache{}, ErrNoType
}
}

func (nc *NilCache) Setup() error {
return hord.ErrNoDial
}

func (nc *NilCache) HealthCheck() error {
return hord.ErrNoDial
}

func (nc *NilCache) Get(_ string) ([]byte, error) {
return nil, hord.ErrNoDial
}

func (nc *NilCache) Set(_ string, _ []byte) error {
return hord.ErrNoDial
}

func (nc *NilCache) Delete(_ string) error {
return hord.ErrNoDial
}

func (nc *NilCache) Keys() ([]string, error) {
return nil, hord.ErrNoDial
}

func (nc *NilCache) Close() {

}
89 changes: 89 additions & 0 deletions drivers/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package cache

import (
"errors"
"testing"

"github.com/madflojo/hord"
"github.com/madflojo/hord/drivers/mock"
)

func TestDial(t *testing.T) {
unitTests := map[string]struct {
config Config
expectedError error
}{
"No Config": {
config: Config{},
expectedError: hord.ErrInvalidDatabase,
},
"No Database": {
config: Config{
Cache: &mock.Database{},
},
expectedError: hord.ErrInvalidDatabase,
},
"No Cache": {
config: Config{
Database: &mock.Database{},
},
expectedError: hord.ErrInvalidDatabase,
},
"Invalid Type": {
config: Config{
Type: "invalid",
Database: &mock.Database{},
Cache: &mock.Database{},
},
expectedError: ErrNoType,
},
"Type: Lookaside": {
config: Config{
Type: Lookaside,
Database: &mock.Database{},
Cache: &mock.Database{},
},
expectedError: nil,
},
"Type: None": {
config: Config{
Type: None,
Database: &mock.Database{},
Cache: &mock.Database{},
},
expectedError: nil,
},
}

for name, test := range unitTests {
t.Run(name, func(t *testing.T) {
_, err := Dial(test.config)
if !errors.Is(err, test.expectedError) {
t.Errorf("Dial(%v) returned error: %s, expected %s", test.config, err, test.expectedError)
}
})
}
}

func TestNilCache(t *testing.T) {
nc := &NilCache{}
if err := nc.Setup(); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.Setup() returned error: %s, expected %s", err, hord.ErrNoDial)
}
if err := nc.HealthCheck(); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.HealthCheck() returned error: %s, expected %s", err, hord.ErrNoDial)
}
if _, err := nc.Get(""); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.Get() returned error: %s, expected %s", err, hord.ErrNoDial)
}
if err := nc.Set("", nil); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.Set() returned error: %s, expected %s", err, hord.ErrNoDial)
}
if err := nc.Delete(""); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.Delete() returned error: %s, expected %s", err, hord.ErrNoDial)
}
if _, err := nc.Keys(); !errors.Is(err, hord.ErrNoDial) {
t.Errorf("NilCache.Keys() returned error: %s, expected %s", err, hord.ErrNoDial)
}
nc.Close()
}
Loading

0 comments on commit 30f6009

Please sign in to comment.