github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/roachtest/toxiproxy.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package main
    12  
    13  import (
    14  	"context"
    15  	gosql "database/sql"
    16  	"fmt"
    17  	"net/url"
    18  	"regexp"
    19  	"runtime"
    20  	"strconv"
    21  	"time"
    22  
    23  	toxiproxy "github.com/Shopify/toxiproxy/client"
    24  	"github.com/cockroachdb/errors"
    25  )
    26  
    27  // cockroachToxiWrapper replaces the cockroach binary. It modifies the listening port so
    28  // that the nodes in the cluster will communicate through toxiproxy instead of
    29  // directly.
    30  const cockroachToxiWrapper = `#!/usr/bin/env bash
    31  set -eu
    32  
    33  cd "$(dirname "${0}")"
    34  
    35  orig_port=""
    36  
    37  args=()
    38  
    39  if [[ "$1" != "start" ]]; then
    40  	./cockroach.real "$@"
    41  	exit $?
    42  fi
    43  
    44  for arg in "$@"; do
    45  	capture=$(echo "${arg}" | sed -E 's/^--port=([0-9]+)$/\1/')
    46  	if [[ "${capture}" != "${arg}"  ]] && [[ -z "${orig_port}" ]] && [[ -n "${capture}" ]]; then
    47  		orig_port="${capture}"
    48  	fi
    49  	args+=("${arg}")
    50  done
    51  
    52  if [[ -z "${orig_port}" ]]; then
    53  	orig_port=26257
    54  fi
    55  
    56  args+=("--advertise-port=$((orig_port+10000))")
    57  
    58  echo "toxiproxy interception:"
    59  echo "original args: $@"
    60  echo "modified args: ${args[@]}"
    61  ./cockroach.real "${args[@]}"
    62  `
    63  
    64  const toxiServerWrapper = `#!/usr/bin/env bash
    65  set -eu
    66  
    67  mkdir -p logs
    68  ./toxiproxy-server -host 0.0.0.0 -port $1 2>&1 > logs/toxiproxy.log & </dev/null
    69  until nc -z localhost $1; do sleep 0.1; echo "waiting for toxiproxy-server..."; done
    70  `
    71  
    72  // A ToxiCluster wraps a cluster and sets it up for use with toxiproxy.
    73  // See Toxify() for details.
    74  type ToxiCluster struct {
    75  	*cluster
    76  	toxClients map[int]*toxiproxy.Client
    77  	toxProxies map[int]*toxiproxy.Proxy
    78  }
    79  
    80  // Toxify takes a cluster and sets it up for use with toxiproxy on the given
    81  // nodes. On these nodes, the cockroach binary must already have been populated
    82  // and the cluster must not have been started yet. The returned ToxiCluster
    83  // wraps the original cluster, whose returned addresses will all go through
    84  // toxiproxy. The upstream (i.e. non-intercepted) addresses are accessible via
    85  // getters prefixed with "External".
    86  func Toxify(ctx context.Context, c *cluster, node nodeListOption) (*ToxiCluster, error) {
    87  	toxiURL := "https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-linux-amd64"
    88  	if local && runtime.GOOS == "darwin" {
    89  		toxiURL = "https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-darwin-amd64"
    90  	}
    91  	if err := func() error {
    92  		if err := c.RunE(ctx, c.All(), "curl", "-Lfo", "toxiproxy-server", toxiURL); err != nil {
    93  			return err
    94  		}
    95  		if err := c.RunE(ctx, c.All(), "chmod", "+x", "toxiproxy-server"); err != nil {
    96  			return err
    97  		}
    98  
    99  		if err := c.RunE(ctx, node, "mv cockroach cockroach.real"); err != nil {
   100  			return err
   101  		}
   102  		if err := c.PutString(ctx, cockroachToxiWrapper, "./cockroach", 0755, node); err != nil {
   103  			return err
   104  		}
   105  		return c.PutString(ctx, toxiServerWrapper, "./toxiproxyd", 0755, node)
   106  	}(); err != nil {
   107  		return nil, errors.Wrap(err, "toxify")
   108  	}
   109  
   110  	tc := &ToxiCluster{
   111  		cluster:    c,
   112  		toxClients: make(map[int]*toxiproxy.Client),
   113  		toxProxies: make(map[int]*toxiproxy.Proxy),
   114  	}
   115  
   116  	for _, i := range node {
   117  		n := c.Node(i)
   118  
   119  		toxPort := 8474 + i
   120  		if err := c.RunE(ctx, n, fmt.Sprintf("./toxiproxyd %d 2>/dev/null >/dev/null < /dev/null", toxPort)); err != nil {
   121  			return nil, errors.Wrap(err, "toxify")
   122  		}
   123  
   124  		externalAddr, port := addrToHostPort(c, c.ExternalAddr(ctx, n)[0])
   125  		tc.toxClients[i] = toxiproxy.NewClient(fmt.Sprintf("http://%s:%d", externalAddr, toxPort))
   126  		proxy, err := tc.toxClients[i].CreateProxy("cockroach", fmt.Sprintf(":%d", tc.poisonedPort(port)), fmt.Sprintf("127.0.0.1:%d", port))
   127  		if err != nil {
   128  			return nil, errors.Wrap(err, "toxify")
   129  		}
   130  		tc.toxProxies[i] = proxy
   131  	}
   132  
   133  	return tc, nil
   134  }
   135  
   136  func (tc *ToxiCluster) poisonedPort(port int) int {
   137  	// NB: to make a change here, you also have to change
   138  	_ = cockroachToxiWrapper
   139  	return port + 10000
   140  }
   141  
   142  // Proxy returns the toxiproxy Proxy intercepting the given node's traffic.
   143  func (tc *ToxiCluster) Proxy(i int) *toxiproxy.Proxy {
   144  	proxy, found := tc.toxProxies[i]
   145  	if !found {
   146  		tc.cluster.t.Fatalf("proxy for node %d not found", i)
   147  	}
   148  	return proxy
   149  }
   150  
   151  // ExternalAddr gives the external host:port of the node(s), bypassing the
   152  // toxiproxy interception.
   153  func (tc *ToxiCluster) ExternalAddr(ctx context.Context, node nodeListOption) []string {
   154  	return tc.cluster.ExternalAddr(ctx, node)
   155  }
   156  
   157  // PoisonedExternalAddr gives the external host:port of the toxiproxy process
   158  // for the given nodes (i.e. the connection will be affected by toxics).
   159  func (tc *ToxiCluster) PoisonedExternalAddr(ctx context.Context, node nodeListOption) []string {
   160  	var out []string
   161  
   162  	extAddrs := tc.ExternalAddr(ctx, node)
   163  	for _, addr := range extAddrs {
   164  		host, port := addrToHostPort(tc.cluster, addr)
   165  		out = append(out, fmt.Sprintf("%s:%d", host, tc.poisonedPort(port)))
   166  	}
   167  	return out
   168  }
   169  
   170  // PoisonedPGAddr gives a connection to the given node that passes through toxiproxy.
   171  func (tc *ToxiCluster) PoisonedPGAddr(ctx context.Context, node nodeListOption) []string {
   172  	var out []string
   173  
   174  	urls := tc.ExternalPGUrl(ctx, node)
   175  	exts := tc.PoisonedExternalAddr(ctx, node)
   176  	for i, s := range urls {
   177  		u, err := url.Parse(s)
   178  		if err != nil {
   179  			tc.cluster.t.Fatal(err)
   180  		}
   181  		u.Host = exts[i]
   182  		out = append(out, u.String())
   183  	}
   184  	return out
   185  }
   186  
   187  // PoisonedConn returns an SQL connection to the specified node through toxiproxy.
   188  func (tc *ToxiCluster) PoisonedConn(ctx context.Context, node int) *gosql.DB {
   189  	url := tc.PoisonedPGAddr(ctx, tc.cluster.Node(node))[0]
   190  	db, err := gosql.Open("postgres", url)
   191  	if err != nil {
   192  		tc.cluster.t.Fatal(err)
   193  	}
   194  	return db
   195  }
   196  
   197  var _ = (*ToxiCluster)(nil).PoisonedConn
   198  var _ = (*ToxiCluster)(nil).PoisonedPGAddr
   199  var _ = (*ToxiCluster)(nil).PoisonedExternalAddr
   200  
   201  var measureRE = regexp.MustCompile(`real[^0-9]+([0-9.]+)`)
   202  
   203  // Measure runs a statement on the given node (bypassing toxiproxy for the
   204  // client connection) and measures the duration (including the invocation time
   205  // of `./cockroach sql`. This is simplistic and does not perform proper
   206  // escaping. It's not useful for anything but simple sanity checks.
   207  func (tc *ToxiCluster) Measure(ctx context.Context, fromNode int, stmt string) time.Duration {
   208  	_, port := addrToHostPort(tc.cluster, tc.ExternalAddr(ctx, tc.Node(fromNode))[0])
   209  	b, err := tc.cluster.RunWithBuffer(ctx, tc.cluster.l, tc.cluster.Node(fromNode), "time", "-p", "./cockroach", "sql", "--insecure", "--port", strconv.Itoa(port), "-e", "'"+stmt+"'")
   210  	tc.cluster.l.Printf("%s\n", b)
   211  	if err != nil {
   212  		tc.cluster.t.Fatal(err)
   213  	}
   214  	matches := measureRE.FindSubmatch(b)
   215  	if len(matches) != 2 {
   216  		tc.cluster.t.Fatalf("unable to extract duration from output: %s", b)
   217  	}
   218  	f, err := strconv.ParseFloat(string(matches[1]), 64)
   219  	if err != nil {
   220  		tc.cluster.t.Fatalf("unable to parse %s as float: %s", b, err)
   221  	}
   222  	return time.Duration(f * 1e9)
   223  }