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 }