github.com/sacloud/iaas-api-go@v1.12.0/helper/power/power_test.go (about)

     1  // Copyright 2022-2023 The sacloud/iaas-api-go Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package power
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"sync"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/sacloud/iaas-api-go"
    25  	"github.com/sacloud/iaas-api-go/defaults"
    26  	"github.com/sacloud/iaas-api-go/types"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  func TestPowerHandler(t *testing.T) {
    31  	t.Parallel()
    32  
    33  	defaultInterval := defaults.DefaultStatePollingInterval
    34  
    35  	defaults.DefaultStatePollingInterval = 10 * time.Millisecond
    36  	BootRetrySpan = time.Millisecond
    37  	ShutdownRetrySpan = time.Millisecond
    38  	defer func() {
    39  		defaults.DefaultStatePollingInterval = defaultInterval
    40  		BootRetrySpan = 0
    41  		ShutdownRetrySpan = 0
    42  	}()
    43  
    44  	ctx := context.Background()
    45  	t.Run("boot", func(t *testing.T) {
    46  		handler := &dummyPowerHandler{
    47  			ignoreBootCount: 3,
    48  			instanceStatus:  types.ServerInstanceStatuses.Down,
    49  		}
    50  		err := boot(ctx, handler)
    51  		require.NoError(t, err)
    52  		require.Equal(t, handler.ignoreBootCount+1, handler.bootCount)
    53  	})
    54  	t.Run("shutdown", func(t *testing.T) {
    55  		handler := &dummyPowerHandler{
    56  			ignoreShutdownCount: 3,
    57  			instanceStatus:      types.ServerInstanceStatuses.Up,
    58  		}
    59  		err := shutdown(ctx, handler, true)
    60  		require.NoError(t, err)
    61  		require.Equal(t, handler.ignoreShutdownCount+1, handler.shutdownCount)
    62  	})
    63  }
    64  
    65  type dummyPowerHandler struct {
    66  	bootCount           int
    67  	shutdownCount       int
    68  	ignoreBootCount     int
    69  	ignoreShutdownCount int
    70  	instanceStatus      types.EServerInstanceStatus
    71  
    72  	mu sync.Mutex
    73  }
    74  
    75  func (d *dummyPowerHandler) boot() error {
    76  	d.bootCount++
    77  	if d.bootCount > d.ignoreBootCount {
    78  		go d.toggleInstanceStatus()
    79  		return iaas.NewAPIError("DUMMY", nil, http.StatusConflict, nil)
    80  	}
    81  	return nil
    82  }
    83  func (d *dummyPowerHandler) shutdown(force bool) error {
    84  	d.shutdownCount++
    85  	if d.shutdownCount > d.ignoreShutdownCount {
    86  		go d.toggleInstanceStatus()
    87  		return iaas.NewAPIError("DUMMY", nil, http.StatusConflict, nil)
    88  	}
    89  	return nil
    90  }
    91  
    92  func (d *dummyPowerHandler) read() (interface{}, error) {
    93  	return d, nil
    94  }
    95  
    96  func (d *dummyPowerHandler) toggleInstanceStatus() {
    97  	time.Sleep(100 * time.Millisecond)
    98  
    99  	d.mu.Lock()
   100  	defer d.mu.Unlock()
   101  
   102  	switch d.instanceStatus {
   103  	case types.ServerInstanceStatuses.Up:
   104  		d.instanceStatus = types.ServerInstanceStatuses.Down
   105  	case types.ServerInstanceStatuses.Down:
   106  		d.instanceStatus = types.ServerInstanceStatuses.Up
   107  	}
   108  }
   109  
   110  // GetInstanceStatus .
   111  func (d *dummyPowerHandler) GetInstanceStatus() types.EServerInstanceStatus {
   112  	d.mu.Lock()
   113  	defer d.mu.Unlock()
   114  
   115  	return d.instanceStatus
   116  }
   117  
   118  // SetInstanceStatus .
   119  func (d *dummyPowerHandler) SetInstanceStatus(v types.EServerInstanceStatus) {
   120  	d.instanceStatus = v
   121  }
   122  
   123  func TestPower_powerRequestWithRetry(t *testing.T) {
   124  	InitialRequestRetrySpan = 1 * time.Millisecond
   125  	InitialRequestTimeout = 100 * time.Millisecond
   126  
   127  	// 最初のシャットダウンが受け入れられる(エラーにならない)まで409-still_creating時にリトライする
   128  	// エラーなしの場合は即時return nilする
   129  	t.Run("retry when received 409 and still_creating response", func(t *testing.T) {
   130  		retried := 0
   131  		maxRetry := 3
   132  		err := powerRequestWithRetry(context.Background(), func() error {
   133  			if retried < maxRetry {
   134  				retried++
   135  				return iaas.NewAPIError("GET", nil, http.StatusConflict, &iaas.APIErrorResponse{
   136  					IsFatal:      true,
   137  					Serial:       "xxx",
   138  					Status:       "409 Conflict",
   139  					ErrorCode:    "still_creating",
   140  					ErrorMessage: "xxx",
   141  				})
   142  			}
   143  			return nil
   144  		})
   145  
   146  		if err != nil {
   147  			t.Fatalf("got unexpected error: %s", err)
   148  		}
   149  		if retried != maxRetry {
   150  			t.Fatalf("powerRequest was not retried: expected: %d, actual: %d", maxRetry, retried)
   151  		}
   152  	})
   153  	// 409時のリトライにはタイムアウトを設定する
   154  	t.Run("retry when received 409 and still_creating should be timed out", func(t *testing.T) {
   155  		err := powerRequestWithRetry(context.Background(), func() error {
   156  			return iaas.NewAPIError("GET", nil, http.StatusConflict, &iaas.APIErrorResponse{
   157  				IsFatal:      true,
   158  				Serial:       "xxx",
   159  				Status:       "409 Conflict",
   160  				ErrorCode:    "still_creating",
   161  				ErrorMessage: "xxx",
   162  			})
   163  		})
   164  
   165  		require.EqualError(t, err, "powerRequestWithRetry: timed out: context deadline exceeded")
   166  	})
   167  	// その他のエラーは即時returnする
   168  	t.Run("force return error when received unexpected error", func(t *testing.T) {
   169  		expected := iaas.NewAPIError("GET", nil, http.StatusNotFound, &iaas.APIErrorResponse{
   170  			IsFatal:      true,
   171  			Serial:       "xxx",
   172  			Status:       "404 NotFound",
   173  			ErrorCode:    "not_found",
   174  			ErrorMessage: "xxx",
   175  		})
   176  		err := powerRequestWithRetry(context.Background(), func() error { return expected })
   177  
   178  		require.EqualValues(t, expected, err)
   179  	})
   180  }