github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/modules/deploy/deploy.go (about)

     1  package deploy
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	confid "github.com/machinefi/w3bstream/pkg/depends/conf/id"
    12  	"github.com/machinefi/w3bstream/pkg/depends/conf/logger"
    13  	"github.com/machinefi/w3bstream/pkg/depends/kit/logr"
    14  	"github.com/machinefi/w3bstream/pkg/depends/kit/sqlx"
    15  	"github.com/machinefi/w3bstream/pkg/depends/kit/sqlx/builder"
    16  	"github.com/machinefi/w3bstream/pkg/depends/x/contextx"
    17  	"github.com/machinefi/w3bstream/pkg/enums"
    18  	"github.com/machinefi/w3bstream/pkg/errors/status"
    19  	"github.com/machinefi/w3bstream/pkg/models"
    20  	"github.com/machinefi/w3bstream/pkg/modules/config"
    21  	"github.com/machinefi/w3bstream/pkg/modules/resource"
    22  	"github.com/machinefi/w3bstream/pkg/modules/robot_notifier"
    23  	"github.com/machinefi/w3bstream/pkg/modules/robot_notifier/lark"
    24  	"github.com/machinefi/w3bstream/pkg/modules/vm"
    25  	"github.com/machinefi/w3bstream/pkg/modules/wasmlog"
    26  	"github.com/machinefi/w3bstream/pkg/types"
    27  )
    28  
    29  func Init(ctx context.Context) error {
    30  	ctx, l := logger.NewSpanContext(ctx, "deploy.Init")
    31  	defer l.End()
    32  
    33  	var (
    34  		d = types.MustMgrDBExecutorFromContext(ctx)
    35  
    36  		ins = &models.Instance{}
    37  		app *models.Applet
    38  		res *models.Resource
    39  
    40  		code []byte
    41  
    42  		fails = []string{"\ninstances failed to deploy:"}
    43  		succs = []string{"\ndeployed instances:"}
    44  	)
    45  
    46  	defer func() {
    47  		message := ""
    48  		if len(fails) > 1 {
    49  			message += strings.Join(fails, "\n")
    50  		}
    51  		if len(succs) > 1 {
    52  			message += strings.Join(succs, "\n")
    53  		}
    54  		body, err := lark.Build(ctx, "Instances Deploying", "INFO", message)
    55  		if err != nil {
    56  			return
    57  		}
    58  		_ = robot_notifier.Push(ctx, body)
    59  	}()
    60  
    61  	// make wasm database lazy init to reduce database connections
    62  	wasmdbconf := *(types.MustWasmDBConfigFromContext(ctx))
    63  	wasmdbconf.LazyInit = true
    64  	ctx = types.WithWasmDBConfig(ctx, &wasmdbconf)
    65  
    66  	list, err := ins.List(d, nil)
    67  	if err != nil {
    68  		l.Error(err)
    69  		return err
    70  	}
    71  	l = l.WithValues("total", len(list))
    72  
    73  	for i := range list {
    74  		ins = &list[i]
    75  		l := l.WithValues("ins", ins.InstanceID, "index", i)
    76  
    77  		app = &models.Applet{RelApplet: models.RelApplet{AppletID: ins.AppletID}}
    78  		err = app.FetchByAppletID(d)
    79  		if err != nil {
    80  			err = errors.Errorf("%v: failed to get applet %v %v", ins.InstanceID, ins.AppletID, err)
    81  			fails = append(fails, err.Error())
    82  			l.Warn(err)
    83  			continue
    84  		}
    85  
    86  		res, code, err = resource.GetContentBySFID(ctx, app.ResourceID)
    87  		if err != nil {
    88  			err = errors.Errorf("%v: failed to get resource %v %v", ins.InstanceID, app.ResourceID, err)
    89  			fails = append(fails, err.Error())
    90  			l.Warn(err)
    91  			continue
    92  		}
    93  
    94  		ctx = contextx.WithContextCompose(
    95  			types.WithResourceContext(res),
    96  			types.WithAppletContext(app),
    97  		)(ctx)
    98  
    99  		state := ins.State
   100  		l = l.WithValues("state_db", ins.State)
   101  
   102  		_ins, err := UpsertByCode(ctx, nil, code, state, ins.InstanceID)
   103  		if err != nil {
   104  			err = errors.Errorf("%v: failed to deploy %v", ins.InstanceID, err)
   105  			fails = append(fails, err.Error())
   106  			l.Warn(err)
   107  			continue
   108  		}
   109  
   110  		if _ins.State != state {
   111  			l.WithValues("state_mem", ins.State).Warn(errors.New("create vm failed"))
   112  			err = errors.Errorf("%v: instance not started", ins.InstanceID)
   113  			fails = append(fails, err.Error())
   114  			continue
   115  		}
   116  		succs = append(succs, ins.InstanceID.String())
   117  		l.Info("started")
   118  	}
   119  	return nil
   120  }
   121  
   122  func GetBySFID(ctx context.Context, id types.SFID) (*models.Instance, error) {
   123  	d := types.MustMgrDBExecutorFromContext(ctx)
   124  	m := &models.Instance{RelInstance: models.RelInstance{InstanceID: id}}
   125  
   126  	if err := m.FetchByInstanceID(d); err != nil {
   127  		if sqlx.DBErr(err).IsNotFound() {
   128  			return nil, status.InstanceNotFound
   129  		}
   130  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   131  	}
   132  	m.State, _ = vm.GetInstanceState(m.InstanceID)
   133  	return m, nil
   134  }
   135  
   136  func GetByAppletSFID(ctx context.Context, id types.SFID) (*models.Instance, error) {
   137  	d := types.MustMgrDBExecutorFromContext(ctx)
   138  	m := &models.Instance{RelApplet: models.RelApplet{AppletID: id}}
   139  
   140  	if err := m.FetchByAppletID(d); err != nil {
   141  		if sqlx.DBErr(err).IsNotFound() {
   142  			return nil, status.InstanceNotFound
   143  		}
   144  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   145  	}
   146  	m.State, _ = vm.GetInstanceState(m.InstanceID)
   147  	return m, nil
   148  }
   149  
   150  func List(ctx context.Context, r *ListReq) (ret *ListRsp, err error) {
   151  	d := types.MustMgrDBExecutorFromContext(ctx)
   152  	m := &models.Instance{}
   153  
   154  	ret = &ListRsp{}
   155  
   156  	adds := builder.Additions{
   157  		builder.Where(r.Condition()),
   158  		r.Addition(),
   159  		builder.Comment("Instance.ListWithProjectPermission"),
   160  	}
   161  	if r.ProjectID != 0 {
   162  		app := &models.Applet{}
   163  		adds = append(adds,
   164  			builder.LeftJoin(d.T(app)).On(m.ColAppletID().Eq(app.ColAppletID())),
   165  		)
   166  	}
   167  
   168  	err = d.QueryAndScan(builder.Select(nil).From(d.T(m), adds...), &ret.Data)
   169  	if err != nil {
   170  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   171  	}
   172  	err = d.QueryAndScan(builder.Select(builder.Count()).From(d.T(m), adds...), &ret.Total)
   173  	if err != nil {
   174  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   175  	}
   176  
   177  	return ret, nil
   178  }
   179  
   180  func ListByCond(ctx context.Context, r *CondArgs) (data []models.Instance, err error) {
   181  	d := types.MustMgrDBExecutorFromContext(ctx)
   182  	m := &models.Instance{}
   183  
   184  	adds := builder.Additions{
   185  		builder.Where(r.Condition()),
   186  		builder.Comment("Instance.ListWithProjectPermission"),
   187  	}
   188  
   189  	if r.ProjectID != 0 {
   190  		app := &models.Applet{}
   191  		adds = append(adds,
   192  			builder.LeftJoin(d.T(app)).On(m.ColAppletID().Eq(app.ColAppletID())),
   193  		)
   194  	}
   195  
   196  	err = d.QueryAndScan(builder.Select(nil).From(d.T(m), adds...), &data)
   197  	if err != nil {
   198  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   199  	}
   200  	return data, nil
   201  }
   202  
   203  func RemoveBySFID(ctx context.Context, id types.SFID) error {
   204  	d := types.MustMgrDBExecutorFromContext(ctx)
   205  	m := &models.Instance{RelInstance: models.RelInstance{InstanceID: id}}
   206  
   207  	return sqlx.NewTasks(d).With(
   208  		func(d sqlx.DBExecutor) error {
   209  			if err := m.DeleteByInstanceID(d); err != nil {
   210  				return status.DatabaseError.StatusErr().
   211  					WithDesc(errors.Wrap(err, id.String()).Error())
   212  			}
   213  			return nil
   214  		},
   215  		func(d sqlx.DBExecutor) error {
   216  			ctx := types.WithMgrDBExecutor(ctx, d)
   217  			return config.Remove(ctx, &config.CondArgs{RelIDs: []types.SFID{id}})
   218  		},
   219  		func(d sqlx.DBExecutor) error {
   220  			if err := vm.DelInstance(ctx, m.InstanceID); err != nil {
   221  				// Warn
   222  			}
   223  			return nil
   224  		},
   225  		func(d sqlx.DBExecutor) error {
   226  			ctx := types.WithMgrDBExecutor(ctx, d)
   227  			return wasmlog.Remove(ctx, &wasmlog.CondArgs{InstanceID: m.InstanceID})
   228  		},
   229  	).Do()
   230  }
   231  
   232  func RemoveByAppletSFID(ctx context.Context, id types.SFID) (err error) {
   233  	var (
   234  		d = types.MustMgrDBExecutorFromContext(ctx)
   235  		m *models.Instance
   236  	)
   237  
   238  	return sqlx.NewTasks(d).With(
   239  		func(d sqlx.DBExecutor) error {
   240  			ctx := types.WithMgrDBExecutor(ctx, d)
   241  			m, err = GetByAppletSFID(ctx, id)
   242  			return err
   243  		},
   244  		func(d sqlx.DBExecutor) error {
   245  			ctx := types.WithMgrDBExecutor(ctx, d)
   246  			return RemoveBySFID(ctx, m.InstanceID)
   247  		},
   248  	).Do()
   249  }
   250  
   251  func Remove(ctx context.Context, r *CondArgs) error {
   252  	var (
   253  		lst []models.Instance
   254  		err error
   255  	)
   256  
   257  	return sqlx.NewTasks(types.MustMgrDBExecutorFromContext(ctx)).With(
   258  		func(db sqlx.DBExecutor) error {
   259  			ctx := types.WithMgrDBExecutor(ctx, db)
   260  			lst, err = ListByCond(ctx, r)
   261  			return err
   262  		},
   263  		func(db sqlx.DBExecutor) error {
   264  			ctx := types.WithMgrDBExecutor(ctx, db)
   265  			for i := range lst {
   266  				err = RemoveBySFID(ctx, lst[i].InstanceID)
   267  				if err != nil {
   268  					return err
   269  				}
   270  			}
   271  			return nil
   272  		},
   273  	).Do()
   274  }
   275  
   276  // UpsertByCode upsert instance and its config, and deploy wasm if needed
   277  func UpsertByCode(ctx context.Context, r *CreateReq, code []byte, state enums.InstanceState, old ...types.SFID) (*models.Instance, error) {
   278  	ctx, l := logr.Start(ctx, "deploy.UpsertByCode")
   279  	defer l.End()
   280  
   281  	var (
   282  		d         = types.MustMgrDBExecutorFromContext(ctx)
   283  		idg       = confid.MustSFIDGeneratorFromContext(ctx)
   284  		forUpdate = false
   285  	)
   286  
   287  	app := types.MustAppletFromContext(ctx)
   288  	ins := &models.Instance{}
   289  
   290  	if state != enums.INSTANCE_STATE__STARTED && state != enums.INSTANCE_STATE__STOPPED {
   291  		return nil, status.InvalidVMState.StatusErr().WithDesc(state.String())
   292  	}
   293  
   294  	err := sqlx.NewTasks(d).With(
   295  		func(d sqlx.DBExecutor) error {
   296  			ins.AppletID = app.AppletID
   297  			if err := ins.FetchByAppletID(d); err != nil {
   298  				if sqlx.DBErr(err).IsNotFound() {
   299  					forUpdate = false
   300  					ins.InstanceID = idg.MustGenSFID()
   301  					ins.State = state
   302  					return nil
   303  				} else {
   304  					return status.DatabaseError.StatusErr().WithDesc(err.Error())
   305  				}
   306  			}
   307  			if len(old) > 0 && old[0] != ins.InstanceID {
   308  				return status.InvalidAppletContext.StatusErr().WithDesc(
   309  					fmt.Sprintf("database: %v arg: %v", ins.InstanceID, old[0]),
   310  				)
   311  			}
   312  			ins.State = state
   313  			forUpdate = true
   314  			return nil
   315  		},
   316  		func(d sqlx.DBExecutor) error {
   317  			var err error
   318  			if forUpdate {
   319  				err = ins.UpdateByInstanceID(d)
   320  			} else {
   321  				err = ins.Create(d)
   322  			}
   323  			if err != nil {
   324  				if sqlx.DBErr(err).IsConflict() {
   325  					return status.MultiInstanceDeployed.StatusErr().
   326  						WithDesc(app.AppletID.String())
   327  				}
   328  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   329  			}
   330  			return nil
   331  		},
   332  		func(db sqlx.DBExecutor) error {
   333  			ctx := types.WithMgrDBExecutor(ctx, db)
   334  			if r != nil && r.Cache != nil {
   335  				if err := config.Remove(ctx, &config.CondArgs{
   336  					RelIDs: []types.SFID{ins.InstanceID},
   337  				}); err != nil {
   338  					return err
   339  				}
   340  				if _, err := config.Create(ctx, ins.InstanceID, r.Cache); err != nil {
   341  					return err
   342  				}
   343  			}
   344  			return nil
   345  		},
   346  		func(d sqlx.DBExecutor) error {
   347  			ctx := types.WithMgrDBExecutor(ctx, d)
   348  			if forUpdate {
   349  				_ = vm.DelInstance(ctx, ins.InstanceID)
   350  			}
   351  			_ctx, err := WithInstanceRuntimeContext(types.WithInstance(ctx, ins))
   352  			if err != nil {
   353  				return err
   354  			}
   355  			// TODO should below actions be in a critical section?
   356  			if err = vm.NewInstance(_ctx, code, ins.InstanceID, state); err != nil {
   357  				return status.CreateInstanceFailed.StatusErr().WithDesc(err.Error())
   358  			}
   359  			ins.State, _ = vm.GetInstanceState(ins.InstanceID)
   360  			if ins.State != state {
   361  				l.Warn(errors.New("unmatched instance state"))
   362  			}
   363  			return nil
   364  		},
   365  	).Do()
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	return ins, nil
   370  }
   371  
   372  func Upsert(ctx context.Context, r *CreateReq, state enums.InstanceState, old ...types.SFID) (*models.Instance, error) {
   373  	res := types.MustResourceFromContext(ctx)
   374  
   375  	_, code, err := resource.GetContentBySFID(ctx, res.ResourceID)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	return UpsertByCode(ctx, r, code, state, old...)
   381  }
   382  
   383  func Deploy(ctx context.Context, cmd enums.DeployCmd) (err error) {
   384  	var m = types.MustInstanceFromContext(ctx)
   385  
   386  	switch cmd {
   387  	case enums.DEPLOY_CMD__HUNGUP:
   388  		m.State = enums.INSTANCE_STATE__STOPPED
   389  	case enums.DEPLOY_CMD__START:
   390  		m.State = enums.INSTANCE_STATE__STARTED
   391  	default:
   392  		return status.UnknownDeployCommand.StatusErr().
   393  			WithDesc(strconv.Itoa(int(cmd)))
   394  	}
   395  
   396  	return sqlx.NewTasks(types.MustMgrDBExecutorFromContext(ctx)).With(
   397  		func(d sqlx.DBExecutor) error {
   398  			if err = m.UpdateByInstanceID(d); err != nil {
   399  				if sqlx.DBErr(err).IsConflict() {
   400  					return status.MultiInstanceDeployed.StatusErr().
   401  						WithDesc(m.AppletID.String())
   402  				}
   403  				if sqlx.DBErr(err).IsNotFound() {
   404  					return status.InstanceNotFound.StatusErr().
   405  						WithDesc(m.AppletID.String())
   406  				}
   407  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   408  			}
   409  			return nil
   410  		},
   411  		func(d sqlx.DBExecutor) error {
   412  			switch m.State {
   413  			case enums.INSTANCE_STATE__STOPPED:
   414  				err = vm.StopInstance(ctx, m.InstanceID)
   415  			case enums.INSTANCE_STATE__STARTED:
   416  				err = vm.StartInstance(ctx, m.InstanceID)
   417  			}
   418  			if err != nil {
   419  				// Warn
   420  			}
   421  			return nil
   422  		},
   423  	).Do()
   424  }