In part 2 I described an in-memory implementation of the Feature Store.

An in-memory implementation is useful for proving the abstraction, it allowed me to implement the API, but, clearly it’s not useful for production services.

Looking again at the interface, there are four different methods that need to be implemented.

type FeatureStore interface {
	AddFeature(resource, id, feature string) error
	GetFeaturesForResource(resource, id string) ([]string, error)
	HasFeature(resource, id, feature string) (bool, error)
	RemoveFeature(resource, id, feature string) error
}

This kind of storage is fairly trivial to implement in Redis, and starting with tests this is quick and easy to implement.

package redis

import (
	"reflect"
	"testing"

	"github.com/garyburd/redigo/redis"
	"github.com/soveran/redisurl"
)

func TestHasFeatureWithNoFeature(t *testing.T) {
	pool, cleanup := getRedisPool(t)
	defer cleanup()
	store := NewRedisFeatureStore(pool)

	ok, err := store.HasFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")

	if err != nil {
		t.Fatal(err)
	}
	if ok {
		t.Fatalf("checking for feature failed: got %v, wanted %v", ok, false)
	}

}

func TestAddFeature(t *testing.T) {
	pool, cleanup := getRedisPool(t)
	defer cleanup()
	store := NewRedisFeatureStore(pool)

	err := store.AddFeature("pipeline", "8e674e29-a848-44a1-a26b-95aaadb96111", "review-apps-2")

	if err != nil {
		t.Fatal(err)
	}
}

func getRedisPool(t *testing.T) (*redis.Pool, func()) {
	pool := &redis.Pool{
		Dial: func() (redis.Conn, error) {
			conn, err := redisurl.ConnectToURL("redis://localhost:6379/9")
			if err != nil {
				t.Fatal(err)
			}
			return conn, nil
		},
		MaxIdle:   1,
		MaxActive: 5,
	}

	return pool, func() {
		c := pool.Get()
		c.Do("FLUSHDB")
		pool.Close()
	}
}

The getRedisPool function returns a Pool, and a closure, which can be used to clean up the test database in-between tests.

The actual implementation is fairly trivial based on the tests…

package redis

import (
	"fmt"

	"github.com/garyburd/redigo/redis"
)

type RedisFeatureStore struct {
	pool *redis.Pool
}

func NewRedisFeatureStore(p *redis.Pool) *RedisFeatureStore {
	return &RedisFeatureStore{pool: p}
}

func (rff *RedisFeatureStore) HasFeature(resource, id, feature string) (bool, error) {
	conn := rff.pool.Get()
	defer conn.Close()

	return redis.Bool(conn.Do("SISMEMBER", resourceKey(resource, id), feature))

}

func (rff *RedisFeatureStore) AddFeature(resource, id, feature string) error {
	conn := rff.pool.Get()
	defer conn.Close()

	_, err := conn.Do("SADD", resourceKey(resource, id), feature)
	return err
}

func resourceKey(resource, id string) string {
	return fmt.Sprintf("%s:%s", resource, id)
}

This uses Redis Sets to manage the set of features associated with resources, the code is fairly simple, grabbing a connection from the Redis pool, and executing commands against Redis.