github.com/opensearch-project/opensearch-go/v2@v2.3.0/opensearchtransport/connection_internal_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  //
     3  // The OpenSearch Contributors require contributions made to
     4  // this file be licensed under the Apache-2.0 license or a
     5  // compatible open source license.
     6  //
     7  // Modifications Copyright OpenSearch Contributors. See
     8  // GitHub history for details.
     9  
    10  // Licensed to Elasticsearch B.V. under one or more contributor
    11  // license agreements. See the NOTICE file distributed with
    12  // this work for additional information regarding copyright
    13  // ownership. Elasticsearch B.V. licenses this file to you under
    14  // the Apache License, Version 2.0 (the "License"); you may
    15  // not use this file except in compliance with the License.
    16  // You may obtain a copy of the License at
    17  //
    18  //    http://www.apache.org/licenses/LICENSE-2.0
    19  //
    20  // Unless required by applicable law or agreed to in writing,
    21  // software distributed under the License is distributed on an
    22  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    23  // KIND, either express or implied.  See the License for the
    24  // specific language governing permissions and limitations
    25  // under the License.
    26  
    27  // +build !integration
    28  
    29  package opensearchtransport
    30  
    31  import (
    32  	"net/url"
    33  	"regexp"
    34  	"testing"
    35  	"time"
    36  )
    37  
    38  func TestSingleConnectionPoolNext(t *testing.T) {
    39  	t.Run("Single URL", func(t *testing.T) {
    40  		pool := &singleConnectionPool{
    41  			connection: &Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
    42  		}
    43  
    44  		for i := 0; i < 7; i++ {
    45  			c, err := pool.Next()
    46  			if err != nil {
    47  				t.Errorf("Unexpected error: %s", err)
    48  			}
    49  
    50  			if c.URL.String() != "http://foo1" {
    51  				t.Errorf("Unexpected URL, want=http://foo1, got=%s", c.URL)
    52  			}
    53  		}
    54  	})
    55  }
    56  
    57  func TestSingleConnectionPoolOnFailure(t *testing.T) {
    58  	t.Run("Noop", func(t *testing.T) {
    59  		pool := &singleConnectionPool{
    60  			connection: &Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
    61  		}
    62  
    63  		if err := pool.OnFailure(&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}}); err != nil {
    64  			t.Errorf("Unexpected error: %s", err)
    65  		}
    66  	})
    67  }
    68  
    69  func TestStatusConnectionPoolNext(t *testing.T) {
    70  	t.Run("No URL", func(t *testing.T) {
    71  		pool := &statusConnectionPool{}
    72  
    73  		c, err := pool.Next()
    74  		if err == nil {
    75  			t.Errorf("Expected error, but got: %s", c.URL)
    76  		}
    77  	})
    78  
    79  	t.Run("Two URLs", func(t *testing.T) {
    80  		var c *Connection
    81  
    82  		pool := &statusConnectionPool{
    83  			live: []*Connection{
    84  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
    85  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo2"}},
    86  			},
    87  			selector: &roundRobinSelector{curr: -1},
    88  		}
    89  
    90  		c, _ = pool.Next()
    91  
    92  		if c.URL.String() != "http://foo1" {
    93  			t.Errorf("Unexpected URL, want=foo1, got=%s", c.URL)
    94  		}
    95  
    96  		c, _ = pool.Next()
    97  		if c.URL.String() != "http://foo2" {
    98  			t.Errorf("Unexpected URL, want=http://foo2, got=%s", c.URL)
    99  		}
   100  
   101  		c, _ = pool.Next()
   102  		if c.URL.String() != "http://foo1" {
   103  			t.Errorf("Unexpected URL, want=http://foo1, got=%s", c.URL)
   104  		}
   105  	})
   106  
   107  	t.Run("Three URLs", func(t *testing.T) {
   108  		pool := &statusConnectionPool{
   109  			live: []*Connection{
   110  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
   111  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo2"}},
   112  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo3"}},
   113  			},
   114  			selector: &roundRobinSelector{curr: -1},
   115  		}
   116  
   117  		var expected string
   118  		for i := 0; i < 11; i++ {
   119  			c, err := pool.Next()
   120  
   121  			if err != nil {
   122  				t.Errorf("Unexpected error: %s", err)
   123  			}
   124  
   125  			switch i % len(pool.live) {
   126  			case 0:
   127  				expected = "http://foo1"
   128  			case 1:
   129  				expected = "http://foo2"
   130  			case 2:
   131  				expected = "http://foo3"
   132  			default:
   133  				t.Fatalf("Unexpected i %% 3: %d", i%3)
   134  			}
   135  
   136  			if c.URL.String() != expected {
   137  				t.Errorf("Unexpected URL, want=%s, got=%s", expected, c.URL)
   138  			}
   139  		}
   140  	})
   141  
   142  	t.Run("Resurrect dead connection when no live is available", func(t *testing.T) {
   143  		pool := &statusConnectionPool{
   144  			live: []*Connection{},
   145  			dead: []*Connection{
   146  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}, Failures: 3},
   147  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo2"}, Failures: 1},
   148  			},
   149  			selector: &roundRobinSelector{curr: -1},
   150  		}
   151  
   152  		c, err := pool.Next()
   153  		if err != nil {
   154  			t.Errorf("Unexpected error: %s", err)
   155  		}
   156  
   157  		if c == nil {
   158  			t.Errorf("Expected connection, got nil: %s", c)
   159  		}
   160  
   161  		if c.URL.String() != "http://foo2" {
   162  			t.Errorf("Expected <http://foo2>, got: %s", c.URL.String())
   163  		}
   164  
   165  		if c.IsDead {
   166  			t.Errorf("Expected connection to be live, got: %s", c)
   167  		}
   168  
   169  		if len(pool.live) != 1 {
   170  			t.Errorf("Expected 1 connection in live list, got: %s", pool.live)
   171  		}
   172  
   173  		if len(pool.dead) != 1 {
   174  			t.Errorf("Expected 1 connection in dead list, got: %s", pool.dead)
   175  		}
   176  	})
   177  }
   178  
   179  func TestStatusConnectionPoolOnSuccess(t *testing.T) {
   180  	t.Run("Move connection to live list and mark it as healthy", func(t *testing.T) {
   181  		pool := &statusConnectionPool{
   182  			dead: []*Connection{
   183  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}, Failures: 3, IsDead: true},
   184  			},
   185  			selector: &roundRobinSelector{curr: -1},
   186  		}
   187  
   188  		conn := pool.dead[0]
   189  
   190  		if err := pool.OnSuccess(conn); err != nil {
   191  			t.Fatalf("Unexpected error: %s", err)
   192  		}
   193  
   194  		if conn.IsDead {
   195  			t.Errorf("Expected the connection to be live; %s", conn)
   196  		}
   197  
   198  		if !conn.DeadSince.IsZero() {
   199  			t.Errorf("Unexpected value for DeadSince: %s", conn.DeadSince)
   200  		}
   201  
   202  		if len(pool.live) != 1 {
   203  			t.Errorf("Expected 1 live connection, got: %d", len(pool.live))
   204  		}
   205  
   206  		if len(pool.dead) != 0 {
   207  			t.Errorf("Expected 0 dead connections, got: %d", len(pool.dead))
   208  		}
   209  	})
   210  }
   211  
   212  func TestStatusConnectionPoolOnFailure(t *testing.T) {
   213  	t.Run("Remove connection, mark it, and sort dead connections", func(t *testing.T) {
   214  		pool := &statusConnectionPool{
   215  			live: []*Connection{
   216  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
   217  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo2"}},
   218  			},
   219  			dead: []*Connection{
   220  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo3"}, Failures: 0},
   221  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo4"}, Failures: 99},
   222  			},
   223  			selector: &roundRobinSelector{curr: -1},
   224  		}
   225  
   226  		conn := pool.live[0]
   227  
   228  		if err := pool.OnFailure(conn); err != nil {
   229  			t.Fatalf("Unexpected error: %s", err)
   230  		}
   231  
   232  		if !conn.IsDead {
   233  			t.Errorf("Expected the connection to be dead; %s", conn)
   234  		}
   235  
   236  		if conn.DeadSince.IsZero() {
   237  			t.Errorf("Unexpected value for DeadSince: %s", conn.DeadSince)
   238  		}
   239  
   240  		if len(pool.live) != 1 {
   241  			t.Errorf("Expected 1 live connection, got: %d", len(pool.live))
   242  		}
   243  
   244  		if len(pool.dead) != 3 {
   245  			t.Errorf("Expected 3 dead connections, got: %d", len(pool.dead))
   246  		}
   247  
   248  		expected := []string{
   249  			"http://foo4",
   250  			"http://foo1",
   251  			"http://foo3",
   252  		}
   253  
   254  		for i, u := range expected {
   255  			if pool.dead[i].URL.String() != u {
   256  				t.Errorf("Unexpected value for item %d in pool.dead: %s", i, pool.dead[i].URL.String())
   257  			}
   258  		}
   259  	})
   260  
   261  	t.Run("Short circuit when the connection is already dead", func(t *testing.T) {
   262  		pool := &statusConnectionPool{
   263  			live: []*Connection{
   264  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}},
   265  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo2"}},
   266  				&Connection{URL: &url.URL{Scheme: "http", Host: "foo3"}},
   267  			},
   268  			selector: &roundRobinSelector{curr: -1},
   269  		}
   270  
   271  		conn := pool.live[0]
   272  		conn.IsDead = true
   273  
   274  		if err := pool.OnFailure(conn); err != nil {
   275  			t.Fatalf("Unexpected error: %s", err)
   276  		}
   277  
   278  		if len(pool.dead) != 0 {
   279  			t.Errorf("Expected the dead list to be empty, got: %s", pool.dead)
   280  		}
   281  	})
   282  }
   283  
   284  func TestStatusConnectionPoolResurrect(t *testing.T) {
   285  	t.Run("Mark the connection as dead and add/remove it to the lists", func(t *testing.T) {
   286  		pool := &statusConnectionPool{
   287  			live:     []*Connection{},
   288  			dead:     []*Connection{&Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}, IsDead: true}},
   289  			selector: &roundRobinSelector{curr: -1},
   290  		}
   291  
   292  		conn := pool.dead[0]
   293  
   294  		if err := pool.resurrect(conn, true); err != nil {
   295  			t.Fatalf("Unexpected error: %s", err)
   296  		}
   297  
   298  		if conn.IsDead {
   299  			t.Errorf("Expected connection to be dead, got: %s", conn)
   300  		}
   301  
   302  		if len(pool.dead) != 0 {
   303  			t.Errorf("Expected no dead connections, got: %s", pool.dead)
   304  		}
   305  
   306  		if len(pool.live) != 1 {
   307  			t.Errorf("Expected 1 live connection, got: %s", pool.live)
   308  		}
   309  	})
   310  
   311  	t.Run("Short circuit removal when the connection is not in the dead list", func(t *testing.T) {
   312  		pool := &statusConnectionPool{
   313  			dead:     []*Connection{&Connection{URL: &url.URL{Scheme: "http", Host: "bar"}, IsDead: true}},
   314  			selector: &roundRobinSelector{curr: -1},
   315  		}
   316  
   317  		conn := &Connection{URL: &url.URL{Scheme: "http", Host: "foo1"}, IsDead: true}
   318  
   319  		if err := pool.resurrect(conn, true); err != nil {
   320  			t.Fatalf("Unexpected error: %s", err)
   321  		}
   322  
   323  		if len(pool.live) != 1 {
   324  			t.Errorf("Expected 1 live connection, got: %s", pool.live)
   325  		}
   326  
   327  		if len(pool.dead) != 1 {
   328  			t.Errorf("Expected 1 dead connection, got: %s", pool.dead)
   329  		}
   330  	})
   331  
   332  	t.Run("Schedule resurrect", func(t *testing.T) {
   333  		defaultResurrectTimeoutInitial = 0
   334  		defer func() { defaultResurrectTimeoutInitial = 60 * time.Second }()
   335  
   336  		pool := &statusConnectionPool{
   337  			live: []*Connection{},
   338  			dead: []*Connection{
   339  				&Connection{
   340  					URL:       &url.URL{Scheme: "http", Host: "foo1"},
   341  					Failures:  100,
   342  					IsDead:    true,
   343  					DeadSince: time.Now().UTC(),
   344  				},
   345  			},
   346  			selector: &roundRobinSelector{curr: -1},
   347  		}
   348  
   349  		conn := pool.dead[0]
   350  		pool.scheduleResurrect(conn)
   351  		time.Sleep(50 * time.Millisecond)
   352  
   353  		pool.Lock()
   354  		defer pool.Unlock()
   355  
   356  		if len(pool.live) != 1 {
   357  			t.Errorf("Expected 1 live connection, got: %s", pool.live)
   358  		}
   359  		if len(pool.dead) != 0 {
   360  			t.Errorf("Expected no dead connections, got: %s", pool.dead)
   361  		}
   362  	})
   363  }
   364  
   365  func TestConnection(t *testing.T) {
   366  	t.Run("String", func(t *testing.T) {
   367  		conn := &Connection{
   368  			URL:       &url.URL{Scheme: "http", Host: "foo1"},
   369  			Failures:  10,
   370  			IsDead:    true,
   371  			DeadSince: time.Now().UTC(),
   372  		}
   373  
   374  		match, err := regexp.MatchString(
   375  			`<http://foo1> dead=true failures=10`,
   376  			conn.String(),
   377  		)
   378  
   379  		if err != nil {
   380  			t.Fatalf("Unexpected error: %s", err)
   381  		}
   382  
   383  		if !match {
   384  			t.Errorf("Unexpected output: %s", conn)
   385  		}
   386  	})
   387  }