Merge branch '20187-cache-discovery-doc'
authorTom Clegg <tom@curii.com>
Fri, 24 Mar 2023 05:29:34 +0000 (01:29 -0400)
committerTom Clegg <tom@curii.com>
Fri, 24 Mar 2023 05:29:34 +0000 (01:29 -0400)
closes #20187

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

lib/controller/handler.go
lib/controller/handler_test.go
sdk/go/arvadostest/proxy.go
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/config/initializers/schema_discovery_cache.rb [deleted file]
services/api/test/functional/arvados/v1/schema_controller_test.rb
services/api/test/integration/remote_user_test.rb

index 4810ec3c257e18d626cbf8a12ff44c475a46ab5d..045e4a8c9c40b71092fd4e5e6d1976b65f8c1760 100644 (file)
@@ -6,12 +6,17 @@ package controller
 
 import (
        "context"
+       "encoding/json"
+       "errors"
        "fmt"
+       "io/ioutil"
+       "mime"
        "net/http"
        "net/http/httptest"
        "net/url"
        "strings"
        "sync"
+       "time"
 
        "git.arvados.org/arvados.git/lib/controller/api"
        "git.arvados.org/arvados.git/lib/controller/federation"
@@ -20,6 +25,7 @@ import (
        "git.arvados.org/arvados.git/lib/controller/router"
        "git.arvados.org/arvados.git/lib/ctrlctx"
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/health"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
 
@@ -39,6 +45,8 @@ type Handler struct {
        insecureClient *http.Client
        dbConnector    ctrlctx.DBConnector
        limitLogCreate chan struct{}
+
+       cache map[string]*cacheEnt
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -162,6 +170,9 @@ func (h *Handler) setup() {
        h.proxy = &proxy{
                Name: "arvados-controller",
        }
+       h.cache = map[string]*cacheEnt{
+               "/discovery/v1/apis/arvados/v1/rest": &cacheEnt{validate: validateDiscoveryDoc},
+       }
 
        go h.trashSweepWorker()
        go h.containerLogSweepWorker()
@@ -208,7 +219,127 @@ func (h *Handler) limitLogCreateRequests(w http.ResponseWriter, req *http.Reques
        next.ServeHTTP(w, req)
 }
 
+// cacheEnt implements a basic stale-while-revalidate cache, suitable
+// for the Arvados discovery document.
+type cacheEnt struct {
+       validate     func(body []byte) error
+       mtx          sync.Mutex
+       header       http.Header
+       body         []byte
+       expireAfter  time.Time
+       refreshAfter time.Time
+       refreshLock  sync.Mutex
+}
+
+const (
+       cacheTTL    = 5 * time.Minute
+       cacheExpire = 24 * time.Hour
+)
+
+func (ent *cacheEnt) refresh(path string, do func(*http.Request) (*http.Response, error)) (http.Header, []byte, error) {
+       ent.refreshLock.Lock()
+       defer ent.refreshLock.Unlock()
+       if header, body, needRefresh := ent.response(); !needRefresh {
+               // another goroutine refreshed successfully while we
+               // were waiting for refreshLock
+               return header, body, nil
+       } else if body != nil {
+               // Cache is present, but expired. We'll try to refresh
+               // below. Meanwhile, other refresh() calls will queue
+               // up for refreshLock -- and we don't want them to
+               // turn into N upstream requests, even if upstream is
+               // failing.  (If we succeed we'll update the expiry
+               // time again below with the real cacheTTL -- this
+               // just takes care of the error case.)
+               ent.mtx.Lock()
+               ent.refreshAfter = time.Now().Add(time.Second)
+               ent.mtx.Unlock()
+       }
+
+       ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
+       defer cancel()
+       // 0.0.0.0:0 is just a placeholder here -- do(), which is
+       // localClusterRequest(), will replace the scheme and host
+       // parts with the real proxy destination.
+       req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://0.0.0.0:0/"+path, nil)
+       if err != nil {
+               return nil, nil, err
+       }
+       resp, err := do(req)
+       if err != nil {
+               return nil, nil, err
+       }
+       if resp.StatusCode != http.StatusOK {
+               return nil, nil, fmt.Errorf("HTTP status %d", resp.StatusCode)
+       }
+       body, err := ioutil.ReadAll(resp.Body)
+       if err != nil {
+               return nil, nil, fmt.Errorf("Read error: %w", err)
+       }
+       header := http.Header{}
+       for k, v := range resp.Header {
+               if !dropHeaders[k] && k != "X-Request-Id" {
+                       header[k] = v
+               }
+       }
+       if ent.validate != nil {
+               if err := ent.validate(body); err != nil {
+                       return nil, nil, err
+               }
+       } else if mediatype, _, err := mime.ParseMediaType(header.Get("Content-Type")); err == nil && mediatype == "application/json" {
+               if !json.Valid(body) {
+                       return nil, nil, errors.New("invalid JSON encoding in response")
+               }
+       }
+       ent.mtx.Lock()
+       defer ent.mtx.Unlock()
+       ent.header = header
+       ent.body = body
+       ent.refreshAfter = time.Now().Add(cacheTTL)
+       ent.expireAfter = time.Now().Add(cacheExpire)
+       return ent.header, ent.body, nil
+}
+
+func (ent *cacheEnt) response() (http.Header, []byte, bool) {
+       ent.mtx.Lock()
+       defer ent.mtx.Unlock()
+       if ent.expireAfter.Before(time.Now()) {
+               ent.header, ent.body, ent.refreshAfter = nil, nil, time.Time{}
+       }
+       return ent.header, ent.body, ent.refreshAfter.Before(time.Now())
+}
+
+func (ent *cacheEnt) ServeHTTP(ctx context.Context, w http.ResponseWriter, path string, do func(*http.Request) (*http.Response, error)) {
+       header, body, needRefresh := ent.response()
+       if body == nil {
+               // need to fetch before we can return anything
+               var err error
+               header, body, err = ent.refresh(path, do)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusBadGateway)
+                       return
+               }
+       } else if needRefresh {
+               // re-fetch in background
+               go func() {
+                       _, _, err := ent.refresh(path, do)
+                       if err != nil {
+                               ctxlog.FromContext(ctx).WithError(err).WithField("path", path).Warn("error refreshing cache")
+                       }
+               }()
+       }
+       for k, v := range header {
+               w.Header()[k] = v
+       }
+       w.WriteHeader(http.StatusOK)
+       w.Write(body)
+}
+
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next http.Handler) {
+       if ent, ok := h.cache[req.URL.Path]; ok && req.Method == http.MethodGet {
+               ent.ServeHTTP(req.Context(), w, req.URL.Path, h.localClusterRequest)
+               return
+       }
        resp, err := h.localClusterRequest(req)
        n, err := h.proxy.ForwardResponse(w, resp, err)
        if err != nil {
@@ -232,3 +363,15 @@ func findRailsAPI(cluster *arvados.Cluster) (*url.URL, bool, error) {
        }
        return best, cluster.TLS.Insecure, nil
 }
+
+func validateDiscoveryDoc(body []byte) error {
+       var dd arvados.DiscoveryDocument
+       err := json.Unmarshal(body, &dd)
+       if err != nil {
+               return fmt.Errorf("error decoding JSON response: %w", err)
+       }
+       if dd.BasePath == "" {
+               return errors.New("error in discovery document: no value for basePath")
+       }
+       return nil
+}
index 76eab9ca1568c1e1f134489fb70d9e732d5e9bae..fcd70d7cc535bbe1db0dbfab391049e15ac30e72 100644 (file)
@@ -16,6 +16,7 @@ import (
        "net/url"
        "os"
        "strings"
+       "sync"
        "testing"
        "time"
 
@@ -37,11 +38,12 @@ func Test(t *testing.T) {
 var _ = check.Suite(&HandlerSuite{})
 
 type HandlerSuite struct {
-       cluster *arvados.Cluster
-       handler *Handler
-       logbuf  *bytes.Buffer
-       ctx     context.Context
-       cancel  context.CancelFunc
+       cluster  *arvados.Cluster
+       handler  *Handler
+       railsSpy *arvadostest.Proxy
+       logbuf   *bytes.Buffer
+       ctx      context.Context
+       cancel   context.CancelFunc
 }
 
 func (s *HandlerSuite) SetUpTest(c *check.C) {
@@ -55,6 +57,8 @@ func (s *HandlerSuite) SetUpTest(c *check.C) {
        s.cluster.API.RequestTimeout = arvados.Duration(5 * time.Minute)
        s.cluster.TLS.Insecure = true
        arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+       s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
+       arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, s.railsSpy.URL.String())
        arvadostest.SetServiceURL(&s.cluster.Services.Controller, "http://localhost:/")
        s.handler = newHandler(s.ctx, s.cluster, "", prometheus.NewRegistry()).(*Handler)
 }
@@ -93,6 +97,204 @@ func (s *HandlerSuite) TestConfigExport(c *check.C) {
        }
 }
 
+func (s *HandlerSuite) TestDiscoveryDocCache(c *check.C) {
+       countRailsReqs := func() int {
+               n := 0
+               for _, req := range s.railsSpy.RequestDumps {
+                       if bytes.Contains(req, []byte("/discovery/v1/apis/arvados/v1/rest")) {
+                               n++
+                       }
+               }
+               return n
+       }
+       getDD := func() int {
+               req := httptest.NewRequest(http.MethodGet, "/discovery/v1/apis/arvados/v1/rest", nil)
+               resp := httptest.NewRecorder()
+               s.handler.ServeHTTP(resp, req)
+               if resp.Code == http.StatusOK {
+                       var dd arvados.DiscoveryDocument
+                       err := json.Unmarshal(resp.Body.Bytes(), &dd)
+                       c.Check(err, check.IsNil)
+                       c.Check(dd.Schemas["Collection"].UUIDPrefix, check.Equals, "4zz18")
+               }
+               return resp.Code
+       }
+       getDDConcurrently := func(n int, expectCode int, checkArgs ...interface{}) *sync.WaitGroup {
+               var wg sync.WaitGroup
+               for i := 0; i < n; i++ {
+                       wg.Add(1)
+                       go func() {
+                               defer wg.Done()
+                               c.Check(getDD(), check.Equals, append([]interface{}{expectCode}, checkArgs...)...)
+                       }()
+               }
+               return &wg
+       }
+       clearCache := func() {
+               for _, ent := range s.handler.cache {
+                       ent.refreshLock.Lock()
+                       ent.mtx.Lock()
+                       ent.body, ent.header, ent.refreshAfter = nil, nil, time.Time{}
+                       ent.mtx.Unlock()
+                       ent.refreshLock.Unlock()
+               }
+       }
+       waitPendingUpdates := func() {
+               for _, ent := range s.handler.cache {
+                       ent.refreshLock.Lock()
+                       defer ent.refreshLock.Unlock()
+                       ent.mtx.Lock()
+                       defer ent.mtx.Unlock()
+               }
+       }
+       refreshNow := func() {
+               waitPendingUpdates()
+               for _, ent := range s.handler.cache {
+                       ent.refreshAfter = time.Now()
+               }
+       }
+       expireNow := func() {
+               waitPendingUpdates()
+               for _, ent := range s.handler.cache {
+                       ent.expireAfter = time.Now()
+               }
+       }
+
+       // Easy path: first req fetches, subsequent reqs use cache.
+       c.Check(countRailsReqs(), check.Equals, 0)
+       c.Check(getDD(), check.Equals, http.StatusOK)
+       c.Check(countRailsReqs(), check.Equals, 1)
+       c.Check(getDD(), check.Equals, http.StatusOK)
+       c.Check(countRailsReqs(), check.Equals, 1)
+       c.Check(getDD(), check.Equals, http.StatusOK)
+       c.Check(countRailsReqs(), check.Equals, 1)
+
+       // To guarantee we have concurrent requests, we set up
+       // railsSpy to hold up the Handler's outgoing requests until
+       // we send to (or close) holdReqs.
+       holdReqs := make(chan struct{})
+       s.railsSpy.Director = func(*http.Request) {
+               <-holdReqs
+       }
+
+       // Race at startup: first req fetches, other concurrent reqs
+       // wait for the initial fetch to complete, then all return.
+       clearCache()
+       reqsBefore := countRailsReqs()
+       wg := getDDConcurrently(5, http.StatusOK, check.Commentf("race at startup"))
+       close(holdReqs)
+       wg.Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       // Race after expiry: concurrent reqs return the cached data
+       // but initiate a new fetch in the background.
+       refreshNow()
+       holdReqs = make(chan struct{})
+       wg = getDDConcurrently(5, http.StatusOK, check.Commentf("race after expiry"))
+       reqsBefore = countRailsReqs()
+       close(holdReqs)
+       wg.Wait()
+       for deadline := time.Now().Add(time.Second); time.Now().Before(deadline) && countRailsReqs() < reqsBefore+1; {
+               time.Sleep(time.Second / 100)
+       }
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       // Configure railsSpy to return an error or bad content
+       // depending on flags.
+       var wantError, wantBadContent bool
+       s.railsSpy.Director = func(req *http.Request) {
+               if wantError {
+                       req.Method = "MAKE-COFFEE"
+               } else if wantBadContent {
+                       req.URL.Path = "/_health/ping"
+                       req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+               }
+       }
+
+       // Error at startup (empty cache) => caller gets error, and we
+       // make an upstream attempt for each incoming request because
+       // we have nothing better to return
+       clearCache()
+       wantError, wantBadContent = true, false
+       reqsBefore = countRailsReqs()
+       holdReqs = make(chan struct{})
+       wg = getDDConcurrently(5, http.StatusBadGateway, check.Commentf("error at startup"))
+       close(holdReqs)
+       wg.Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+5)
+
+       // Response status is OK but body is not a discovery document
+       wantError, wantBadContent = false, true
+       reqsBefore = countRailsReqs()
+       c.Check(getDD(), check.Equals, http.StatusBadGateway)
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       // Error condition clears => caller gets OK, cache is warmed
+       // up
+       wantError, wantBadContent = false, false
+       reqsBefore = countRailsReqs()
+       getDDConcurrently(5, http.StatusOK, check.Commentf("success after errors at startup")).Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       // Error with warm cache => caller gets OK (with no attempt to
+       // re-fetch)
+       wantError, wantBadContent = true, false
+       reqsBefore = countRailsReqs()
+       getDDConcurrently(5, http.StatusOK, check.Commentf("error with warm cache")).Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore)
+
+       // Error with stale cache => caller gets OK with stale data
+       // while the re-fetch is attempted in the background
+       refreshNow()
+       wantError, wantBadContent = true, false
+       reqsBefore = countRailsReqs()
+       holdReqs = make(chan struct{})
+       getDDConcurrently(5, http.StatusOK, check.Commentf("error with stale cache")).Wait()
+       close(holdReqs)
+       // Only one attempt to re-fetch (holdReqs ensured the first
+       // update took long enough for the last incoming request to
+       // arrive)
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       refreshNow()
+       wantError, wantBadContent = false, false
+       reqsBefore = countRailsReqs()
+       holdReqs = make(chan struct{})
+       getDDConcurrently(5, http.StatusOK, check.Commentf("refresh cache after error condition clears")).Wait()
+       close(holdReqs)
+       waitPendingUpdates()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+
+       // Make sure expireAfter is getting set
+       waitPendingUpdates()
+       exp := s.handler.cache["/discovery/v1/apis/arvados/v1/rest"].expireAfter.Sub(time.Now())
+       c.Check(exp > cacheTTL, check.Equals, true)
+       c.Check(exp < cacheExpire, check.Equals, true)
+
+       // After the cache *expires* it behaves as if uninitialized:
+       // each incoming request does a new upstream request until one
+       // succeeds.
+       //
+       // First check failure after expiry:
+       expireNow()
+       wantError, wantBadContent = true, false
+       reqsBefore = countRailsReqs()
+       holdReqs = make(chan struct{})
+       wg = getDDConcurrently(5, http.StatusBadGateway, check.Commentf("error after expiry"))
+       close(holdReqs)
+       wg.Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+5)
+
+       // Success after expiry:
+       wantError, wantBadContent = false, false
+       reqsBefore = countRailsReqs()
+       holdReqs = make(chan struct{})
+       wg = getDDConcurrently(5, http.StatusOK, check.Commentf("success after expiry"))
+       close(holdReqs)
+       wg.Wait()
+       c.Check(countRailsReqs(), check.Equals, reqsBefore+1)
+}
+
 func (s *HandlerSuite) TestVocabularyExport(c *check.C) {
        voc := `{
                "strict_tags": false,
@@ -210,7 +412,7 @@ func (s *HandlerSuite) TestProxyDiscoveryDoc(c *check.C) {
 // etc.
 func (s *HandlerSuite) TestRequestCancel(c *check.C) {
        ctx, cancel := context.WithCancel(context.Background())
-       req := httptest.NewRequest("GET", "/discovery/v1/apis/arvados/v1/rest", nil).WithContext(ctx)
+       req := httptest.NewRequest("GET", "/static/login_failure", nil).WithContext(ctx)
        resp := httptest.NewRecorder()
        cancel()
        s.handler.ServeHTTP(resp, req)
index 48700d8b186d8fd4d104ed820a6d3060772a86bc..9940ddd3d96404552282b5e6b8e887c31b3a1862 100644 (file)
@@ -26,6 +26,10 @@ type Proxy struct {
 
        // A dump of each request that has been proxied.
        RequestDumps [][]byte
+
+       // If non-nil, func will be called on each incoming request
+       // before proxying it.
+       Director func(*http.Request)
 }
 
 // NewProxy returns a new Proxy that saves a dump of each reqeust
@@ -63,6 +67,9 @@ func NewProxy(c *check.C, svc arvados.Service) *Proxy {
                URL:    u,
        }
        rp.Director = func(r *http.Request) {
+               if proxy.Director != nil {
+                       proxy.Director(r)
+               }
                dump, _ := httputil.DumpRequest(r, true)
                proxy.RequestDumps = append(proxy.RequestDumps, dump)
                r.URL.Scheme = target.Scheme
index 0300b750755ed89cc05de639d527391d9e24a039..34dfe966b0a38a7d347eb7427706d35b12465557 100644 (file)
@@ -24,213 +24,212 @@ class Arvados::V1::SchemaController < ApplicationController
   protected
 
   def discovery_doc
-    Rails.cache.fetch 'arvados_v1_rest_discovery' do
-      Rails.application.eager_load!
-      remoteHosts = {}
-      Rails.configuration.RemoteClusters.each {|k,v| if k != :"*" then remoteHosts[k] = v["Host"] end }
-      discovery = {
-        kind: "discovery#restDescription",
-        discoveryVersion: "v1",
-        id: "arvados:v1",
-        name: "arvados",
-        version: "v1",
-        # format is YYYYMMDD, must be fixed width (needs to be lexically
-        # sortable), updated manually, may be used by clients to
-        # determine availability of API server features.
-        revision: "20220510",
-        source_version: AppVersion.hash,
-        sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
-        packageVersion: AppVersion.package_version,
-        generatedAt: db_current_time.iso8601,
-        title: "Arvados API",
-        description: "The API to interact with Arvados.",
-        documentationLink: "http://doc.arvados.org/api/index.html",
-        defaultCollectionReplication: Rails.configuration.Collections.DefaultReplication,
-        protocol: "rest",
-        baseUrl: root_url + "arvados/v1/",
-        basePath: "/arvados/v1/",
-        rootUrl: root_url,
-        servicePath: "arvados/v1/",
-        batchPath: "batch",
-        uuidPrefix: Rails.configuration.ClusterID,
-        defaultTrashLifetime: Rails.configuration.Collections.DefaultTrashLifetime,
-        blobSignatureTtl: Rails.configuration.Collections.BlobSigningTTL,
-        maxRequestSize: Rails.configuration.API.MaxRequestSize,
-        maxItemsPerResponse: Rails.configuration.API.MaxItemsPerResponse,
-        dockerImageFormats: Rails.configuration.Containers.SupportedDockerImageFormats.keys,
-        crunchLogBytesPerEvent: Rails.configuration.Containers.Logging.LogBytesPerEvent,
-        crunchLogSecondsBetweenEvents: Rails.configuration.Containers.Logging.LogSecondsBetweenEvents,
-        crunchLogThrottlePeriod: Rails.configuration.Containers.Logging.LogThrottlePeriod,
-        crunchLogThrottleBytes: Rails.configuration.Containers.Logging.LogThrottleBytes,
-        crunchLogThrottleLines: Rails.configuration.Containers.Logging.LogThrottleLines,
-        crunchLimitLogBytesPerJob: Rails.configuration.Containers.Logging.LimitLogBytesPerJob,
-        crunchLogPartialLineThrottlePeriod: Rails.configuration.Containers.Logging.LogPartialLineThrottlePeriod,
-        crunchLogUpdatePeriod: Rails.configuration.Containers.Logging.LogUpdatePeriod,
-        crunchLogUpdateSize: Rails.configuration.Containers.Logging.LogUpdateSize,
-        remoteHosts: remoteHosts,
-        remoteHostsViaDNS: Rails.configuration.RemoteClusters["*"].Proxy,
-        websocketUrl: Rails.configuration.Services.Websocket.ExternalURL.to_s,
-        workbenchUrl: Rails.configuration.Services.Workbench1.ExternalURL.to_s,
-        workbench2Url: Rails.configuration.Services.Workbench2.ExternalURL.to_s,
-        keepWebServiceUrl: Rails.configuration.Services.WebDAV.ExternalURL.to_s,
-        gitUrl: Rails.configuration.Services.GitHTTP.ExternalURL.to_s,
-        parameters: {
-          alt: {
+    Rails.application.eager_load!
+    remoteHosts = {}
+    Rails.configuration.RemoteClusters.each {|k,v| if k != :"*" then remoteHosts[k] = v["Host"] end }
+    discovery = {
+      kind: "discovery#restDescription",
+      discoveryVersion: "v1",
+      id: "arvados:v1",
+      name: "arvados",
+      version: "v1",
+      # format is YYYYMMDD, must be fixed width (needs to be lexically
+      # sortable), updated manually, may be used by clients to
+      # determine availability of API server features.
+      revision: "20220510",
+      source_version: AppVersion.hash,
+      sourceVersion: AppVersion.hash, # source_version should be deprecated in the future
+      packageVersion: AppVersion.package_version,
+      generatedAt: db_current_time.iso8601,
+      title: "Arvados API",
+      description: "The API to interact with Arvados.",
+      documentationLink: "http://doc.arvados.org/api/index.html",
+      defaultCollectionReplication: Rails.configuration.Collections.DefaultReplication,
+      protocol: "rest",
+      baseUrl: root_url + "arvados/v1/",
+      basePath: "/arvados/v1/",
+      rootUrl: root_url,
+      servicePath: "arvados/v1/",
+      batchPath: "batch",
+      uuidPrefix: Rails.configuration.ClusterID,
+      defaultTrashLifetime: Rails.configuration.Collections.DefaultTrashLifetime,
+      blobSignatureTtl: Rails.configuration.Collections.BlobSigningTTL,
+      maxRequestSize: Rails.configuration.API.MaxRequestSize,
+      maxItemsPerResponse: Rails.configuration.API.MaxItemsPerResponse,
+      dockerImageFormats: Rails.configuration.Containers.SupportedDockerImageFormats.keys,
+      crunchLogBytesPerEvent: Rails.configuration.Containers.Logging.LogBytesPerEvent,
+      crunchLogSecondsBetweenEvents: Rails.configuration.Containers.Logging.LogSecondsBetweenEvents,
+      crunchLogThrottlePeriod: Rails.configuration.Containers.Logging.LogThrottlePeriod,
+      crunchLogThrottleBytes: Rails.configuration.Containers.Logging.LogThrottleBytes,
+      crunchLogThrottleLines: Rails.configuration.Containers.Logging.LogThrottleLines,
+      crunchLimitLogBytesPerJob: Rails.configuration.Containers.Logging.LimitLogBytesPerJob,
+      crunchLogPartialLineThrottlePeriod: Rails.configuration.Containers.Logging.LogPartialLineThrottlePeriod,
+      crunchLogUpdatePeriod: Rails.configuration.Containers.Logging.LogUpdatePeriod,
+      crunchLogUpdateSize: Rails.configuration.Containers.Logging.LogUpdateSize,
+      remoteHosts: remoteHosts,
+      remoteHostsViaDNS: Rails.configuration.RemoteClusters["*"].Proxy,
+      websocketUrl: Rails.configuration.Services.Websocket.ExternalURL.to_s,
+      workbenchUrl: Rails.configuration.Services.Workbench1.ExternalURL.to_s,
+      workbench2Url: Rails.configuration.Services.Workbench2.ExternalURL.to_s,
+      keepWebServiceUrl: Rails.configuration.Services.WebDAV.ExternalURL.to_s,
+      gitUrl: Rails.configuration.Services.GitHTTP.ExternalURL.to_s,
+      parameters: {
+        alt: {
+          type: "string",
+          description: "Data format for the response.",
+          default: "json",
+          enum: [
+            "json"
+          ],
+          enumDescriptions: [
+            "Responses with Content-Type of application/json"
+          ],
+          location: "query"
+        },
+        fields: {
+          type: "string",
+          description: "Selector specifying which fields to include in a partial response.",
+          location: "query"
+        },
+        key: {
+          type: "string",
+          description: "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
+          location: "query"
+        },
+        oauth_token: {
+          type: "string",
+          description: "OAuth 2.0 token for the current user.",
+          location: "query"
+        }
+      },
+      auth: {
+        oauth2: {
+          scopes: {
+            "https://api.arvados.org/auth/arvados" => {
+              description: "View and manage objects"
+            },
+            "https://api.arvados.org/auth/arvados.readonly" => {
+              description: "View objects"
+            }
+          }
+        }
+      },
+      schemas: {},
+      resources: {}
+    }
+
+    ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
+      begin
+        ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
+      rescue
+        # No controller -> no discovery.
+        next
+      end
+      object_properties = {}
+      k.columns.
+        select { |col| col.name != 'id' && !col.name.start_with?('secret_') }.
+        collect do |col|
+        if k.serialized_attributes.has_key? col.name
+          object_properties[col.name] = {
+            type: k.serialized_attributes[col.name].object_class.to_s
+          }
+        elsif k.attribute_types[col.name].is_a? JsonbType::Hash
+          object_properties[col.name] = {
+            type: Hash.to_s
+          }
+        elsif k.attribute_types[col.name].is_a? JsonbType::Array
+          object_properties[col.name] = {
+            type: Array.to_s
+          }
+        else
+          object_properties[col.name] = {
+            type: col.type
+          }
+        end
+      end
+      discovery[:schemas][k.to_s + 'List'] = {
+        id: k.to_s + 'List',
+        description: k.to_s + ' list',
+        type: "object",
+        properties: {
+          kind: {
             type: "string",
-            description: "Data format for the response.",
-            default: "json",
-            enum: [
-                   "json"
-                  ],
-            enumDescriptions: [
-                               "Responses with Content-Type of application/json"
-                              ],
-            location: "query"
+            description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
+            default: "arvados##{k.to_s.camelcase(:lower)}List"
+          },
+          etag: {
+            type: "string",
+            description: "List version."
+          },
+          items: {
+            type: "array",
+            description: "The list of #{k.to_s.pluralize}.",
+            items: {
+              "$ref" => k.to_s
+            }
           },
-          fields: {
+          next_link: {
             type: "string",
-            description: "Selector specifying which fields to include in a partial response.",
-            location: "query"
+            description: "A link to the next page of #{k.to_s.pluralize}."
           },
-          key: {
+          next_page_token: {
             type: "string",
-            description: "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
-            location: "query"
+            description: "The page token for the next page of #{k.to_s.pluralize}."
           },
-          oauth_token: {
+          selfLink: {
             type: "string",
-            description: "OAuth 2.0 token for the current user.",
-            location: "query"
+            description: "A link back to this list."
           }
-        },
-        auth: {
-          oauth2: {
-            scopes: {
-              "https://api.arvados.org/auth/arvados" => {
-                description: "View and manage objects"
-              },
-              "https://api.arvados.org/auth/arvados.readonly" => {
-                description: "View objects"
-              }
-            }
+        }
+      }
+      discovery[:schemas][k.to_s] = {
+        id: k.to_s,
+        description: k.to_s,
+        type: "object",
+        uuidPrefix: (k.respond_to?(:uuid_prefix) ? k.uuid_prefix : nil),
+        properties: {
+          uuid: {
+            type: "string",
+            description: "Object ID."
+          },
+          etag: {
+            type: "string",
+            description: "Object version."
           }
-        },
-        schemas: {},
-        resources: {}
+        }.merge(object_properties)
       }
-
-      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
-        begin
-          ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
-        rescue
-          # No controller -> no discovery.
-          next
-        end
-        object_properties = {}
-        k.columns.
-          select { |col| col.name != 'id' && !col.name.start_with?('secret_') }.
-          collect do |col|
-          if k.serialized_attributes.has_key? col.name
-            object_properties[col.name] = {
-              type: k.serialized_attributes[col.name].object_class.to_s
-            }
-          elsif k.attribute_types[col.name].is_a? JsonbType::Hash
-            object_properties[col.name] = {
-              type: Hash.to_s
-            }
-          elsif k.attribute_types[col.name].is_a? JsonbType::Array
-            object_properties[col.name] = {
-              type: Array.to_s
-            }
-          else
-            object_properties[col.name] = {
-              type: col.type
-            }
-          end
-        end
-        discovery[:schemas][k.to_s + 'List'] = {
-          id: k.to_s + 'List',
-          description: k.to_s + ' list',
-          type: "object",
-          properties: {
-            kind: {
-              type: "string",
-              description: "Object type. Always arvados##{k.to_s.camelcase(:lower)}List.",
-              default: "arvados##{k.to_s.camelcase(:lower)}List"
-            },
-            etag: {
-              type: "string",
-              description: "List version."
-            },
-            items: {
-              type: "array",
-              description: "The list of #{k.to_s.pluralize}.",
-              items: {
-                "$ref" => k.to_s
+      discovery[:resources][k.to_s.underscore.pluralize] = {
+        methods: {
+          get: {
+            id: "arvados.#{k.to_s.underscore.pluralize}.get",
+            path: "#{k.to_s.underscore.pluralize}/{uuid}",
+            httpMethod: "GET",
+            description: "Gets a #{k.to_s}'s metadata by UUID.",
+            parameters: {
+              uuid: {
+                type: "string",
+                description: "The UUID of the #{k.to_s} in question.",
+                required: true,
+                location: "path"
               }
             },
-            next_link: {
-              type: "string",
-              description: "A link to the next page of #{k.to_s.pluralize}."
-            },
-            next_page_token: {
-              type: "string",
-              description: "The page token for the next page of #{k.to_s.pluralize}."
-            },
-            selfLink: {
-              type: "string",
-              description: "A link back to this list."
-            }
-          }
-        }
-        discovery[:schemas][k.to_s] = {
-          id: k.to_s,
-          description: k.to_s,
-          type: "object",
-          uuidPrefix: (k.respond_to?(:uuid_prefix) ? k.uuid_prefix : nil),
-          properties: {
-            uuid: {
-              type: "string",
-              description: "Object ID."
-            },
-            etag: {
-              type: "string",
-              description: "Object version."
-            }
-          }.merge(object_properties)
-        }
-        discovery[:resources][k.to_s.underscore.pluralize] = {
-          methods: {
-            get: {
-              id: "arvados.#{k.to_s.underscore.pluralize}.get",
-              path: "#{k.to_s.underscore.pluralize}/{uuid}",
-              httpMethod: "GET",
-              description: "Gets a #{k.to_s}'s metadata by UUID.",
-              parameters: {
-                uuid: {
-                  type: "string",
-                  description: "The UUID of the #{k.to_s} in question.",
-                  required: true,
-                  location: "path"
-                }
-              },
-              parameterOrder: [
-                               "uuid"
-                              ],
-              response: {
-                "$ref" => k.to_s
-              },
-              scopes: [
-                       "https://api.arvados.org/auth/arvados",
-                       "https://api.arvados.org/auth/arvados.readonly"
-                      ]
+            parameterOrder: [
+              "uuid"
+            ],
+            response: {
+              "$ref" => k.to_s
             },
-            index: {
-              id: "arvados.#{k.to_s.underscore.pluralize}.index",
-              path: k.to_s.underscore.pluralize,
-              httpMethod: "GET",
-              description:
-                 %|Index #{k.to_s.pluralize}.
+            scopes: [
+              "https://api.arvados.org/auth/arvados",
+              "https://api.arvados.org/auth/arvados.readonly"
+            ]
+          },
+          index: {
+            id: "arvados.#{k.to_s.underscore.pluralize}.index",
+            path: k.to_s.underscore.pluralize,
+            httpMethod: "GET",
+            description:
+              %|Index #{k.to_s.pluralize}.
 
                    The <code>index</code> method returns a
                    <a href="/api/resources.html">resource list</a> of
@@ -251,243 +250,242 @@ class Arvados::V1::SchemaController < ApplicationController
                      "request_time":0.157236317
                     }
                     </pre>|,
-              parameters: {
-              },
-              response: {
-                "$ref" => "#{k.to_s}List"
-              },
-              scopes: [
-                       "https://api.arvados.org/auth/arvados",
-                       "https://api.arvados.org/auth/arvados.readonly"
-                      ]
+            parameters: {
             },
-            create: {
-              id: "arvados.#{k.to_s.underscore.pluralize}.create",
-              path: "#{k.to_s.underscore.pluralize}",
-              httpMethod: "POST",
-              description: "Create a new #{k.to_s}.",
-              parameters: {},
-              request: {
-                required: true,
-                properties: {
-                  k.to_s.underscore => {
-                    "$ref" => k.to_s
-                  }
-                }
-              },
-              response: {
-                "$ref" => k.to_s
-              },
-              scopes: [
-                       "https://api.arvados.org/auth/arvados"
-                      ]
+            response: {
+              "$ref" => "#{k.to_s}List"
             },
-            update: {
-              id: "arvados.#{k.to_s.underscore.pluralize}.update",
-              path: "#{k.to_s.underscore.pluralize}/{uuid}",
-              httpMethod: "PUT",
-              description: "Update attributes of an existing #{k.to_s}.",
-              parameters: {
-                uuid: {
-                  type: "string",
-                  description: "The UUID of the #{k.to_s} in question.",
-                  required: true,
-                  location: "path"
+            scopes: [
+              "https://api.arvados.org/auth/arvados",
+              "https://api.arvados.org/auth/arvados.readonly"
+            ]
+          },
+          create: {
+            id: "arvados.#{k.to_s.underscore.pluralize}.create",
+            path: "#{k.to_s.underscore.pluralize}",
+            httpMethod: "POST",
+            description: "Create a new #{k.to_s}.",
+            parameters: {},
+            request: {
+              required: true,
+              properties: {
+                k.to_s.underscore => {
+                  "$ref" => k.to_s
                 }
-              },
-              request: {
+              }
+            },
+            response: {
+              "$ref" => k.to_s
+            },
+            scopes: [
+              "https://api.arvados.org/auth/arvados"
+            ]
+          },
+          update: {
+            id: "arvados.#{k.to_s.underscore.pluralize}.update",
+            path: "#{k.to_s.underscore.pluralize}/{uuid}",
+            httpMethod: "PUT",
+            description: "Update attributes of an existing #{k.to_s}.",
+            parameters: {
+              uuid: {
+                type: "string",
+                description: "The UUID of the #{k.to_s} in question.",
                 required: true,
-                properties: {
-                  k.to_s.underscore => {
-                    "$ref" => k.to_s
-                  }
+                location: "path"
+              }
+            },
+            request: {
+              required: true,
+              properties: {
+                k.to_s.underscore => {
+                  "$ref" => k.to_s
                 }
-              },
+              }
+            },
+            response: {
+              "$ref" => k.to_s
+            },
+            scopes: [
+              "https://api.arvados.org/auth/arvados"
+            ]
+          },
+          delete: {
+            id: "arvados.#{k.to_s.underscore.pluralize}.delete",
+            path: "#{k.to_s.underscore.pluralize}/{uuid}",
+            httpMethod: "DELETE",
+            description: "Delete an existing #{k.to_s}.",
+            parameters: {
+              uuid: {
+                type: "string",
+                description: "The UUID of the #{k.to_s} in question.",
+                required: true,
+                location: "path"
+              }
+            },
+            response: {
+              "$ref" => k.to_s
+            },
+            scopes: [
+              "https://api.arvados.org/auth/arvados"
+            ]
+          }
+        }
+      }
+      # Check for Rails routes that don't match the usual actions
+      # listed above
+      d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
+      Rails.application.routes.routes.each do |route|
+        action = route.defaults[:action]
+        httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
+          method if route.verb.match(method)
+        }.compact.first
+        if httpMethod and
+          route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize and
+          ctl_class.action_methods.include? action
+          if !d_methods[action.to_sym]
+            method = {
+              id: "arvados.#{k.to_s.underscore.pluralize}.#{action}",
+              path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
+              httpMethod: httpMethod,
+              description: "#{action} #{k.to_s.underscore.pluralize}",
+              parameters: {},
               response: {
-                "$ref" => k.to_s
+                "$ref" => (action == 'index' ? "#{k.to_s}List" : k.to_s)
               },
               scopes: [
-                       "https://api.arvados.org/auth/arvados"
-                      ]
-            },
-            delete: {
-              id: "arvados.#{k.to_s.underscore.pluralize}.delete",
-              path: "#{k.to_s.underscore.pluralize}/{uuid}",
-              httpMethod: "DELETE",
-              description: "Delete an existing #{k.to_s}.",
-              parameters: {
-                uuid: {
+                "https://api.arvados.org/auth/arvados"
+              ]
+            }
+            route.segment_keys.each do |key|
+              if key != :format
+                key = :uuid if key == :id
+                method[:parameters][key] = {
                   type: "string",
-                  description: "The UUID of the #{k.to_s} in question.",
+                  description: "",
                   required: true,
                   location: "path"
                 }
-              },
-              response: {
-                "$ref" => k.to_s
-              },
-              scopes: [
-                       "https://api.arvados.org/auth/arvados"
-                      ]
-            }
-          }
-        }
-        # Check for Rails routes that don't match the usual actions
-        # listed above
-        d_methods = discovery[:resources][k.to_s.underscore.pluralize][:methods]
-        Rails.application.routes.routes.each do |route|
-          action = route.defaults[:action]
-          httpMethod = ['GET', 'POST', 'PUT', 'DELETE'].map { |method|
-            method if route.verb.match(method)
-          }.compact.first
-          if httpMethod and
-              route.defaults[:controller] == 'arvados/v1/' + k.to_s.underscore.pluralize and
-              ctl_class.action_methods.include? action
-            if !d_methods[action.to_sym]
-              method = {
-                id: "arvados.#{k.to_s.underscore.pluralize}.#{action}",
-                path: route.path.spec.to_s.sub('/arvados/v1/','').sub('(.:format)','').sub(/:(uu)?id/,'{uuid}'),
-                httpMethod: httpMethod,
-                description: "#{action} #{k.to_s.underscore.pluralize}",
-                parameters: {},
-                response: {
-                  "$ref" => (action == 'index' ? "#{k.to_s}List" : k.to_s)
-                },
-                scopes: [
-                         "https://api.arvados.org/auth/arvados"
-                        ]
-              }
-              route.segment_keys.each do |key|
-                if key != :format
-                  key = :uuid if key == :id
-                  method[:parameters][key] = {
-                    type: "string",
-                    description: "",
-                    required: true,
-                    location: "path"
-                  }
-                end
               end
-            else
-              # We already built a generic method description, but we
-              # might find some more required parameters through
-              # introspection.
-              method = d_methods[action.to_sym]
             end
-            if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
-              ctl_class.send("_#{action}_requires_parameters".to_sym).each do |l, v|
-                if v.is_a? Hash
-                  method[:parameters][l] = v
-                else
-                  method[:parameters][l] = {}
-                end
-                if !method[:parameters][l][:default].nil?
-                  # The JAVA SDK is sensitive to all values being strings
-                  method[:parameters][l][:default] = method[:parameters][l][:default].to_s
-                end
-                method[:parameters][l][:type] ||= 'string'
-                method[:parameters][l][:description] ||= ''
-                method[:parameters][l][:location] = (route.segment_keys.include?(l) ? 'path' : 'query')
-                if method[:parameters][l][:required].nil?
-                  method[:parameters][l][:required] = v != false
-                end
+          else
+            # We already built a generic method description, but we
+            # might find some more required parameters through
+            # introspection.
+            method = d_methods[action.to_sym]
+          end
+          if ctl_class.respond_to? "_#{action}_requires_parameters".to_sym
+            ctl_class.send("_#{action}_requires_parameters".to_sym).each do |l, v|
+              if v.is_a? Hash
+                method[:parameters][l] = v
+              else
+                method[:parameters][l] = {}
+              end
+              if !method[:parameters][l][:default].nil?
+                # The JAVA SDK is sensitive to all values being strings
+                method[:parameters][l][:default] = method[:parameters][l][:default].to_s
+              end
+              method[:parameters][l][:type] ||= 'string'
+              method[:parameters][l][:description] ||= ''
+              method[:parameters][l][:location] = (route.segment_keys.include?(l) ? 'path' : 'query')
+              if method[:parameters][l][:required].nil?
+                method[:parameters][l][:required] = v != false
               end
             end
-            d_methods[action.to_sym] = method
+          end
+          d_methods[action.to_sym] = method
 
-            if action == 'index'
-              list_method = method.dup
-              list_method[:id].sub!('index', 'list')
-              list_method[:description].sub!('Index', 'List')
-              list_method[:description].sub!('index', 'list')
-              d_methods[:list] = list_method
-            end
+          if action == 'index'
+            list_method = method.dup
+            list_method[:id].sub!('index', 'list')
+            list_method[:description].sub!('Index', 'List')
+            list_method[:description].sub!('index', 'list')
+            d_methods[:list] = list_method
           end
         end
       end
+    end
 
-      # The 'replace_files' option is implemented in lib/controller,
-      # not Rails -- we just need to add it here so discovery-aware
-      # clients know how to validate it.
-      [:create, :update].each do |action|
-        discovery[:resources]['collections'][:methods][action][:parameters]['replace_files'] = {
-          type: 'object',
-          description: 'Files and directories to initialize/replace with content from other collections.',
-          required: false,
-          location: 'query',
-          properties: {},
-          additionalProperties: {type: 'string'},
-        }
-      end
+    # The 'replace_files' option is implemented in lib/controller,
+    # not Rails -- we just need to add it here so discovery-aware
+    # clients know how to validate it.
+    [:create, :update].each do |action|
+      discovery[:resources]['collections'][:methods][action][:parameters]['replace_files'] = {
+        type: 'object',
+        description: 'Files and directories to initialize/replace with content from other collections.',
+        required: false,
+        location: 'query',
+        properties: {},
+        additionalProperties: {type: 'string'},
+      }
+    end
 
-      discovery[:resources]['configs'] = {
-        methods: {
-          get: {
-            id: "arvados.configs.get",
-            path: "config",
-            httpMethod: "GET",
-            description: "Get public config",
-            parameters: {
-            },
-            parameterOrder: [
-            ],
-            response: {
-            },
-            scopes: [
-              "https://api.arvados.org/auth/arvados",
-              "https://api.arvados.org/auth/arvados.readonly"
-            ]
+    discovery[:resources]['configs'] = {
+      methods: {
+        get: {
+          id: "arvados.configs.get",
+          path: "config",
+          httpMethod: "GET",
+          description: "Get public config",
+          parameters: {
           },
-        }
+          parameterOrder: [
+          ],
+          response: {
+          },
+          scopes: [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
       }
+    }
 
-      discovery[:resources]['vocabularies'] = {
-        methods: {
-          get: {
-            id: "arvados.vocabularies.get",
-            path: "vocabulary",
-            httpMethod: "GET",
-            description: "Get vocabulary definition",
-            parameters: {
-            },
-            parameterOrder: [
-            ],
-            response: {
-            },
-            scopes: [
-              "https://api.arvados.org/auth/arvados",
-              "https://api.arvados.org/auth/arvados.readonly"
-            ]
+    discovery[:resources]['vocabularies'] = {
+      methods: {
+        get: {
+          id: "arvados.vocabularies.get",
+          path: "vocabulary",
+          httpMethod: "GET",
+          description: "Get vocabulary definition",
+          parameters: {
           },
-        }
+          parameterOrder: [
+          ],
+          response: {
+          },
+          scopes: [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
       }
+    }
 
-      discovery[:resources]['sys'] = {
-        methods: {
-          get: {
-            id: "arvados.sys.trash_sweep",
-            path: "sys/trash_sweep",
-            httpMethod: "POST",
-            description: "apply scheduled trash and delete operations",
-            parameters: {
-            },
-            parameterOrder: [
-            ],
-            response: {
-            },
-            scopes: [
-              "https://api.arvados.org/auth/arvados",
-              "https://api.arvados.org/auth/arvados.readonly"
-            ]
+    discovery[:resources]['sys'] = {
+      methods: {
+        get: {
+          id: "arvados.sys.trash_sweep",
+          path: "sys/trash_sweep",
+          httpMethod: "POST",
+          description: "apply scheduled trash and delete operations",
+          parameters: {
           },
-        }
+          parameterOrder: [
+          ],
+          response: {
+          },
+          scopes: [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
       }
+    }
 
-      Rails.configuration.API.DisabledAPIs.each do |method, _|
-        ctrl, action = method.to_s.split('.', 2)
-        discovery[:resources][ctrl][:methods].delete(action.to_sym)
-      end
-      discovery
+    Rails.configuration.API.DisabledAPIs.each do |method, _|
+      ctrl, action = method.to_s.split('.', 2)
+      discovery[:resources][ctrl][:methods].delete(action.to_sym)
     end
+    discovery
   end
 end
diff --git a/services/api/config/initializers/schema_discovery_cache.rb b/services/api/config/initializers/schema_discovery_cache.rb
deleted file mode 100644 (file)
index c2cb8de..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Delete the cached discovery document during startup. Otherwise we
-# might still serve an old discovery document after updating the
-# schema and restarting the server.
-
-Rails.cache.delete 'arvados_v1_rest_discovery'
index 89feecb454a9fa74541b7328cf282287ee46da6e..f96f1af53746e6399efe911d19fb3d7976e95961 100644 (file)
@@ -9,7 +9,6 @@ class Arvados::V1::SchemaControllerTest < ActionController::TestCase
   setup do forget end
   teardown do forget end
   def forget
-    Rails.cache.delete 'arvados_v1_rest_discovery'
     AppVersion.forget
   end
 
index 179d30f3cbf3c255a1570ba3227b732603dd8ef9..af7b747d7439b0ae8ce1cda31e5262a7cb77a67e 100644 (file)
@@ -55,7 +55,6 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
         SSLCertName: [["CN", WEBrick::Utils::getservername]],
         StartCallback: lambda { ready.push(true) })
       srv.mount_proc '/discovery/v1/apis/arvados/v1/rest' do |req, res|
-        Rails.cache.delete 'arvados_v1_rest_discovery'
         res.body = Arvados::V1::SchemaController.new.send(:discovery_doc).to_json
       end
       srv.mount_proc '/arvados/v1/users/current' do |req, res|