github.com/sacloud/libsacloud/v2@v2.32.3/helper/setup/retryable_setup.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 setup
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/sacloud/libsacloud/v2/helper/defaults"
    24  
    25  	"github.com/sacloud/libsacloud/v2/sacloud"
    26  	"github.com/sacloud/libsacloud/v2/sacloud/accessor"
    27  	"github.com/sacloud/libsacloud/v2/sacloud/types"
    28  )
    29  
    30  // MaxRetryCountExceededError リトライ最大数超過エラー
    31  type MaxRetryCountExceededError error
    32  
    33  // CreateFunc リソース作成関数
    34  type CreateFunc func(ctx context.Context, zone string) (accessor.ID, error)
    35  
    36  // ProvisionBeforeUpFunc リソース作成後、起動前のプロビジョニング関数
    37  //
    38  // リソース作成後に起動が行われないリソース(VPCルータなど)向け。
    39  // 必要であればこの中でリソース起動処理を行う。
    40  type ProvisionBeforeUpFunc func(ctx context.Context, zone string, id types.ID, target interface{}) error
    41  
    42  // DeleteFunc リソース削除関数。
    43  //
    44  // リソース作成時のコピー待ちの間にリソースのAvailabilityがFailedになった場合に利用される。
    45  type DeleteFunc func(ctx context.Context, zone string, id types.ID) error
    46  
    47  // ReadFunc リソース起動待ちなどで利用するリソースのRead用Func
    48  type ReadFunc func(ctx context.Context, zone string, id types.ID) (interface{}, error)
    49  
    50  // RetryableSetup リソース作成時にコピー待ちや起動待ちが必要なリソースのビルダー。
    51  //
    52  // リソースのビルドの際、必要に応じてリトライ(リソースの削除&再作成)を行う。
    53  type RetryableSetup struct {
    54  	// Create リソース作成用関数
    55  	Create CreateFunc
    56  	// IsWaitForCopy コピー待ちを行うか
    57  	IsWaitForCopy bool
    58  	// IsWaitForUp 起動待ちを行うか
    59  	IsWaitForUp bool
    60  	// ProvisionBeforeUp リソース起動前のプロビジョニング関数
    61  	ProvisionBeforeUp ProvisionBeforeUpFunc
    62  	// Delete リソース削除用関数
    63  	Delete DeleteFunc
    64  	// WaitForUp リソース起動待ち関数
    65  	Read ReadFunc
    66  	// RetryCount リトライ回数
    67  	RetryCount int
    68  	// ProvisioningRetryCount プロビジョニングリトライ回数
    69  	ProvisioningRetryCount int
    70  	// ProvisioningRetryInterval プロビジョニングリトライ間隔
    71  	ProvisioningRetryInterval time.Duration
    72  	// DeleteRetryCount 削除リトライ回数
    73  	DeleteRetryCount int
    74  	// DeleteRetryInterval 削除リトライ間隔
    75  	DeleteRetryInterval time.Duration
    76  	// sacloud.StateWaiterによるステート待ちの間隔
    77  	PollingInterval time.Duration
    78  }
    79  
    80  // Setup リソースのビルドを行う。必要に応じてリトライ(リソースの削除&再作成)を行う。
    81  func (r *RetryableSetup) Setup(ctx context.Context, zone string) (interface{}, error) {
    82  	if (r.IsWaitForCopy || r.IsWaitForUp) && r.Read == nil {
    83  		return nil, errors.New("failed: Read is required when IsWaitForCopy or IsWaitForUp is true")
    84  	}
    85  
    86  	r.init()
    87  
    88  	var created interface{}
    89  	for r.RetryCount+1 > 0 {
    90  		r.RetryCount--
    91  
    92  		// リソース作成
    93  		target, err := r.createResource(ctx, zone)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  		id := target.GetID()
    98  
    99  		// コピー待ち
   100  		if r.IsWaitForCopy {
   101  			// コピー待ち、Failedになった場合はリソース削除
   102  			state, err := r.waitForCopyWithCleanup(ctx, zone, id)
   103  			if err != nil {
   104  				return state, err
   105  			}
   106  			if state != nil {
   107  				created = state
   108  			}
   109  		} else {
   110  			created = target
   111  		}
   112  
   113  		// 起動前の設定など
   114  		if err := r.provisionBeforeUp(ctx, zone, id, created); err != nil {
   115  			return created, err
   116  		}
   117  
   118  		// 起動待ち
   119  		if err := r.waitForUp(ctx, zone, id, created); err != nil {
   120  			return created, err
   121  		}
   122  
   123  		if created != nil {
   124  			break
   125  		}
   126  	}
   127  
   128  	if created == nil {
   129  		return nil, MaxRetryCountExceededError(fmt.Errorf("max retry count exceeded"))
   130  	}
   131  	return created, nil
   132  }
   133  
   134  func (r *RetryableSetup) init() {
   135  	if r.RetryCount <= 0 {
   136  		r.RetryCount = defaults.DefaultMaxRetryCount
   137  	}
   138  	if r.DeleteRetryCount <= 0 {
   139  		r.DeleteRetryCount = defaults.DefaultDeleteRetryCount
   140  	}
   141  	if r.DeleteRetryInterval <= 0 {
   142  		r.DeleteRetryInterval = defaults.DefaultDeleteWaitInterval
   143  	}
   144  	if r.ProvisioningRetryCount <= 0 {
   145  		r.ProvisioningRetryCount = defaults.DefaultProvisioningRetryCount
   146  	}
   147  	if r.ProvisioningRetryInterval <= 0 {
   148  		r.ProvisioningRetryInterval = defaults.DefaultProvisioningWaitInterval
   149  	}
   150  	if r.PollingInterval <= 0 {
   151  		r.PollingInterval = defaults.DefaultPollingInterval
   152  	}
   153  }
   154  
   155  func (r *RetryableSetup) createResource(ctx context.Context, zone string) (accessor.ID, error) {
   156  	if r.Create == nil {
   157  		return nil, fmt.Errorf("create func is required")
   158  	}
   159  	return r.Create(ctx, zone)
   160  }
   161  
   162  func (r *RetryableSetup) waitForCopyWithCleanup(ctx context.Context, zone string, id types.ID) (interface{}, error) {
   163  	waiter := &sacloud.StatePollingWaiter{
   164  		ReadFunc: func() (interface{}, error) {
   165  			return r.Read(ctx, zone, id)
   166  		},
   167  		TargetAvailability: []types.EAvailability{
   168  			types.Availabilities.Available,
   169  			types.Availabilities.Failed,
   170  		},
   171  		PendingAvailability: []types.EAvailability{
   172  			types.Availabilities.Unknown,
   173  			types.Availabilities.Migrating,
   174  			types.Availabilities.Uploading,
   175  			types.Availabilities.Transferring,
   176  			types.Availabilities.Discontinued,
   177  		},
   178  		PollingInterval: r.PollingInterval,
   179  	}
   180  
   181  	//wait
   182  	compChan, progChan, errChan := waiter.AsyncWaitForState(ctx)
   183  	var state interface{}
   184  	var err error
   185  
   186  loop:
   187  	for {
   188  		select {
   189  		case v := <-compChan:
   190  			state = v
   191  			break loop
   192  		case v := <-progChan:
   193  			state = v
   194  		case e := <-errChan:
   195  			err = e
   196  			break loop
   197  		}
   198  	}
   199  
   200  	if state != nil {
   201  		// Availabilityを持ち、Failedになっていた場合はリソースを削除してリトライ
   202  		if f, ok := state.(accessor.Availability); ok && f != nil {
   203  			if f.GetAvailability().IsFailed() {
   204  				// FailedになったばかりだとDelete APIが失敗する(コピー進行中など)場合があるため、
   205  				// 任意の回数リトライ&待機を行う
   206  				for i := 0; i < r.DeleteRetryCount; i++ {
   207  					time.Sleep(r.DeleteRetryInterval)
   208  					if err = r.Delete(ctx, zone, id); err == nil {
   209  						break
   210  					}
   211  				}
   212  
   213  				return nil, nil
   214  			}
   215  		}
   216  
   217  		return state, nil
   218  	}
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	return nil, nil
   224  }
   225  
   226  func (r *RetryableSetup) provisionBeforeUp(ctx context.Context, zone string, id types.ID, created interface{}) error {
   227  	if r.ProvisionBeforeUp != nil && created != nil {
   228  		var err error
   229  		for i := 0; i < r.ProvisioningRetryCount; i++ {
   230  			if err = r.ProvisionBeforeUp(ctx, zone, id, created); err == nil {
   231  				break
   232  			}
   233  			time.Sleep(r.ProvisioningRetryInterval)
   234  		}
   235  		return err
   236  	}
   237  	return nil
   238  }
   239  
   240  func (r *RetryableSetup) waitForUp(ctx context.Context, zone string, id types.ID, created interface{}) error {
   241  	if r.IsWaitForUp && created != nil {
   242  		waiter := &sacloud.StatePollingWaiter{
   243  			ReadFunc: func() (interface{}, error) {
   244  				return r.Read(ctx, zone, id)
   245  			},
   246  			TargetAvailability: []types.EAvailability{
   247  				types.Availabilities.Available,
   248  			},
   249  			PendingAvailability: []types.EAvailability{
   250  				types.Availabilities.Unknown,
   251  				types.Availabilities.Migrating,
   252  				types.Availabilities.Uploading,
   253  				types.Availabilities.Transferring,
   254  				types.Availabilities.Discontinued,
   255  			},
   256  			TargetInstanceStatus: []types.EServerInstanceStatus{
   257  				types.ServerInstanceStatuses.Up,
   258  			},
   259  			PendingInstanceStatus: []types.EServerInstanceStatus{
   260  				types.ServerInstanceStatuses.Unknown,
   261  				types.ServerInstanceStatuses.Cleaning,
   262  				types.ServerInstanceStatuses.Down,
   263  			},
   264  			PollingInterval: r.PollingInterval,
   265  		}
   266  		_, err := waiter.WaitForState(ctx)
   267  		return err
   268  	}
   269  	return nil
   270  }