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
migmartri
left a comment
There was a problem hiding this comment.
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?
| environment: | ||
| - ENVIRONMENT=development | ||
| redis: | ||
| image: bitnami/redis |
There was a problem hiding this comment.
Can you use a tagged image?
| volumes: | ||
| - 'redis_data:/bitnami/redis' | ||
| ports: | ||
| - 6379:6379 |
There was a problem hiding this comment.
is this port needed to be exposed in the host?
There was a problem hiding this comment.
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).
| return yamlStruct.Repos, nil | ||
| } | ||
|
|
||
| func strToPtr(s string) *string { |
There was a problem hiding this comment.
are you sure this function does not exist somewhere else already?
There was a problem hiding this comment.
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.
| // 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.
returns a data.Repos object to manage repositories
Does it?
There was a problem hiding this comment.
er.. good catch ;). It did, but I moved to a static variable instead.
| 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.
This is interesting, why is this needed?
There was a problem hiding this comment.
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
| ) | ||
|
|
||
| // Pool is a pool of Zoom connections used by other packages | ||
| var Pool *zoom.Pool |
There was a problem hiding this comment.
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.
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.
| type Repo models.Repo | ||
|
|
||
| // ModelId returns the unique name of the Repo | ||
| func (r *Repo) ModelId() string { |
There was a problem hiding this comment.
are you sure you cannot name it ModelID (see https://github.com/golang/go/wiki/CodeReviewComments#initialisms) ?
There was a problem hiding this comment.
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
|
|
||
| // ModelId returns the unique name of the Repo | ||
| func (r *Repo) ModelId() string { | ||
| return *r.Name |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| // StrToPtr converts a string to a *string | ||
| func StrToPtr(s string) *string { |
There was a problem hiding this comment.
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.
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.
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
FooPtrfunctions 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
goimportsprogramming in Go would be such a pain.goimportswork 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 makesutila choice of last resort, because abusing it will eventually lead to having manyutilpackages 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 theutilname. Ah, and some people call itutils...utils.Foo + util.Bar
There was a problem hiding this comment.
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.
is this int64(..) really needed for readability? Constant are automatically interpreted to be of the required type.
There was a problem hiding this comment.
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.
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. |
| } | ||
|
|
||
| // CloseRedisPool closes a pool of Zoom connections | ||
| func CloseRedisPool() { |
There was a problem hiding this comment.
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.
I was considering adding a mutex here, but I agree that the easier option is to just not close the pool.
| currentConfig = Configuration{} | ||
| pool := GetRedisPool() | ||
| assert.NotNil(t, pool, "Redis Pool") | ||
| CloseRedisPool() |
There was a problem hiding this comment.
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.
Go allows to avoid repeating the name of the type:
[]models.Repo {
{
Name: ...,
URL: ...,
},
}
| Name: "stable", | ||
| URL: "http://storage.googleapis.com/kubernetes-charts", | ||
| repos := []models.Repo{ | ||
| models.Repo{ |
| func setupTestRepoCache(repos *[]models.Repo) { | ||
| if repos == nil { | ||
| repos = &[]models.Repo{ | ||
| models.Repo{ |
| // 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.
(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
| var once sync.Once | ||
|
|
||
| // NewCachedRepos sets up a Zoom collection of repositories | ||
| func NewCachedRepos(repos []models.Repo) { |
There was a problem hiding this comment.
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
| Repo{ | ||
| Name: "stable", | ||
| URL: "https://kubernetes-charts.storage.googleapis.com", | ||
| models.Repo{ |
There was a problem hiding this comment.
I thought we removed these unnecessary type names here
| if err != nil { | ||
| return nil, err | ||
| } | ||
| repos := []*data.Repo{} |
There was a problem hiding this comment.
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
| if err != nil { | ||
| return err | ||
| } | ||
| repos := []*data.Repo{} |
mkmik
left a comment
There was a problem hiding this comment.
ok, generally ok for me, modulo a couple of minor comments above