-
Notifications
You must be signed in to change notification settings - Fork 220
Move repo store from in memory to redis #333
Conversation
prydonius
commented
Aug 7, 2017
- Adds Zoom Redis library
- Adds collection for Repos that is backed by Redis
- Removes config.Repo struct and moves to using generated models.Repo
- Adds Zoom Redis library - Adds collection for Repos that is backed by Redis - Removes config.Repo struct and moves to using generated models.Repo
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.
What's the reason for moving repositories to redis? Are we persisting its content across deployments? it seems that configure monocular loads the repos in config on every boot? cache.NewCachedRepos(conf.Repos)
Are we planning on storing the charts as well?
docker-compose.yml
Outdated
@@ -22,3 +22,14 @@ services: | |||
- $HOME/.kube/:/root/.kube | |||
environment: | |||
- ENVIRONMENT=development | |||
redis: | |||
image: bitnami/redis |
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.
Can you use a tagged image?
volumes: | ||
- 'redis_data:/bitnami/redis' | ||
ports: | ||
- 6379:6379 |
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.
is this port needed to be exposed in the host?
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.
I'm exposing it because I was testing through my editor so needed to connect from my host, I think it's useful for that especially given testing locally in Go is very common.
(on a side note, I've been considering whether we should remove Docker Compose entirely and move to Minikube with host mounts and something like telepresence.io for the Go service).
src/api/config/repos/repos.go
Outdated
@@ -69,3 +63,7 @@ func loadReposFromFile(filePath string) (Repos, error) { | |||
} | |||
return yamlStruct.Repos, nil | |||
} | |||
|
|||
func strToPtr(s string) *string { |
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.
are you sure this function does not exist somewhere else already?
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.
It is in the handlers package, which imports repos so it can't be imported here. I'll move this to a new util package that only holds basic functions like this.
src/api/data/cache/repos.go
Outdated
// Repos is a Zoom Collection for the Repo model | ||
var Repos *zoom.Collection | ||
|
||
// NewCachedRepos returns a data.Repos object to manage repositories |
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.
returns a data.Repos object to manage repositories
Does it?
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.
er.. good catch ;). It did, but I moved to a static variable instead.
@@ -8,7 +8,7 @@ REPO_PATH := github.com/kubernetes-helm/${SHORT_NAME} | |||
DEV_ENV_IMAGE := quay.io/deis/go-dev:v0.22.0 | |||
SWAGGER_IMAGE := quay.io/goswagger/swagger:0.6.0 | |||
DEV_ENV_WORK_DIR := /go/src/${REPO_PATH}/src/api | |||
DEV_ENV_PREFIX := docker run --rm -e GO15VENDOREXPERIMENT=1 -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} | |||
DEV_ENV_PREFIX := docker run --rm -e GO15VENDOREXPERIMENT=1 -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} --net=host |
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.
This is interesting, why is this needed?
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.
Rather than change the Makefile to start the test using Docker Compose, I opted to keep this as is and instead make sure a Redis server is running locally. In Travis, I enabled the Redis server and locally I am exporting 6379 from the container. Perhaps this is a bit lazy, and what I should do is change the Makefile to use Compose. What do you think?
This will lay the groundwork for adding repos at runtime via the UI (see #257), each Monocular instance needs a single source of truth for what repos exist. It uses the config to bootstrap the repos, yes - and actually currently it will add them back if they are deleted at runtime. We might want to change that, but I think it is fine for now. I consider the repos in the config file the "default repos" that should always be configured when starting up - which is useful in a CI/CD environment where you don't want to have to manually configure repos in the UI every time.
I think this is the next step, yes. Though we may need to consider the points brought up in #251 (comment) |
Codecov Report
@@ Coverage Diff @@
## master #333 +/- ##
==========================================
+ Coverage 88.99% 89.98% +0.99%
==========================================
Files 15 18 +3
Lines 618 769 +151
==========================================
+ Hits 550 692 +142
- Misses 45 48 +3
- Partials 23 29 +6
Continue to review full report at Codecov.
|
e0d6c0d
to
3e30598
Compare
src/api/config/cache.go
Outdated
) | ||
|
||
// Pool is a pool of Zoom connections used by other packages | ||
var Pool *zoom.Pool |
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.
so we have a global pool instance that starts uninitialized and gets set every time you call NewRedisPool
?
hm, does look a bit surprising given the fact that the constructor is NewRedisPool
which is usually how you call functions that create something and return it instead of also having side effects.
Furthermore, the write to the global variable is not protected by a mutex. This will thus fail if you call NewRedisPool
and access Pool
from different goroutines (it can be easily shown with building the code
/ running the tests with the -race
flag).
An simple improvement would be to just make this explicit: InitPool
, and possibly also a DestroyPool
that will nil
the global variable after closing the pool, so that tests cannot accidentally run with a closed pool if they forget to properly initialize the pool.
Another improvement would be to provide an accessor GetPool
or Pool()
that accesses the global variable protected by a sync.Mutex
(or better, by a sync.RWMutex
).
Obviously, avoiding global state would be an alternative, but in Go globals are generally less frowned upon, but that doesn't mean that we have to make them racy.
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.
Thanks @mmikulicic, this is definitely the part I was most concerned about with this PR so thanks for the feedback! I agree that InitPool
is a better name, and I will move to using an accessor with a mutex.
src/api/data/repos.go
Outdated
type Repo models.Repo | ||
|
||
// ModelId returns the unique name of the Repo | ||
func (r *Repo) ModelId() string { |
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.
are you sure you cannot name it ModelID
(see https://github.com/golang/go/wiki/CodeReviewComments#initialisms) ?
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.
Yeah, linter complained about it too, but unfortunately this is how they are named in Zoom https://github.com/albrow/zoom#what-is-a-model
src/api/data/repos.go
Outdated
|
||
// ModelId returns the unique name of the Repo | ||
func (r *Repo) ModelId() string { | ||
return *r.Name |
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.
are you sure you want to crash if r.Name
is uninitialized?
An option is to return e.g. "<nil>"
if nil; useful so that you don't crash the app you're debugging.
src/api/data/util/util.go
Outdated
} | ||
|
||
// StrToPtr converts a string to a *string | ||
func StrToPtr(s string) *string { |
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.
Nitpick StrToPtrOfWhat?
: obviously of str! hence StrToStrPtr
, or perhaps justStrPtr
?
or turning this around: why don't we just call the package pointerto
, and have pointerto.String
, pointerto.Int64
?
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.
Whilst creating a package is nice, I can see util
having more useful shared functions in the future, so I'll rename this to StrPtr
.
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.
creating packages is cheap.
util
is a particularly bad name for a package, for a few reasons:
-
by definition, it means that the bunch of thing you put in there has not much in common besides being generally useful. The family of
FooPtr
functions here have a lot in common: they take a Foo and return a *Foo, for the purpose of taking the address of a literal which is not allowed by language spec. -
without
goimports
programming in Go would be such a pain.goimports
work by assuming that there is signal in the combination of packagename+function signature. It doesn't work well when a lot of different packages have both the same name and same functions (like the often happens with the various log libraries) and obviously it doesn't work well you need code from multiple packages that are named in the same way (something that usually doesn't happen with the aforementioned log libraries). This makesutil
a choice of last resort, because abusing it will eventually lead to having manyutil
packages spread in your code base and in the libraries you depend on and thus a kitten will die every time you'll have to manually import or manually set an alias of an import and/or rename a bunch of package aliases in a code snippet that you're moving around the codebase just because of a conflict with theutil
name. Ah, and some people call itutils
...utils.Foo + util.Bar
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.
Fair point, you've convinced me - I'll go for creating a pointerto
package, I like that it's more readable to. Thanks!
assert.Equal(t, w.Code, http.StatusBadRequest, "expect a 400 response code") | ||
var httpBody models.Error | ||
assert.NoErr(t, testutil.ErrorModelFromJSON(w.Body, &httpBody)) | ||
assert.Equal(t, *httpBody.Code, int64(400), "response code in HTTP body data") |
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.
is this int64(..)
really needed for readability? Constant are automatically interpreted to be of the required type.
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.
This appeared to be failing because it was interpreting 400
as an int
and not equating it to *httpBody.Code
, but I'll check again.
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.
ah sure, I missed the fact that assert.Equal
accepts interface{}
. Yeah, the revolt of the helpers...
uses sync.Once to ensure pool and collection is only initialized once
@mmikulicic made all the requested changes - PTAL! r.e. miniredis, I will move to that in a separate PR to avoid adding more to this one. |
src/api/config/cache.go
Outdated
} | ||
|
||
// CloseRedisPool closes a pool of Zoom connections | ||
func CloseRedisPool() { |
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.
this is not threadsafe.
the sync.Once
provides a very simple pattern on top of a mutex but it can only do one thing.
if you also want to safely mutate the pool variable (by setting it to nil) you need to deal with that mutex explicitly.
Another option is to just call close and let it be called multiple times and ignore the error.
Another option again is to just not care about closing the pool, since there is no provision here for reopening it again later (since the once
is not reset)
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.
I was considering adding a mutex here, but I agree that the easier option is to just not close the pool.
src/api/config/cache_test.go
Outdated
currentConfig = Configuration{} | ||
pool := GetRedisPool() | ||
assert.NotNil(t, pool, "Redis Pool") | ||
CloseRedisPool() |
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.
doing this here means that the moment you add another test into this package it won't work, because closing the pool will not reset the once
and thus subsequent calls to GetRedisPool will just return nil
.
Name: "waps", | ||
URL: "./localhost", | ||
repos := []models.Repo{ | ||
models.Repo{ |
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.
Go allows to avoid repeating the name of the type:
[]models.Repo {
{
Name: ...,
URL: ...,
},
}
src/api/data/cache/cache_test.go
Outdated
Name: "stable", | ||
URL: "http://storage.googleapis.com/kubernetes-charts", | ||
repos := []models.Repo{ | ||
models.Repo{ |
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.
same here
src/api/data/cache/cache_test.go
Outdated
func setupTestRepoCache(repos *[]models.Repo) { | ||
if repos == nil { | ||
repos = &[]models.Repo{ | ||
models.Repo{ |
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.
same here
src/api/data/cache/repos.go
Outdated
// NewCachedRepos sets up a Zoom collection of repositories | ||
func NewCachedRepos(repos []models.Repo) { | ||
log.Info("setting up Repos collection") | ||
var err error |
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.
(general comment, not longer relevant if you follow my suggestion above):
if you don't use this err outside of the closure passed to once.Do then please declare it inside of the closure below
src/api/data/cache/repos.go
Outdated
var once sync.Once | ||
|
||
// NewCachedRepos sets up a Zoom collection of repositories | ||
func NewCachedRepos(repos []models.Repo) { |
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.
This is racy since the Repos
global could be access from multiple goroutines in mixed read and write mode.
Furthermore I don't really like deciding here how we want to fail.
Let's rewrite it as:
var (
reposSingleton *zoom.Collection
once sync.Once
)
func UpdateCache(repos []models.Repo) error {
repos, err := GetRepos()
if err != nil {
return err
}
for _, r := range repos {
// Convert to Zoom model
repo := data.Repo(r)
err = repos.Save(&repo)
if err != nil {
return err
}
}
return nil
}
func GetRepos() (*zoom.Collection, error) {
var err error
once.Do(func() {
reposSingleton, err = config.GetRedisPool().NewCollectionWithOptions(&data.Repo{}, zoom.DefaultCollectionOptions.WithIndex(true))
})
return reposSingleton, err
}
There's a bug with the API responses because |
yeah, lazy initialization is dangerous |
allows helpers package to use the repos collection
src/api/config/repos/repos.go
Outdated
Repo{ | ||
Name: "stable", | ||
URL: "https://kubernetes-charts.storage.googleapis.com", | ||
models.Repo{ |
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.
I thought we removed these unnecessary type names here
src/api/data/cache/cache.go
Outdated
if err != nil { | ||
return nil, err | ||
} | ||
repos := []*data.Repo{} |
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.
I personally prefer to use:
var repos []*data.Repo
in this case, since it better conveys the idea that it will get filled by FindAll
src/api/data/cache/cache.go
Outdated
if err != nil { | ||
return err | ||
} | ||
repos := []*data.Repo{} |
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.
same here
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.
ok, generally ok for me, modulo a couple of minor comments above