17339: Merge branch 'main' into 17339-s3aws-driver-memory-footprint
authorWard Vandewege <ward@curii.com>
Tue, 7 Dec 2021 22:20:27 +0000 (17:20 -0500)
committerWard Vandewege <ward@curii.com>
Tue, 7 Dec 2021 22:20:27 +0000 (17:20 -0500)
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

doc/_config.yml
doc/admin/config-urls.html.textile.liquid [new file with mode: 0644]
doc/admin/metrics.html.textile.liquid
doc/install/install-manual-prerequisites.html.textile.liquid
doc/install/install-postgresql.html.textile.liquid
lib/controller/dblock/dblock.go
services/keep-balance/balance.go
services/keep-balance/balance_run_test.go
services/keep-balance/balance_test.go
services/keepstore/unix_volume.go

index dde87323d778e47f7e6297da784a1995d9f9726d..83be731e8173db39891985c6e0c6b953d63706bb 100644 (file)
@@ -226,6 +226,7 @@ navbar:
       - install/config.html.textile.liquid
       - admin/config-migration.html.textile.liquid
       - admin/config.html.textile.liquid
+      - admin/config-urls.html.textile.liquid
     - Core:
       - install/install-api-server.html.textile.liquid
     - Keep:
diff --git a/doc/admin/config-urls.html.textile.liquid b/doc/admin/config-urls.html.textile.liquid
new file mode 100644 (file)
index 0000000..1358fd8
--- /dev/null
@@ -0,0 +1,274 @@
+---
+layout: default
+navsection: installguide
+title: InternalURLs and ExternalURL
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+The Arvados configuration is stored at @/etc/arvados/config.yml@. See the "Configuration reference":config.html for more detail.
+
+The @Services@ section lists a number of Arvados services, each with an @InternalURLs@ and/or @ExternalURL@ configuration key. This document explains the precise meaning of these configuration keys, and how they are used by the Arvados services.
+
+The @ExternalURL@ is the address where the service should be reachable by clients, both from inside and from outside the Arvados cluster. Some services do not expose an Arvados API, only Prometheus metrics. In that case, @ExternalURL@ is not used.
+
+The keys under @InternalURLs@ are addresses that are used by the reverse proxy (e.g. Nginx) that fronts Arvados services. The exception is the @Keepstore@ service, where clients connect directly to the addresses listed under @InternalURLs@. If a service is not fronted by a reverse proxy, e.g. when its endpoint only exposes Prometheus metrics, the intention is that metrics are collected directly from the endpoints defined in @InternalURLs@.
+
+@InternalURLs@ are also used by the service itself to figure out which address/port to listen on.
+
+If the Arvados service lives behind a reverse proxy (e.g. Nginx), configuring the reverse proxy and the @InternalURLs@ and @ExternalURL@ values must be done in concert.
+
+h2. Overview
+
+<div class="offset1">
+table(table table-bordered table-condensed).
+|_.Service     |_.ExternalURL required? |_.InternalURLs required?|_.InternalURLs must be reachable from other cluster nodes?|_.Note|
+|railsapi       |no                     |yes|no ^1^|InternalURLs only used by Controller|
+|controller     |yes                    |yes|no ^2^|InternalURLs only used by reverse proxy (e.g. Nginx)|
+|arvados-dispatch-cloud|no              |yes|no ^3^|InternalURLs only used to expose Prometheus metrics|
+|arvados-dispatch-lsf|no                |yes|no ^3^|InternalURLs only used to expose Prometheus metrics|
+|git-http       |yes                    |yes|no ^2^|InternalURLs only used by reverse proxy (e.g. Nginx)|
+|git-ssh        |yes                    |no |no    ||
+|keepproxy      |yes                    |yes|no ^2^|InternalURLs only used by reverse proxy (e.g. Nginx)|
+|keepstore      |no                     |yes|yes   |All clients connect to InternalURLs|
+|keep-balance   |no                     |yes|no ^3^|InternalURLs only used to expose Prometheus metrics|
+|keep-web       |yes                    |yes|no ^2^|InternalURLs only used by reverse proxy (e.g. Nginx)|
+|websocket      |yes                    |yes|no ^2^|InternalURLs only used by reverse proxy (e.g. Nginx)|
+|workbench1     |yes                    |no|no     ||
+|workbench2     |yes                    |no|no     ||
+</div>
+
+^1^ If @Controller@ runs on a different host than @RailsAPI@, the @InternalURLs@ will need to be reachable from the host that runs @Controller@.
+^2^ If the reverse proxy (e.g. Nginx) does not run on the same host as the Arvados service it fronts, the @InternalURLs@ will need to be reachable from the host that runs the reverse proxy.
+^3^ If the Prometheus metrics are not collected from the same machine that runs the service, the @InternalURLs@ will need to be reachable from the host that collects the metrics.
+
+When @InternalURLs@ do not need to be reachable from other nodes, it is most secure to use loopback addresses as @InternalURLs@, e.g. @http://127.0.0.1:9005@.
+
+It is recommended to use a split-horizon DNS setup where the hostnames specified in @ExternalURL@ resolve to an internal IP address from inside the Arvados cluster, and a publicly routed external IP address when resolved from outside the cluster. This simplifies firewalling and provides optimally efficient traffic routing. In a cloud environment where traffic that flows via public IP addresses is charged, using split horizon DNS can also avoid unnecessary expense.
+
+h2. Examples
+
+The remainder of this document walks through a number of examples to provide more detail.
+
+h3. Keep-balance
+
+Consider this section for the @Keep-balance@ service:
+
+{% codeblock as yaml %}
+      Keepbalance:
+        InternalURLs:
+          "http://ip-10-0-1-233.internal:9005/": {}
+{% endcodeblock %}
+
+@Keep-balance@ has an API endpoint, but it is only used to expose "Prometheus":https://prometheus.io metrics.
+
+There is no @ExternalURL@ key because @Keep-balance@ does not have an Arvados API, no Arvados services need to connect to @Keep-balance@.
+
+The value for @InternalURLs@ tells the @Keep-balance@ service to start up and listen on port 9005, if it is started on a host where @ip-10-0-1-233.internal@ resolves to a local IP address. If @Keep-balance@ is started on a machine where the @ip-10-0-1-233.internal@ hostname does not resolve to a local IP address, it would refuse to start up, because it would not be able to find a local IP address to listen on.
+
+It is also possible to use IP addresses in @InternalURLs@, for example:
+
+{% codeblock as yaml %}
+      Keepbalance:
+        InternalURLs:
+          "http://127.0.0.1:9005/": {}
+{% endcodeblock %}
+
+In this example, @Keep-balance@ would start up and listen on port 9005 at the @127.0.0.1@ IP address. Prometheus would only be able to access the @Keep-balance@ metrics if it could reach that IP and port, e.g. if it runs on the same machine.
+
+Finally, it is also possible to listen on all interfaces, for example:
+
+{% codeblock as yaml %}
+      Keepbalance:
+        InternalURLs:
+          "http://0.0.0.0:9005/": {}
+{% endcodeblock %}
+
+In this case, @Keep-balance@ will listen on port 9005 on all IP addresses local to the machine.
+
+h3. Keepstore
+
+Consider this section for the @Keepstore@ service:
+
+{% codeblock as yaml %}
+      Keepstore:
+        InternalURLs:
+          "http://keep0.ClusterID.example.com:25107": {}
+          "http://keep1.ClusterID.example.com:25107": {}
+{% endcodeblock %}
+
+There is no @ExternalURL@ key because @Keepstore@ is only accessed from inside the Arvados cluster. For access from outside, all traffic goes via @Keepproxy@.
+
+When @Keepstore@ is installed on the host where @keep0.ClusterID.example.com@ resolves to a local IP address, it will listen on port 25107 on that IP address. Likewise on the @keep1.ClusterID.example.com@ host. On all other systems, @Keepstore@ will refuse to start.
+
+h3. Keepproxy
+
+Consider this section for the @Keepproxy@ service:
+
+{% codeblock as yaml %}
+      Keepproxy:
+        ExternalURL: https://keep.ClusterID.example.com
+        InternalURLs:
+          "http://localhost:25107": {}
+{% endcodeblock %}
+
+The @ExternalURL@ advertised is @https://keep.ClusterID.example.com@. The @Keepproxy@ service will start up on @localhost@ port 25107, however. This is possible because we also configure Nginx to terminate SSL and sit in front of the @Keepproxy@ service:
+
+<notextile><pre><code>upstream keepproxy {
+  server                127.0.0.1:<span class="userinput">25107</span>;
+}
+
+server {
+  listen                  443 ssl;
+  server_name             <span class="userinput">keep.ClusterID.example.com</span>;
+
+  proxy_connect_timeout   90s;
+  proxy_read_timeout      300s;
+  proxy_set_header        X-Real-IP $remote_addr;
+  proxy_http_version      1.1;
+  proxy_request_buffering off;
+  proxy_max_temp_file_size 0;
+
+  ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
+  ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
+
+  # Clients need to be able to upload blocks of data up to 64MiB in size.
+  client_max_body_size    64m;
+
+  location / {
+    proxy_pass            http://keepproxy;
+  }
+}
+</code></pre></notextile>
+
+If a client connects to the @Keepproxy@ service, it will talk to Nginx which will reverse proxy the traffic to the @Keepproxy@ service.
+
+h3. Workbench
+
+Consider this section for the @Workbench@ service:
+
+{% codeblock as yaml %}
+  Workbench1:
+    ExternalURL: "https://workbench.ClusterID.example.com"
+{% endcodeblock %}
+
+The @ExternalURL@ advertised is @https://workbench.ClusterID.example.com@. There is no value for @InternalURLs@ because Workbench1 is a Rails application served by Passenger. The only client connecting to the Passenger process is the reverse proxy (e.g. Nginx), and the listening host/post is configured in its configuration:
+
+<notextile><pre><code>
+server {
+  listen       443 ssl;
+  server_name  workbench.ClusterID.example.com;
+
+  ssl_certificate     /YOUR/PATH/TO/cert.pem;
+  ssl_certificate_key /YOUR/PATH/TO/cert.key;
+
+  root /var/www/arvados-workbench/current/public;
+  index  index.html;
+
+  passenger_enabled on;
+  # If you're using RVM, uncomment the line below.
+  #passenger_ruby /usr/local/rvm/wrappers/default/ruby;
+
+  # `client_max_body_size` should match the corresponding setting in
+  # the API.MaxRequestSize and Controller's server's Nginx configuration.
+  client_max_body_size 128m;
+}
+</code></pre></notextile>
+
+h3. API server
+
+Consider this section for the @RailsAPI@ service:
+
+{% codeblock as yaml %}
+      RailsAPI:
+        InternalURLs:
+          "http://localhost:8004": {}
+{% endcodeblock %}
+
+There is no @ExternalURL@ defined because the @RailsAPI@ is not directly accessible and does not need to advertise a URL: all traffic to it flows via @Controller@, which is the only client that talks to it.
+
+The @RailsAPI@ service is also a Rails application, and its listening host/port is defined in the Nginx configuration:
+
+<notextile><pre><code>
+server {
+  # This configures the Arvados API server.  It is written using Ruby
+  # on Rails and uses the Passenger application server.
+
+  listen localhost:8004;
+  server_name localhost-api;
+
+  root /var/www/arvados-api/current/public;
+  index  index.html index.htm index.php;
+
+  passenger_enabled on;
+
+  # If you are using RVM, uncomment the line below.
+  # If you're using system ruby, leave it commented out.
+  #passenger_ruby /usr/local/rvm/wrappers/default/ruby;
+
+  # This value effectively limits the size of API objects users can
+  # create, especially collections.  If you change this, you should
+  # also ensure the following settings match it:
+  # * `client_max_body_size` in the previous server section
+  # * `API.MaxRequestSize` in config.yml
+  client_max_body_size 128m;
+}
+</code></pre></notextile>
+
+So then, why is there a need to specify @InternalURLs@ for the @RailsAPI@ service? It is there because this is how the @Controller@ service locates the @RailsAPI@ service it should talk to. Since this connection is internal to the Arvados cluster, @Controller@ uses @InternalURLs@ to find the @RailsAPI@ endpoint.
+
+h3. Controller
+
+Consider this section for the @Controller@ service:
+
+{% codeblock as yaml %}
+  Controller:
+    InternalURLs:
+      "http://localhost:8003": {}
+    ExternalURL: "https://ClusterID.example.com"
+{% endcodeblock %}
+
+The @ExternalURL@ advertised is @https://ClusterID.example.com@. The @Controller@ service will start up on @localhost@ port 8003. Nginx is configured to sit in front of the @Controller@ service and terminates SSL:
+
+<notextile><pre><code>
+# This is the port where nginx expects to contact arvados-controller.
+upstream controller {
+  server     localhost:8003  fail_timeout=10s;
+}
+
+server {
+  # This configures the public https port that clients will actually connect to,
+  # the request is reverse proxied to the upstream 'controller'
+
+  listen       443 ssl;
+  server_name  ClusterID.example.com;
+
+  ssl_certificate     /YOUR/PATH/TO/cert.pem;
+  ssl_certificate_key /YOUR/PATH/TO/cert.key;
+
+  # Refer to the comment about this setting in the passenger (arvados
+  # api server) section of your Nginx configuration.
+  client_max_body_size 128m;
+
+  location / {
+    proxy_pass            http://controller;
+    proxy_redirect        off;
+    proxy_connect_timeout 90s;
+    proxy_read_timeout    300s;
+
+    proxy_set_header      Host              $http_host;
+    proxy_set_header      Upgrade           $http_upgrade;
+    proxy_set_header      Connection        "upgrade";
+    proxy_set_header      X-External-Client $external_client;
+    proxy_set_header      X-Forwarded-For   $proxy_add_x_forwarded_for;
+    proxy_set_header      X-Forwarded-Proto https;
+    proxy_set_header      X-Real-IP         $remote_addr;
+  }
+}
+</code></pre></notextile>
+
+
index 0cfa0a2e604cc0ee40bcbe3cc1a44836b3247b72..b140bcc1badda0c2996725bf62a026345c0646c6 100644 (file)
@@ -34,6 +34,7 @@ table(table table-bordered table-condensed table-hover).
 |arvados-api-server||
 |arvados-controller|✓|
 |arvados-dispatch-cloud|✓|
+|arvados-dispatch-lsf|✓|
 |arvados-git-httpd||
 |arvados-ws|✓|
 |composer||
index 084f32e029c4c3a99e4db207b5b07a4f51374e36..360cfbabdd21639dd8d27e2947f13be7ce6f342b 100644 (file)
@@ -50,7 +50,7 @@ Arvados consists of many components, some of which may be omitted (at the cost o
 table(table table-bordered table-condensed).
 |\3=. *Core*|
 |"PostgreSQL database":install-postgresql.html |Stores data for the API server.|Required.|
-|"API server":install-api-server.html |Core Arvados logic for managing users, groups, collections, containers, and enforcing permissions.|Required.|
+|"API server + Controller":install-api-server.html |Core Arvados logic for managing users, groups, collections, containers, and enforcing permissions.|Required.|
 |\3=. *Keep (storage)*|
 |"Keepstore":install-keepstore.html |Stores content-addressed blocks in a variety of backends (local filesystem, cloud object storage).|Required.|
 |"Keepproxy":install-keepproxy.html |Gateway service to access keep servers from external networks.|Required to be able to use arv-put, arv-get, or arv-mount outside the private Arvados network.|
@@ -65,7 +65,8 @@ table(table table-bordered table-condensed).
 |"Git server":install-arv-git-httpd.html |Arvados-hosted git repositories, with Arvados-token based authentication.|Optional, but required by Workflow Composer.|
 |\3=. *Crunch (running containers)*|
 |"arvados-dispatch-cloud":crunch2-cloud/install-dispatch-cloud.html |Allocate and free cloud VM instances on demand based on workload.|Optional, not needed for a static Slurm cluster such as on-premises HPC.|
-|"crunch-dispatch-slurm":crunch2-slurm/install-dispatch.html |Run analysis workflows using Docker containers distributed across a Slurm cluster.|Optional, not needed for a Cloud installation, or if you wish to use Arvados for data management only.|
+|"crunch-dispatch-slurm":crunch2-slurm/install-dispatch.html |Run analysis workflows using Docker or Singularity containers distributed across a Slurm cluster.|Optional, not needed for a Cloud installation, or if you wish to use Arvados for data management only.|
+|"crunch-dispatch-lsf":crunch2-lsf/install-dispatch.html |Run analysis workflows using Docker or Singularity containers distributed across an LSF cluster.|Optional, not needed for a Cloud installation, or if you wish to use Arvados for data management only.|
 
 h2(#identity). Identity provider
 
@@ -97,7 +98,8 @@ h2(#scheduler). Container compute scheduler
 Choose which backend you will use to schedule computation.
 
 * On AWS EC2 and Azure, you probably want to use @arvados-dispatch-cloud@ to manage the full lifecycle of cloud compute nodes: starting up nodes sized to the container request, executing containers on those nodes, and shutting nodes down when no longer needed.
-* For on-premise HPC clusters using "slurm":https://slurm.schedmd.com/ use @crunch-dispatch-slurm@ to execute containers with slurm job submissions.
+* For on-premises HPC clusters using "slurm":https://slurm.schedmd.com/ use @crunch-dispatch-slurm@ to execute containers with slurm job submissions.
+* For on-premises HPC clusters using "LSF":https://www.ibm.com/products/hpc-workload-management/ use @crunch-dispatch-lsf@ to execute containers with slurm job submissions.
 * For single node demos, use @crunch-dispatch-local@ to execute containers directly.
 
 h2(#machines). Hardware (or virtual machines)
@@ -117,7 +119,7 @@ table(table table-bordered table-condensed).
 </div>
 
 ^1^ Should be scaled up as needed
-^2^ Refers to shell nodes managed by Arvados, that provide ssh access for users to interact with Arvados at the command line.  Optional.
+^2^ Refers to shell nodes managed by Arvados that provide ssh access for users to interact with Arvados at the command line.  Optional.
 
 {% include 'notebox_begin' %}
 For a small demo installation, it is possible to run all the Arvados services on a single node.  Special considerations for single-node installs will be noted in boxes like this.
@@ -145,6 +147,7 @@ h2(#dnstls). DNS entries and TLS certificates
 The following services are normally public-facing and require DNS entries and corresponding TLS certificates.  Get certificates from your preferred TLS certificate provider.  We recommend using "Let's Encrypt":https://letsencrypt.org/.  You can run several services on the same node, but each distinct DNS name requires a valid, matching TLS certificate.
 
 This guide uses the following DNS name conventions.  A later part of this guide will describe how to set up Nginx virtual hosts.
+It is possible to use custom DNS names for the Arvados services.
 
 <div class="offset1">
 table(table table-bordered table-condensed).
index 60afa1e24fa51b50237c308128b708d006d71d24..1413890cde7dbd5f6be60f64f7ffe9e32a552525 100644 (file)
@@ -11,9 +11,14 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 Arvados requires at least version *9.4* of PostgreSQL.
 
+* "AWS":#aws
 * "CentOS 7":#centos7
 * "Debian or Ubuntu":#debian
 
+h3(#aws). AWS
+
+When deploying on AWS, Arvados can use an Aurora RDS PostgreSQL database. Aurora Serverless is not recommended.
+
 h3(#centos7). CentOS 7
 {% assign rh_version = "7" %}
 {% include 'note_python_sc' %}
@@ -35,4 +40,4 @@ Debian 10 (Buster) and Ubuntu 16.04 (Xenial) and later versions include a suffic
 # Install PostgreSQL
   <notextile><pre># <span class="userinput">apt-get --no-install-recommends install postgresql postgresql-contrib</span></pre></notextile>
 # Configure the database to launch at boot and start now
-  <notextile><pre># <span class="userinput">systemctl enable --now postgresql</span></pre></notextile>
+<notextile><pre># <span class="userinput">systemctl enable --now postgresql</span></pre></notextile>
index b0d348870b180adc42aeadc48ba8110e38f380a7..1a36822d5b7f91e81c5b0deb167a105a962b3dfb 100644 (file)
@@ -35,8 +35,8 @@ func (dbl *DBLocker) Lock(ctx context.Context, getdb func(context.Context) (*sql
        for ; ; time.Sleep(retryDelay) {
                dbl.mtx.Lock()
                if dbl.conn != nil {
-                       // Already locked by another caller in this
-                       // process. Wait for them to release.
+                       // Another goroutine is already locked/waiting
+                       // on this lock. Wait for them to release.
                        dbl.mtx.Unlock()
                        continue
                }
@@ -52,9 +52,15 @@ func (dbl *DBLocker) Lock(ctx context.Context, getdb func(context.Context) (*sql
                        dbl.mtx.Unlock()
                        continue
                }
-               _, err = conn.ExecContext(ctx, `SELECT pg_advisory_lock($1)`, dbl.key)
+               var locked bool
+               err = conn.QueryRowContext(ctx, `SELECT pg_try_advisory_lock($1)`, dbl.key).Scan(&locked)
                if err != nil {
-                       logger.WithError(err).Infof("error getting pg_advisory_lock %d", dbl.key)
+                       logger.WithError(err).Infof("error getting pg_try_advisory_lock %d", dbl.key)
+                       conn.Close()
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               if !locked {
                        conn.Close()
                        dbl.mtx.Unlock()
                        continue
index bb590e13b33f0535d5a7d2610d8902ddab577300..eb6f580f435c56072ad381f984703e229c524f29 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "context"
        "crypto/md5"
+       "errors"
        "fmt"
        "io"
        "io/ioutil"
@@ -217,8 +218,8 @@ func (bal *Balancer) cleanupMounts() {
        rwdev := map[string]*KeepService{}
        for _, srv := range bal.KeepServices {
                for _, mnt := range srv.mounts {
-                       if !mnt.ReadOnly && mnt.DeviceID != "" {
-                               rwdev[mnt.DeviceID] = srv
+                       if !mnt.ReadOnly {
+                               rwdev[mnt.UUID] = srv
                        }
                }
        }
@@ -227,8 +228,8 @@ func (bal *Balancer) cleanupMounts() {
        for _, srv := range bal.KeepServices {
                var dedup []*KeepMount
                for _, mnt := range srv.mounts {
-                       if mnt.ReadOnly && rwdev[mnt.DeviceID] != nil {
-                               bal.logf("skipping srv %s readonly mount %q because same device %q is mounted read-write on srv %s", srv, mnt.UUID, mnt.DeviceID, rwdev[mnt.DeviceID])
+                       if mnt.ReadOnly && rwdev[mnt.UUID] != nil {
+                               bal.logf("skipping srv %s readonly mount %q because same volume is mounted read-write on srv %s", srv, mnt.UUID, rwdev[mnt.UUID])
                        } else {
                                dedup = append(dedup, mnt)
                        }
@@ -266,6 +267,29 @@ func (bal *Balancer) CheckSanityEarly(c *arvados.Client) error {
                }
        }
 
+       mountProblem := false
+       type deviceMount struct {
+               srv *KeepService
+               mnt *KeepMount
+       }
+       deviceMounted := map[string]deviceMount{} // DeviceID -> mount
+       for _, srv := range bal.KeepServices {
+               for _, mnt := range srv.mounts {
+                       if first, dup := deviceMounted[mnt.DeviceID]; dup && first.mnt.UUID != mnt.UUID && mnt.DeviceID != "" {
+                               bal.logf("config error: device %s is mounted with multiple volume UUIDs: %s on %s, and %s on %s",
+                                       mnt.DeviceID,
+                                       first.mnt.UUID, first.srv,
+                                       mnt.UUID, srv)
+                               mountProblem = true
+                               continue
+                       }
+                       deviceMounted[mnt.DeviceID] = deviceMount{srv, mnt}
+               }
+       }
+       if mountProblem {
+               return errors.New("cannot continue with config errors (see above)")
+       }
+
        var checkPage arvados.CollectionList
        if err = c.RequestAndDecode(&checkPage, "GET", "arvados/v1/collections", nil, arvados.ResourceListParams{
                Limit:              new(int),
@@ -357,12 +381,10 @@ func (bal *Balancer) GetCurrentState(ctx context.Context, c *arvados.Client, pag
        deviceMount := map[string]*KeepMount{}
        for _, srv := range bal.KeepServices {
                for _, mnt := range srv.mounts {
-                       equiv := deviceMount[mnt.DeviceID]
+                       equiv := deviceMount[mnt.UUID]
                        if equiv == nil {
                                equiv = mnt
-                               if mnt.DeviceID != "" {
-                                       deviceMount[mnt.DeviceID] = equiv
-                               }
+                               deviceMount[mnt.UUID] = equiv
                        }
                        equivMount[equiv] = append(equivMount[equiv], mnt)
                }
@@ -667,7 +689,7 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                                // new/remaining replicas uniformly
                                // across qualifying mounts on a given
                                // server.
-                               return rendezvousLess(si.mnt.DeviceID, sj.mnt.DeviceID, blkid)
+                               return rendezvousLess(si.mnt.UUID, sj.mnt.UUID, blkid)
                        }
                })
 
@@ -692,7 +714,7 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                // and returns true if all requirements are met.
                trySlot := func(i int) bool {
                        slot := slots[i]
-                       if wantMnt[slot.mnt] || wantDev[slot.mnt.DeviceID] {
+                       if wantMnt[slot.mnt] || wantDev[slot.mnt.UUID] {
                                // Already allocated a replica to this
                                // backend device, possibly on a
                                // different server.
@@ -707,9 +729,7 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                                slots[i].want = true
                                wantSrv[slot.mnt.KeepService] = true
                                wantMnt[slot.mnt] = true
-                               if slot.mnt.DeviceID != "" {
-                                       wantDev[slot.mnt.DeviceID] = true
-                               }
+                               wantDev[slot.mnt.UUID] = true
                                replWant += slot.mnt.Replication
                        }
                        return replProt >= desired && replWant >= desired
@@ -751,7 +771,7 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                // haven't already been added to unsafeToDelete
                // because the servers report different Mtimes.
                for _, slot := range slots {
-                       if slot.repl != nil && wantDev[slot.mnt.DeviceID] {
+                       if slot.repl != nil && wantDev[slot.mnt.UUID] {
                                unsafeToDelete[slot.repl.Mtime] = true
                        }
                }
@@ -834,7 +854,7 @@ func computeBlockState(slots []slot, onlyCount map[*KeepMount]bool, have, needRe
                if onlyCount != nil && !onlyCount[slot.mnt] {
                        continue
                }
-               if countedDev[slot.mnt.DeviceID] {
+               if countedDev[slot.mnt.UUID] {
                        continue
                }
                switch {
@@ -848,9 +868,7 @@ func computeBlockState(slots []slot, onlyCount map[*KeepMount]bool, have, needRe
                        bbs.pulling++
                        repl += slot.mnt.Replication
                }
-               if slot.mnt.DeviceID != "" {
-                       countedDev[slot.mnt.DeviceID] = true
-               }
+               countedDev[slot.mnt.UUID] = true
        }
        if repl < needRepl {
                bbs.unachievable = true
index 4e2c6803ca81cc593798d8285945aa9f90a1446c..0d1b6b5912d2820be5a40f1000871b02c1321e0e 100644 (file)
@@ -397,6 +397,32 @@ func (s *runSuite) TestRefuseNonAdmin(c *check.C) {
        c.Check(pullReqs.Count(), check.Equals, 0)
 }
 
+func (s *runSuite) TestRefuseSameDeviceDifferentVolumes(c *check.C) {
+       opts := RunOptions{
+               CommitPulls: true,
+               CommitTrash: true,
+               Logger:      ctxlog.TestLogger(c),
+       }
+       s.stub.serveCurrentUserAdmin()
+       s.stub.serveZeroCollections()
+       s.stub.serveKeepServices(stubServices)
+       s.stub.mux.HandleFunc("/mounts", func(w http.ResponseWriter, r *http.Request) {
+               hostid := r.Host[:5] // "keep0.zzzzz.arvadosapi.com:25107" => "keep0"
+               json.NewEncoder(w).Encode([]arvados.KeepMount{{
+                       UUID:           "zzzzz-ivpuk-0000000000" + hostid,
+                       DeviceID:       "keep0-vol0",
+                       StorageClasses: map[string]bool{"default": true},
+               }})
+       })
+       trashReqs := s.stub.serveKeepstoreTrash()
+       pullReqs := s.stub.serveKeepstorePull()
+       srv := s.newServer(&opts)
+       _, err := srv.runOnce()
+       c.Check(err, check.ErrorMatches, "cannot continue with config errors.*")
+       c.Check(trashReqs.Count(), check.Equals, 0)
+       c.Check(pullReqs.Count(), check.Equals, 0)
+}
+
 func (s *runSuite) TestWriteLostBlocks(c *check.C) {
        lostf, err := ioutil.TempFile("", "keep-balance-lost-blocks-test-")
        c.Assert(err, check.IsNil)
index c529ac150e092c37c9f5510d0463be6a0eebe553..df04145b9dd15e3d1066b0a1d5a71c57ba27a5f8 100644 (file)
@@ -167,8 +167,8 @@ func (bal *balancerSuite) testMultipleViews(c *check.C, readonly bool) {
                srv.mounts[0].KeepMount.DeviceID = fmt.Sprintf("writable-by-srv-%x", i)
                srv.mounts = append(srv.mounts, &KeepMount{
                        KeepMount: arvados.KeepMount{
-                               DeviceID:       fmt.Sprintf("writable-by-srv-%x", (i+1)%len(bal.srvs)),
-                               UUID:           fmt.Sprintf("zzzzz-mount-%015x", i<<16),
+                               DeviceID:       bal.srvs[(i+1)%len(bal.srvs)].mounts[0].KeepMount.DeviceID,
+                               UUID:           bal.srvs[(i+1)%len(bal.srvs)].mounts[0].KeepMount.UUID,
                                ReadOnly:       readonly,
                                Replication:    1,
                                StorageClasses: map[string]bool{"default": true},
@@ -347,6 +347,7 @@ func (bal *balancerSuite) TestDecreaseReplBlockTooNew(c *check.C) {
 func (bal *balancerSuite) TestCleanupMounts(c *check.C) {
        bal.srvs[3].mounts[0].KeepMount.ReadOnly = true
        bal.srvs[3].mounts[0].KeepMount.DeviceID = "abcdef"
+       bal.srvs[14].mounts[0].KeepMount.UUID = bal.srvs[3].mounts[0].KeepMount.UUID
        bal.srvs[14].mounts[0].KeepMount.DeviceID = "abcdef"
        c.Check(len(bal.srvs[3].mounts), check.Equals, 1)
        bal.cleanupMounts()
@@ -485,32 +486,32 @@ func (bal *balancerSuite) TestVolumeReplication(c *check.C) {
 }
 
 func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
-       bal.srvs[0].mounts[0].KeepMount.DeviceID = "abcdef"
-       bal.srvs[9].mounts[0].KeepMount.DeviceID = "abcdef"
-       bal.srvs[14].mounts[0].KeepMount.DeviceID = "abcdef"
+       dupUUID := bal.srvs[0].mounts[0].KeepMount.UUID
+       bal.srvs[9].mounts[0].KeepMount.UUID = dupUUID
+       bal.srvs[14].mounts[0].KeepMount.UUID = dupUUID
        // block 0 belongs on servers 3 and e, which have different
-       // device IDs.
+       // UUIDs.
        bal.try(c, tester{
                known:      0,
                desired:    map[string]int{"default": 2},
                current:    slots{1},
                shouldPull: slots{0}})
        // block 1 belongs on servers 0 and 9, which both report
-       // having a replica, but the replicas are on the same device
-       // ID -- so we should pull to the third position (7).
+       // having a replica, but the replicas are on the same volume
+       // -- so we should pull to the third position (7).
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 2},
                current:    slots{0, 1},
                shouldPull: slots{2}})
-       // block 1 can be pulled to the doubly-mounted device, but the
+       // block 1 can be pulled to the doubly-mounted volume, but the
        // pull should only be done on the first of the two servers.
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 2},
                current:    slots{2},
                shouldPull: slots{0}})
-       // block 0 has one replica on a single device mounted on two
+       // block 0 has one replica on a single volume mounted on two
        // servers (e,9 at positions 1,9). Trashing the replica on 9
        // would lose the block.
        bal.try(c, tester{
@@ -523,7 +524,7 @@ func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
                        pulling: 1,
                }})
        // block 0 is overreplicated, but the second and third
-       // replicas are the same replica according to DeviceID
+       // replicas are the same replica according to volume UUID
        // (despite different Mtimes). Don't trash the third replica.
        bal.try(c, tester{
                known:   0,
@@ -595,7 +596,7 @@ func (bal *balancerSuite) TestChangeStorageClasses(c *check.C) {
                desired:          map[string]int{"default": 2, "special": 1},
                current:          slots{0, 1},
                shouldPull:       slots{9},
-               shouldPullMounts: []string{"zzzzz-mount-special00000009"}})
+               shouldPullMounts: []string{"zzzzz-mount-special20000009"}})
        // If some storage classes are not satisfied, don't trash any
        // excess replicas. (E.g., if someone desires repl=1 on
        // class=durable, and we have two copies on class=volatile, we
@@ -605,7 +606,7 @@ func (bal *balancerSuite) TestChangeStorageClasses(c *check.C) {
                desired:          map[string]int{"special": 1},
                current:          slots{0, 1},
                shouldPull:       slots{9},
-               shouldPullMounts: []string{"zzzzz-mount-special00000009"}})
+               shouldPullMounts: []string{"zzzzz-mount-special20000009"}})
        // Once storage classes are satisfied, trash excess replicas
        // that appear earlier in probe order but aren't needed to
        // satisfy the desired classes.
index 46f4db4095bfb286c82f4f07a988c16cee5ebe63..a053ba3e6b19042c48423cf31303e8cc2059fa1d 100644 (file)
@@ -379,23 +379,25 @@ func (v *UnixVolume) IndexTo(prefix string, w io.Writer) error {
                        continue
                }
                blockdirpath := filepath.Join(v.Root, subdir)
-               blockdir, err := v.os.Open(blockdirpath)
-               if err != nil {
-                       v.logger.WithError(err).Errorf("error reading %q", blockdirpath)
-                       return fmt.Errorf("error reading %q: %s", blockdirpath, err)
-               }
-               v.os.stats.TickOps("readdir")
-               v.os.stats.Tick(&v.os.stats.ReaddirOps)
-               // ReadDir() (compared to Readdir(), which returns
-               // FileInfo structs) helps complete the sequence of
-               // readdirent calls as quickly as possible, reducing
-               // the likelihood of NFS EBADCOOKIE (523) errors.
-               dirents, err := blockdir.ReadDir(-1)
-               blockdir.Close()
-               if err != nil {
-                       v.logger.WithError(err).Errorf("error reading %q", blockdirpath)
-                       return fmt.Errorf("error reading %q: %s", blockdirpath, err)
+
+               var dirents []os.DirEntry
+               for attempt := 0; ; attempt++ {
+                       v.os.stats.TickOps("readdir")
+                       v.os.stats.Tick(&v.os.stats.ReaddirOps)
+                       dirents, err = os.ReadDir(blockdirpath)
+                       if err == nil {
+                               break
+                       } else if attempt < 5 && strings.Contains(err.Error(), "errno 523") {
+                               // EBADCOOKIE (NFS stopped accepting
+                               // our readdirent cookie) -- retry a
+                               // few times before giving up
+                               v.logger.WithError(err).Printf("retry after error reading %s", blockdirpath)
+                               continue
+                       } else {
+                               return err
+                       }
                }
+
                for _, dirent := range dirents {
                        fileInfo, err := dirent.Info()
                        if os.IsNotExist(err) {