github.com/sacloud/libsacloud/v2@v2.32.3/helper/power/power_test.go (about)

     1  // Copyright 2016-2022 The Libsacloud 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/libsacloud/v2/sacloud"
    25  	"github.com/sacloud/libsacloud/v2/sacloud/types"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  func TestPowerHandler(t *testing.T) {
    30  	t.Parallel()
    31  
    32  	defaultInterval := sacloud.DefaultStatePollingInterval
    33  
    34  	sacloud.DefaultStatePollingInterval = 10 * time.Millisecond
    35  	BootRetrySpan = time.Millisecond
    36  	ShutdownRetrySpan = time.Millisecond
    37  	defer func() {
    38  		sacloud.DefaultStatePollingInterval = defaultInterval
    39  		BootRetrySpan = 0
    40  		ShutdownRetrySpan = 0
    41  	}()
    42  
    43  	ctx := context.Background()
    44  	t.Run("boot", func(t *testing.T) {
    45  		handler := &dummyPowerHandler{
    46  			ignoreBootCount: 3,
    47  			instanceStatus:  types.ServerInstanceStatuses.Down,
    48  		}
    49  		err := boot(ctx, handler)
    50  		require.NoError(t, err)
    51  		require.Equal(t, handler.ignoreBootCount+1, handler.bootCount)
    52  	})
    53  	t.Run("shutdown", func(t *testing.T) {
    54  		handler := &dummyPowerHandler{
    55  			ignoreShutdownCount: 3,
    56  			instanceStatus:      types.ServerInstanceStatuses.Up,
    57  		}
    58  		err := shutdown(ctx, handler, true)
    59  		require.NoError(t, err)
    60  		require.Equal(t, handler.ignoreShutdownCount+1, handler.shutdownCount)
    61  	})
    62  }
    63  
    64  type dummyPowerHandler struct {
    65  	bootCount           int
    66  	shutdownCount       int
    67  	ignoreBootCount     int
    68  	ignoreShutdownCount int
    69  	instanceStatus      types.EServerInstanceStatus
    70  
    71  	mu sync.Mutex
    72  }
    73  
    74  func (d *dummyPowerHandler) boot() error {
    75  	d.bootCount++
    76  	if d.bootCount > d.ignoreBootCount {
    77  		go d.toggleInstanceStatus()
    78  		return sacloud.NewAPIError("DUMMY", nil, http.StatusConflict, nil)
    79  	}
    80  	return nil
    81  }
    82  func (d *dummyPowerHandler) shutdown(force bool) error {
    83  	d.shutdownCount++
    84  	if d.shutdownCount > d.ignoreShutdownCount {
    85  		go d.toggleInstanceStatus()
    86  		return sacloud.NewAPIError("DUMMY", nil, http.StatusConflict, nil)
    87  	}
    88  	return nil
    89  }
    90  
    91  func (d *dummyPowerHandler) read() (interface{}, error) {
    92  	return d, nil
    93  }
    94  
    95  func (d *dummyPowerHandler) toggleInstanceStatus() {
    96  	time.Sleep(100 * time.Millisecond)
    97  
    98  	d.mu.Lock()
    99  	defer d.mu.Unlock()
   100  
   101  	switch d.instanceStatus {
   102  	case types.ServerInstanceStatuses.Up:
   103  		d.instanceStatus = types.ServerInstanceStatuses.Down
   104  	case types.ServerInstanceStatuses.Down:
   105  		d.instanceStatus = types.ServerInstanceStatuses.Up
   106  	}
   107  }
   108  
   109  // GetInstanceStatus .
   110  func (d *dummyPowerHandler) GetInstanceStatus() types.EServerInstanceStatus {
   111  	d.mu.Lock()
   112  	defer d.mu.Unlock()
   113  
   114  	return d.instanceStatus
   115  }
   116  
   117  // SetInstanceStatus .
   118  func (d *dummyPowerHandler) SetInstanceStatus(v types.EServerInstanceStatus) {
   119  	d.instanceStatus = v
   120  }
   121  
   122  func TestPower_powerRequestWithRetry(t *testing.T) {
   123  	InitialRequestRetrySpan = 1 * time.Millisecond
   124  	InitialRequestTimeout = 100 * time.Millisecond
   125  
   126  	// 最初のシャットダウンが受け入れられる(エラーにならない)まで409-still_creating時にリトライする
   127  	// エラーなしの場合は即時return nilする
   128  	t.Run("retry when received 409 and still_creating response", func(t *testing.T) {
   129  		retried := 0
   130  		maxRetry := 3
   131  		err := powerRequestWithRetry(context.Background(), func() error {
   132  			if retried < maxRetry {
   133  				retried++
   134  				return sacloud.NewAPIError("GET", nil, http.StatusConflict, &sacloud.APIErrorResponse{
   135  					IsFatal:      true,
   136  					Serial:       "xxx",
   137  					Status:       "409 Conflict",
   138  					ErrorCode:    "still_creating",
   139  					ErrorMessage: "xxx",
   140  				})
   141  			}
   142  			return nil
   143  		})
   144  
   145  		if err != nil {
   146  			t.Fatalf("got unexpected error: %s", err)
   147  		}
   148  		if retried != maxRetry {
   149  			t.Fatalf("powerRequest was not retried: expected: %d, actual: %d", maxRetry, retried)
   150  		}
   151  	})
   152  	// 409時のリトライにはタイムアウトを設定する
   153  	t.Run("retry when received 409 and still_creating should be timed out", func(t *testing.T) {
   154  		err := powerRequestWithRetry(context.Background(), func() error {
   155  			return sacloud.NewAPIError("GET", nil, http.StatusConflict, &sacloud.APIErrorResponse{
   156  				IsFatal:      true,
   157  				Serial:       "xxx",
   158  				Status:       "409 Conflict",
   159  				ErrorCode:    "still_creating",
   160  				ErrorMessage: "xxx",
   161  			})
   162  		})
   163  
   164  		require.EqualError(t, err, "powerRequestWithRetry: timed out: context deadline exceeded")
   165  	})
   166  	// その他のエラーは即時returnする
   167  	t.Run("force return error when received unexpected error", func(t *testing.T) {
   168  		expected := sacloud.NewAPIError("GET", nil, http.StatusNotFound, &sacloud.APIErrorResponse{
   169  			IsFatal:      true,
   170  			Serial:       "xxx",
   171  			Status:       "404 NotFound",
   172  			ErrorCode:    "not_found",
   173  			ErrorMessage: "xxx",
   174  		})
   175  		err := powerRequestWithRetry(context.Background(), func() error { return expected })
   176  
   177  		require.EqualValues(t, expected, err)
   178  	})
   179  }