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 }