From 2689e6e447d2c9a7507deb953990f3329de8c50d Mon Sep 17 00:00:00 2001 From: Jusshersmith Date: Thu, 25 Apr 2019 18:54:49 +0100 Subject: [PATCH 1/2] new group cache package and cache provider creation of an on-demand group cache package, and a new 'groupcache' provider wrapper with tests for both. --- internal/auth/options.go | 17 +- internal/auth/providers/group_cache.go | 125 +++++++++++++ internal/auth/providers/group_cache_test.go | 164 ++++++++++++++++++ internal/auth/providers/okta.go | 6 +- internal/auth/providers/provider_default.go | 6 + internal/auth/providers/providers.go | 2 + .../auth/providers/singleflight_middleware.go | 9 +- internal/auth/providers/test_provider.go | 6 + internal/pkg/groups/localcache.go | 92 ++++++++++ internal/pkg/groups/localcache_test.go | 70 ++++++++ 10 files changed, 481 insertions(+), 16 deletions(-) create mode 100644 internal/auth/providers/group_cache.go create mode 100644 internal/auth/providers/group_cache_test.go create mode 100644 internal/pkg/groups/localcache.go create mode 100644 internal/pkg/groups/localcache_test.go diff --git a/internal/auth/options.go b/internal/auth/options.go index 9cae0f6a..6d3c0a68 100644 --- a/internal/auth/options.go +++ b/internal/auth/options.go @@ -38,6 +38,8 @@ import ( // CookieHTTPOnly - bool - set httponly cookie flag // RequestTimeout - duration - overall request timeout // AuthCodeSecret - string - the seed string for secure auth codes (optionally base64 encoded) +// GroupCacheProviderTTL - time.Duration - cache TTL for the group-cache provider used for on-demand group caching +// GroupsCacheRefreshTTL - time.Duratoin - cache TTL for the groups fillcache mechanism used to preemptively fill group caches // PassHostHeader - bool - pass the request Host Header to upstream (default true) // SkipProviderButton - bool - if true, will skip sign-in-page to directly reach the next step: oauth/start // PassUserHeaders - bool (default true) - pass X-Forwarded-User and X-Forwarded-Email information to upstream @@ -89,6 +91,7 @@ type Options struct { AuthCodeSecret string `envconfig:"AUTH_CODE_SECRET"` + GroupCacheProviderTTL time.Duration `envconfig:"GROUP_CACHE_PROVIDER_TTL" default:"10m"` GroupsCacheRefreshTTL time.Duration `envconfig:"GROUPS_CACHE_REFRESH_TTL" default:"10m"` SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL" default:"720h"` @@ -301,7 +304,10 @@ func newProvider(o *Options) (providers.Provider, error) { if err != nil { return nil, err } - singleFlightProvider = providers.NewSingleFlightProvider(oktaProvider) + tags := []string{"provider:okta"} + + groupsCache := providers.NewGroupCache(oktaProvider, o.GroupCacheProviderTTL, oktaProvider.StatsdClient, tags) + singleFlightProvider = providers.NewSingleFlightProvider(groupsCache) default: return nil, fmt.Errorf("unimplemented provider: %q", o.Provider) } @@ -334,14 +340,7 @@ func AssignStatsdClient(opts *Options) func(*Authenticator) error { "statsd client is running") proxy.StatsdClient = StatsdClient - switch v := proxy.provider.(type) { - case *providers.GoogleProvider: - v.SetStatsdClient(StatsdClient) - case *providers.SingleFlightProvider: - v.AssignStatsdClient(StatsdClient) - default: - logger.Info("provider does not have statsd client") - } + proxy.provider.SetStatsdClient(StatsdClient) return nil } } diff --git a/internal/auth/providers/group_cache.go b/internal/auth/providers/group_cache.go new file mode 100644 index 00000000..2d46eb7f --- /dev/null +++ b/internal/auth/providers/group_cache.go @@ -0,0 +1,125 @@ +package providers + +import ( + "sort" + "strings" + "time" + + "github.com/buzzfeed/sso/internal/pkg/groups" + "github.com/buzzfeed/sso/internal/pkg/sessions" + "github.com/datadog/datadog-go/statsd" +) + +var ( + // This is a compile-time check to make sure our types correctly implement the interface: + // https://medium.com/@matryer/golang-tip-compile-time-checks-to-ensure-your-type-satisfies-an-interface-c167afed3aae + _ Provider = &GroupCache{} +) + +type Cache interface { + Get(key groups.CacheKey) (groups.CacheEntry, bool) + Set(key groups.CacheKey, val groups.CacheEntry) + Purge(key groups.CacheKey) +} + +// GroupCache is designed to act as a provider while wrapping subsequent provider's functions, +// while also offering a caching mechanism (specifically used for group caching at the moment). +type GroupCache struct { + statsdClient *statsd.Client + provider Provider + cache Cache +} + +// NewGroupCache returns a new GroupCache (which includes a LocalCache from the groups package) +func NewGroupCache(provider Provider, ttl time.Duration, statsdClient *statsd.Client, tags []string) *GroupCache { + return &GroupCache{ + statsdClient: statsdClient, + provider: provider, + cache: groups.NewLocalCache(ttl, statsdClient, tags), + } +} + +// SetStatsdClient calls the provider's SetStatsdClient function. +func (p *GroupCache) SetStatsdClient(statsdClient *statsd.Client) { + p.statsdClient = statsdClient + p.provider.SetStatsdClient(statsdClient) +} + +// Data returns the provider Data +func (p *GroupCache) Data() *ProviderData { + return p.provider.Data() +} + +// Redeem wraps the provider's Redeem function +func (p *GroupCache) Redeem(redirectURL, code string) (*sessions.SessionState, error) { + return p.provider.Redeem(redirectURL, code) +} + +// ValidateSessionState wraps the provider's ValidateSessionState function. +func (p *GroupCache) ValidateSessionState(s *sessions.SessionState) bool { + return p.provider.ValidateSessionState(s) +} + +// GetSignInURL wraps the provider's GetSignInURL function. +func (p *GroupCache) GetSignInURL(redirectURI, finalRedirect string) string { + return p.provider.GetSignInURL(redirectURI, finalRedirect) +} + +// RefreshSessionIfNeeded wraps the provider's RefreshSessionIfNeeded function. +func (p *GroupCache) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) { + return p.provider.RefreshSessionIfNeeded(s) +} + +// ValidateGroupMembership wraps the provider's ValidateGroupMembership around calls to check local cache for group membership information. +func (p *GroupCache) ValidateGroupMembership(email string, allowedGroups []string, accessToken string) ([]string, error) { + // Create a cache key and check to see if it's in the cache. If not, call the provider's + // ValidateGroupMembership function and cache the result. + sort.Strings(allowedGroups) + key := groups.CacheKey{ + Email: email, + AllowedGroups: strings.Join(allowedGroups, ","), + } + + val, ok := p.cache.Get(key) + if ok { + p.statsdClient.Incr("provider.groupcache", + []string{ + "action:ValidateGroupMembership", + "cache:hit", + }, 1.0) + return val.ValidGroups, nil + } + + // The key isn't in the cache, so pass the call on to the subsequent provider + p.statsdClient.Incr("provider.groupcache", + []string{ + "action:ValidateGroupMembership", + "cache:miss", + }, 1.0) + + validGroups, err := p.provider.ValidateGroupMembership(email, allowedGroups, accessToken) + if err != nil { + return nil, err + } + + entry := groups.CacheEntry{ + ValidGroups: validGroups, + } + p.cache.Set(key, entry) + return validGroups, nil +} + +// Revoke wraps the provider's Revoke function. +func (p *GroupCache) Revoke(s *sessions.SessionState) error { + return p.provider.Revoke(s) +} + +// RefreshAccessToken wraps the provider's RefreshAccessToken function. +func (p *GroupCache) RefreshAccessToken(refreshToken string) (string, time.Duration, error) { + return p.provider.RefreshAccessToken(refreshToken) +} + +// Stop calls the providers stop function. +func (p *GroupCache) Stop() { + p.provider.Stop() +} diff --git a/internal/auth/providers/group_cache_test.go b/internal/auth/providers/group_cache_test.go new file mode 100644 index 00000000..079aaee4 --- /dev/null +++ b/internal/auth/providers/group_cache_test.go @@ -0,0 +1,164 @@ +package providers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" + + "github.com/buzzfeed/sso/internal/pkg/groups" + "github.com/buzzfeed/sso/internal/pkg/testutil" + "github.com/datadog/datadog-go/statsd" +) + +func newTestProviderServer(body []byte, code int) (*url.URL, *httptest.Server) { + s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(code) + rw.Write(body) + })) + u, _ := url.Parse(s.URL) + return u, s +} + +// We define an Okta provider because the cache being tested here is currently +// only used by the Okta provider. +func newTestProvider(providerData *ProviderData, t *testing.T) *OktaProvider { + if providerData == nil { + providerData = &ProviderData{ + ProviderName: "", + ClientID: "", + ClientSecret: "", + SignInURL: &url.URL{}, + RedeemURL: &url.URL{}, + RevokeURL: &url.URL{}, + ProfileURL: &url.URL{}, + ValidateURL: &url.URL{}, + Scope: ""} + } + provider, err := NewOktaProvider(providerData, "test.okta.com", "default") + if err != nil { + t.Fatalf("new okta provider returns unexpected error: %q", err) + } + return provider +} + +func TestCachedGroupsAreNotUsed(t *testing.T) { + + type serverResp struct { + Groups []string `json:"groups"` + } + + //set up the test server + provider := newTestProvider(nil, t) + resp := serverResp{ + Groups: []string{"allowedGroup1", "allowedGroup2"}, + } + body, err := json.Marshal(resp) + testutil.Equal(t, nil, err) + var server *httptest.Server + provider.ProfileURL, server = newTestProviderServer(body, http.StatusOK) + defer server.Close() + + // set up the cache + ttl := time.Millisecond * 10 + statsdClient, _ := statsd.New("127.0.0.1:8125") + tags := []string{"tags:test"} + GroupsCache := NewGroupCache(provider, ttl, statsdClient, tags) + + // The below cached `MatchedGroups` should not be returned because the list of + // allowed groups we pass in are different to the cached `AllowedGroups`. It should instead + // make a call to the Provider (our test server). + cacheKey := groups.CacheKey{ + Email: "email@test.com", + AllowedGroups: "allowedGroup1", + } + cacheData := groups.CacheEntry{ + ValidGroups: []string{"allowedGroup1", "allowedGroup2", "allowedGroup3"}, + } + GroupsCache.cache.Set(cacheKey, cacheData) + + // If the groups stored in the `serverResp` struct are returned, it means the + // cache was skipped because the allowedGroups that we pass in are different to + // those stored in the cache. This is the outcome we expect in this test. + email := "email@test.com" + actualAllowedGroups := []string{"allowedGroup2", "allowedGroup1"} + accessToken := "123456" + actualMatchedGroups, err := GroupsCache.ValidateGroupMembership(email, actualAllowedGroups, accessToken) + if err != nil { + t.Fatalf("unexpected error caused while validating group membership: %q", err) + } + if !reflect.DeepEqual(actualMatchedGroups, actualAllowedGroups) { + t.Logf("expected groups to match: %q", actualAllowedGroups) + t.Logf(" actual groups returned: %q", actualMatchedGroups) + if reflect.DeepEqual(actualMatchedGroups, cacheData.ValidGroups) { + t.Fatalf("It looks like the groups in the cache were returned. In this case, the cache should have been skipped") + } + t.Fatalf("Unexpected groups returned.") + } + + // We want to test that the groups returned are *now* cached, so we change the resp + // that the test server will send. If it matches this new response, we know the cache + // was skipped, which we do not expect to happen. + resp = serverResp{ + Groups: []string{"allowedGroup1", "allowedGroup2"}, + } + body, err = json.Marshal(resp) + testutil.Equal(t, nil, err) + provider.ProfileURL, server = newTestProviderServer(body, http.StatusOK) + defer server.Close() + + actualMatchedGroups, err = GroupsCache.ValidateGroupMembership(email, actualAllowedGroups, accessToken) + if err != nil { + t.Fatalf("unexpected error caused while validating group membership: %q", err) + } + if !reflect.DeepEqual(actualMatchedGroups, actualAllowedGroups) { + t.Logf("expected groups to match: %q", actualAllowedGroups) + t.Logf(" actual groups returned: %q", actualMatchedGroups) + if reflect.DeepEqual(actualMatchedGroups, resp.Groups) { + t.Fatalf("It looks like the cache was skipped, and the provider was called. In this case, the cache should have been used") + } + } + +} + +// maybe we can skip this test, as the above test technically tests for the same thing, but just in a slightly more obfuscated way. +func TestCachedGroupsAreUsed(t *testing.T) { + provider := newTestProvider(nil, t) + + // set up the cache + ttl := time.Millisecond * 10 + statsdClient, _ := statsd.New("127.0.0.1:8125") + tags := []string{"tags:test"} + GroupsCache := NewGroupCache(provider, ttl, statsdClient, tags) + + // In this case, the below `MatchedGroups` should be returned because the list of + // allowed groups are pass in match them. + cacheKey := groups.CacheKey{ + Email: "email@test.com", + AllowedGroups: "allowedGroup1,allowedGroup2,allowedGroup3", + } + cacheData := groups.CacheEntry{ + ValidGroups: []string{"allowedGroup1", "allowedGroup2", "allowedGroup3"}, + } + GroupsCache.cache.Set(cacheKey, cacheData) + + // We haven't set up a test server in this case because we pass in a list of allowed groups + // that match the allowed groups in the cache, so the cached matched groups should be used + // If the cache is skipped, an error will probably be returned when it tries to call the + // provider endpoint. + email := "email@test.com" + actualAllowedGroups := []string{"allowedGroup1", "allowedGroup2", "allowedGroup3"} + accessToken := "123456" + actualMatchedGroups, err := GroupsCache.ValidateGroupMembership(email, actualAllowedGroups, accessToken) + if err != nil { + t.Fatalf("unexpected error caused while validating group membership: %q", err) + } + if !reflect.DeepEqual(actualMatchedGroups, actualAllowedGroups) { + t.Logf("expected groups to match: %q", actualAllowedGroups) + t.Logf(" actual groups returned: %q", actualMatchedGroups) + t.Fatalf("unexpected groups returned") + } +} diff --git a/internal/auth/providers/okta.go b/internal/auth/providers/okta.go index aa3733aa..d8062d56 100644 --- a/internal/auth/providers/okta.go +++ b/internal/auth/providers/okta.go @@ -101,6 +101,11 @@ func NewOktaProvider(p *ProviderData, OrgURL, providerServerID string) (*OktaPro return oktaProvider, nil } +// Sets the providers StatsdClient +func (p *OktaProvider) SetStatsdClient(statsdClient *statsd.Client) { + p.StatsdClient = statsdClient +} + // ValidateSessionState attempts to validate the session state's access token. func (p *OktaProvider) ValidateSessionState(s *sessions.SessionState) bool { if s.AccessToken == "" { @@ -323,7 +328,6 @@ func (p *OktaProvider) ValidateGroupMembership(email string, allowedGroups []str if len(allowedGroups) == 0 { return []string{}, nil } - userinfo, err := p.GetUserProfile(accessToken) if err != nil { return nil, err diff --git a/internal/auth/providers/provider_default.go b/internal/auth/providers/provider_default.go index 49e068bb..83fb899e 100644 --- a/internal/auth/providers/provider_default.go +++ b/internal/auth/providers/provider_default.go @@ -12,8 +12,14 @@ import ( log "github.com/buzzfeed/sso/internal/pkg/logging" "github.com/buzzfeed/sso/internal/pkg/sessions" + "github.com/datadog/datadog-go/statsd" ) +// SetStatsdClient fulfills the Provider interface +func (p *ProviderData) SetStatsdClient(*statsd.Client) { + return +} + // Redeem takes in a redirect url and code and calls the redeem url endpoint, returning a session state if a valid // access token is redeemed. func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) { diff --git a/internal/auth/providers/providers.go b/internal/auth/providers/providers.go index 54f1b234..f69583dd 100644 --- a/internal/auth/providers/providers.go +++ b/internal/auth/providers/providers.go @@ -5,6 +5,7 @@ import ( "time" "github.com/buzzfeed/sso/internal/pkg/sessions" + "github.com/datadog/datadog-go/statsd" ) var ( @@ -36,6 +37,7 @@ const ( // Provider is an interface exposing functions necessary to authenticate with a given provider. type Provider interface { + SetStatsdClient(*statsd.Client) Data() *ProviderData Redeem(string, string) (*sessions.SessionState, error) ValidateSessionState(*sessions.SessionState) bool diff --git a/internal/auth/providers/singleflight_middleware.go b/internal/auth/providers/singleflight_middleware.go index 7492ca1e..9b7d3a58 100644 --- a/internal/auth/providers/singleflight_middleware.go +++ b/internal/auth/providers/singleflight_middleware.go @@ -59,13 +59,10 @@ func (p *SingleFlightProvider) do(endpoint, key string, fn func() (interface{}, return resp, err } -// AssignStatsdClient adds a statsd client to the provider if possible. -func (p *SingleFlightProvider) AssignStatsdClient(StatsdClient *statsd.Client) { +// SetStatsdClient calls the provider's SetStatsdClient function. +func (p *SingleFlightProvider) SetStatsdClient(StatsdClient *statsd.Client) { p.StatsdClient = StatsdClient - switch v := p.provider.(type) { - case *GoogleProvider: - v.SetStatsdClient(StatsdClient) - } + p.provider.SetStatsdClient(StatsdClient) } // Data returns the provider data diff --git a/internal/auth/providers/test_provider.go b/internal/auth/providers/test_provider.go index 62746e7f..0c8a5ec6 100644 --- a/internal/auth/providers/test_provider.go +++ b/internal/auth/providers/test_provider.go @@ -5,6 +5,7 @@ import ( "time" "github.com/buzzfeed/sso/internal/pkg/sessions" + "github.com/datadog/datadog-go/statsd" ) // TestProvider is a test implementation of the Provider interface. @@ -50,6 +51,11 @@ func NewTestProvider(providerURL *url.URL) *TestProvider { } } +// SetStatsdClient fulfills the Provider interface +func (tp *TestProvider) SetStatsdClient(*statsd.Client) { + return +} + // ValidateSessionState returns the mock provider's ValidToken field value. func (tp *TestProvider) ValidateSessionState(*sessions.SessionState) bool { return tp.ValidToken diff --git a/internal/pkg/groups/localcache.go b/internal/pkg/groups/localcache.go new file mode 100644 index 00000000..4ec77e4e --- /dev/null +++ b/internal/pkg/groups/localcache.go @@ -0,0 +1,92 @@ +package groups + +import ( + "time" + + "github.com/datadog/datadog-go/statsd" + "golang.org/x/sync/syncmap" +) + +// NewLocalCache returns a LocalCache instance +func NewLocalCache( + ttl time.Duration, + statsdClient *statsd.Client, + tags []string, +) *LocalCache { + return &LocalCache{ + ttl: ttl, + localCacheData: &syncmap.Map{}, + metrics: statsdClient, + tags: tags, + } +} + +type LocalCache struct { + // Cache configuration + ttl time.Duration + metrics *statsd.Client + tags []string + + // Cache data + localCacheData *syncmap.Map +} + +// Cachekey defines the key used to store the data in the cache. +type CacheKey struct { + Email string + AllowedGroups string +} + +// CacheEntry defines the data we want to store in the cache. +type CacheEntry struct { + ValidGroups []string +} + +// get will attempt to retrieve an entry from the cache at the given key +func (lc *LocalCache) get(key CacheKey) (CacheEntry, bool) { + val, ok := lc.localCacheData.Load(key) + if ok { + return val.(CacheEntry), ok + } + + return CacheEntry{}, false +} + +// set will attempt to set an entry in the cache to a given key +// for the prescribed TTL +func (lc *LocalCache) set(key CacheKey, data CacheEntry) { + lc.localCacheData.Store(key, data) + + // Spawn the TTL cleanup goroutine if a TTL is set + if lc.ttl > 0 { + go func(key CacheKey) { + <-time.After(lc.ttl) + lc.Purge(key) + }(key) + } +} + +// Get retrieves a key from a local cache. If found, it will create and return an +// 'Entry' using the returned values. If not found, it will return an empty 'Entry' +func (lc *LocalCache) Get(key CacheKey) (CacheEntry, bool) { + val, ok := lc.get(key) + if ok { + lc.metrics.Incr("localcache.hit", lc.tags, 1.0) + return val, ok + } + + lc.metrics.Incr("localcache.miss", lc.tags, 1.0) + return CacheEntry{}, false +} + +// Set will set an entry within the current cache +func (lc *LocalCache) Set(key CacheKey, entry CacheEntry) { + lc.metrics.Incr("localcache.set", lc.tags, 1.0) + lc.set(key, entry) +} + +// Purge will remove a set of keys from the local cache map +func (lc *LocalCache) Purge(key CacheKey) { + lc.metrics.Incr("localcache.purge", lc.tags, 1.0) + lc.localCacheData.Delete(key) +} diff --git a/internal/pkg/groups/localcache_test.go b/internal/pkg/groups/localcache_test.go new file mode 100644 index 00000000..5e388153 --- /dev/null +++ b/internal/pkg/groups/localcache_test.go @@ -0,0 +1,70 @@ +package groups + +import ( + "reflect" + "testing" + "time" + + //"github.com/cactus/go-statsd-client/statsd/statsdtest" + "github.com/datadog/datadog-go/statsd" + //"github.com/cactus/go-statsd-client/statsd" +) + +func TestNotAvailableAfterTTL(t *testing.T) { + // Create a cache with a 10 millisecond TTL + statsdClient, _ := statsd.New("127.0.0.1:8125") + cache := NewLocalCache(time.Millisecond*10, statsdClient, []string{"test_case"}) + + // Create a cache Key and Entry and insert it into the cache + cacheKey := CacheKey{ + Email: "email@test.com", + AllowedGroups: "testGroup", + } + cacheData := CacheEntry{ + ValidGroups: []string{"testGroup"}, + } + cache.Set(cacheKey, cacheData) + + // Check the cached entry can be retrieved from the cache. + if data, _ := cache.Get(cacheKey); !reflect.DeepEqual(data, cacheData) { + t.Logf(" expected data to be '%+v'", cacheData) + t.Logf("actual data returned was '%+v'", data) + t.Fatalf("unexpected data returned") + } + + // If we wait 10ms (or lets say, 50 for good luck), it will have been removed + time.Sleep(time.Millisecond * 50) + + if _, found := cache.get(cacheKey); found { + t.Fatalf("expected key not to be have been found after the TTL expired") + } +} + +func TestNotAvailableAfterPurge(t *testing.T) { + statsdClient, _ := statsd.New("127.0.0.1:8125") + cache := NewLocalCache(time.Duration(10)*time.Second, statsdClient, []string{"test_case"}) + + // Create a cache Key and Entry and insert it into the cache + cacheKey := CacheKey{ + Email: "email@test.com", + AllowedGroups: "testGroup", + } + cacheData := CacheEntry{ + ValidGroups: []string{"testGroup"}, + } + cache.Set(cacheKey, cacheData) + + // Check the cached entry can be retrieved from the cache. + if data, _ := cache.Get(cacheKey); !reflect.DeepEqual(data, cacheData) { + t.Logf(" expected data to be '%+v'", cacheData) + t.Logf("actual data returned was '%+v'", data) + t.Fatalf("unexpected data returned") + } + + cache.Purge(cacheKey) + + // Purge should have removed the entry, despite being within the cache TTL + if _, found := cache.get(cacheKey); found { + t.Fatalf("expected key not to be have been found after purging") + } +} From 02669c1656ed81007369b7f250e51ed98466ebf7 Mon Sep 17 00:00:00 2001 From: Jusshersmith Date: Tue, 30 Apr 2019 11:56:09 +0100 Subject: [PATCH 2/2] go mod adding new sync vendored dir --- go.mod | 1 + vendor/golang.org/x/sync/AUTHORS | 3 + vendor/golang.org/x/sync/CONTRIBUTORS | 3 + vendor/golang.org/x/sync/LICENSE | 27 ++ vendor/golang.org/x/sync/PATENTS | 22 ++ vendor/golang.org/x/sync/syncmap/map.go | 372 ++++++++++++++++++++++++ vendor/modules.txt | 2 + 7 files changed, 430 insertions(+) create mode 100644 vendor/golang.org/x/sync/AUTHORS create mode 100644 vendor/golang.org/x/sync/CONTRIBUTORS create mode 100644 vendor/golang.org/x/sync/LICENSE create mode 100644 vendor/golang.org/x/sync/PATENTS create mode 100644 vendor/golang.org/x/sync/syncmap/map.go diff --git a/go.mod b/go.mod index 3c636371..cc5a255a 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/sirupsen/logrus v1.3.0 golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 google.golang.org/api v0.1.0 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/vendor/golang.org/x/sync/AUTHORS b/vendor/golang.org/x/sync/AUTHORS new file mode 100644 index 00000000..15167cd7 --- /dev/null +++ b/vendor/golang.org/x/sync/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/sync/CONTRIBUTORS b/vendor/golang.org/x/sync/CONTRIBUTORS new file mode 100644 index 00000000..1c4577e9 --- /dev/null +++ b/vendor/golang.org/x/sync/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/sync/LICENSE b/vendor/golang.org/x/sync/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/golang.org/x/sync/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/sync/PATENTS b/vendor/golang.org/x/sync/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/sync/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/sync/syncmap/map.go b/vendor/golang.org/x/sync/syncmap/map.go new file mode 100644 index 00000000..80e15847 --- /dev/null +++ b/vendor/golang.org/x/sync/syncmap/map.go @@ -0,0 +1,372 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package syncmap provides a concurrent map implementation. +// It is a prototype for a proposed addition to the sync package +// in the standard library. +// (https://golang.org/issue/18177) +package syncmap + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +// Map is a concurrent map with amortized-constant-time loads, stores, and deletes. +// It is safe for multiple goroutines to call a Map's methods concurrently. +// +// The zero Map is valid and empty. +// +// A Map must not be copied after first use. +type Map struct { + mu sync.Mutex + + // read contains the portion of the map's contents that are safe for + // concurrent access (with or without mu held). + // + // The read field itself is always safe to load, but must only be stored with + // mu held. + // + // Entries stored in read may be updated concurrently without mu, but updating + // a previously-expunged entry requires that the entry be copied to the dirty + // map and unexpunged with mu held. + read atomic.Value // readOnly + + // dirty contains the portion of the map's contents that require mu to be + // held. To ensure that the dirty map can be promoted to the read map quickly, + // it also includes all of the non-expunged entries in the read map. + // + // Expunged entries are not stored in the dirty map. An expunged entry in the + // clean map must be unexpunged and added to the dirty map before a new value + // can be stored to it. + // + // If the dirty map is nil, the next write to the map will initialize it by + // making a shallow copy of the clean map, omitting stale entries. + dirty map[interface{}]*entry + + // misses counts the number of loads since the read map was last updated that + // needed to lock mu to determine whether the key was present. + // + // Once enough misses have occurred to cover the cost of copying the dirty + // map, the dirty map will be promoted to the read map (in the unamended + // state) and the next store to the map will make a new dirty copy. + misses int +} + +// readOnly is an immutable struct stored atomically in the Map.read field. +type readOnly struct { + m map[interface{}]*entry + amended bool // true if the dirty map contains some key not in m. +} + +// expunged is an arbitrary pointer that marks entries which have been deleted +// from the dirty map. +var expunged = unsafe.Pointer(new(interface{})) + +// An entry is a slot in the map corresponding to a particular key. +type entry struct { + // p points to the interface{} value stored for the entry. + // + // If p == nil, the entry has been deleted and m.dirty == nil. + // + // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry + // is missing from m.dirty. + // + // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty + // != nil, in m.dirty[key]. + // + // An entry can be deleted by atomic replacement with nil: when m.dirty is + // next created, it will atomically replace nil with expunged and leave + // m.dirty[key] unset. + // + // An entry's associated value can be updated by atomic replacement, provided + // p != expunged. If p == expunged, an entry's associated value can be updated + // only after first setting m.dirty[key] = e so that lookups using the dirty + // map find the entry. + p unsafe.Pointer // *interface{} +} + +func newEntry(i interface{}) *entry { + return &entry{p: unsafe.Pointer(&i)} +} + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *Map) Load(key interface{}) (value interface{}, ok bool) { + read, _ := m.read.Load().(readOnly) + e, ok := read.m[key] + if !ok && read.amended { + m.mu.Lock() + // Avoid reporting a spurious miss if m.dirty got promoted while we were + // blocked on m.mu. (If further loads of the same key will not miss, it's + // not worth copying the dirty map for this key.) + read, _ = m.read.Load().(readOnly) + e, ok = read.m[key] + if !ok && read.amended { + e, ok = m.dirty[key] + // Regardless of whether the entry was present, record a miss: this key + // will take the slow path until the dirty map is promoted to the read + // map. + m.missLocked() + } + m.mu.Unlock() + } + if !ok { + return nil, false + } + return e.load() +} + +func (e *entry) load() (value interface{}, ok bool) { + p := atomic.LoadPointer(&e.p) + if p == nil || p == expunged { + return nil, false + } + return *(*interface{})(p), true +} + +// Store sets the value for a key. +func (m *Map) Store(key, value interface{}) { + read, _ := m.read.Load().(readOnly) + if e, ok := read.m[key]; ok && e.tryStore(&value) { + return + } + + m.mu.Lock() + read, _ = m.read.Load().(readOnly) + if e, ok := read.m[key]; ok { + if e.unexpungeLocked() { + // The entry was previously expunged, which implies that there is a + // non-nil dirty map and this entry is not in it. + m.dirty[key] = e + } + e.storeLocked(&value) + } else if e, ok := m.dirty[key]; ok { + e.storeLocked(&value) + } else { + if !read.amended { + // We're adding the first new key to the dirty map. + // Make sure it is allocated and mark the read-only map as incomplete. + m.dirtyLocked() + m.read.Store(readOnly{m: read.m, amended: true}) + } + m.dirty[key] = newEntry(value) + } + m.mu.Unlock() +} + +// tryStore stores a value if the entry has not been expunged. +// +// If the entry is expunged, tryStore returns false and leaves the entry +// unchanged. +func (e *entry) tryStore(i *interface{}) bool { + p := atomic.LoadPointer(&e.p) + if p == expunged { + return false + } + for { + if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { + return true + } + p = atomic.LoadPointer(&e.p) + if p == expunged { + return false + } + } +} + +// unexpungeLocked ensures that the entry is not marked as expunged. +// +// If the entry was previously expunged, it must be added to the dirty map +// before m.mu is unlocked. +func (e *entry) unexpungeLocked() (wasExpunged bool) { + return atomic.CompareAndSwapPointer(&e.p, expunged, nil) +} + +// storeLocked unconditionally stores a value to the entry. +// +// The entry must be known not to be expunged. +func (e *entry) storeLocked(i *interface{}) { + atomic.StorePointer(&e.p, unsafe.Pointer(i)) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) { + // Avoid locking if it's a clean hit. + read, _ := m.read.Load().(readOnly) + if e, ok := read.m[key]; ok { + actual, loaded, ok := e.tryLoadOrStore(value) + if ok { + return actual, loaded + } + } + + m.mu.Lock() + read, _ = m.read.Load().(readOnly) + if e, ok := read.m[key]; ok { + if e.unexpungeLocked() { + m.dirty[key] = e + } + actual, loaded, _ = e.tryLoadOrStore(value) + } else if e, ok := m.dirty[key]; ok { + actual, loaded, _ = e.tryLoadOrStore(value) + m.missLocked() + } else { + if !read.amended { + // We're adding the first new key to the dirty map. + // Make sure it is allocated and mark the read-only map as incomplete. + m.dirtyLocked() + m.read.Store(readOnly{m: read.m, amended: true}) + } + m.dirty[key] = newEntry(value) + actual, loaded = value, false + } + m.mu.Unlock() + + return actual, loaded +} + +// tryLoadOrStore atomically loads or stores a value if the entry is not +// expunged. +// +// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and +// returns with ok==false. +func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) { + p := atomic.LoadPointer(&e.p) + if p == expunged { + return nil, false, false + } + if p != nil { + return *(*interface{})(p), true, true + } + + // Copy the interface after the first load to make this method more amenable + // to escape analysis: if we hit the "load" path or the entry is expunged, we + // shouldn't bother heap-allocating. + ic := i + for { + if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) { + return i, false, true + } + p = atomic.LoadPointer(&e.p) + if p == expunged { + return nil, false, false + } + if p != nil { + return *(*interface{})(p), true, true + } + } +} + +// Delete deletes the value for a key. +func (m *Map) Delete(key interface{}) { + read, _ := m.read.Load().(readOnly) + e, ok := read.m[key] + if !ok && read.amended { + m.mu.Lock() + read, _ = m.read.Load().(readOnly) + e, ok = read.m[key] + if !ok && read.amended { + delete(m.dirty, key) + } + m.mu.Unlock() + } + if ok { + e.delete() + } +} + +func (e *entry) delete() (hadValue bool) { + for { + p := atomic.LoadPointer(&e.p) + if p == nil || p == expunged { + return false + } + if atomic.CompareAndSwapPointer(&e.p, p, nil) { + return true + } + } +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently, Range may reflect any mapping for that key +// from any point during the Range call. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *Map) Range(f func(key, value interface{}) bool) { + // We need to be able to iterate over all of the keys that were already + // present at the start of the call to Range. + // If read.amended is false, then read.m satisfies that property without + // requiring us to hold m.mu for a long time. + read, _ := m.read.Load().(readOnly) + if read.amended { + // m.dirty contains keys not in read.m. Fortunately, Range is already O(N) + // (assuming the caller does not break out early), so a call to Range + // amortizes an entire copy of the map: we can promote the dirty copy + // immediately! + m.mu.Lock() + read, _ = m.read.Load().(readOnly) + if read.amended { + read = readOnly{m: m.dirty} + m.read.Store(read) + m.dirty = nil + m.misses = 0 + } + m.mu.Unlock() + } + + for k, e := range read.m { + v, ok := e.load() + if !ok { + continue + } + if !f(k, v) { + break + } + } +} + +func (m *Map) missLocked() { + m.misses++ + if m.misses < len(m.dirty) { + return + } + m.read.Store(readOnly{m: m.dirty}) + m.dirty = nil + m.misses = 0 +} + +func (m *Map) dirtyLocked() { + if m.dirty != nil { + return + } + + read, _ := m.read.Load().(readOnly) + m.dirty = make(map[interface{}]*entry, len(read.m)) + for k, e := range read.m { + if !e.tryExpungeLocked() { + m.dirty[k] = e + } + } +} + +func (e *entry) tryExpungeLocked() (isExpunged bool) { + p := atomic.LoadPointer(&e.p) + for p == nil { + if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { + return true + } + p = atomic.LoadPointer(&e.p) + } + return p == expunged +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 86106d22..80319281 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -34,6 +34,8 @@ golang.org/x/oauth2/google golang.org/x/oauth2/internal golang.org/x/oauth2/jws golang.org/x/oauth2/jwt +# golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 +golang.org/x/sync/syncmap # golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e golang.org/x/sys/unix golang.org/x/sys/windows