github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/cluster/impl_deployapp.go (about)

     1  /*
     2   * Copyright (c) 2024-present unTill Software Development Group B.V.
     3   * @author Denis Gribanov
     4   */
     5  
     6  package cluster
     7  
     8  import (
     9  	"fmt"
    10  	"net/http"
    11  
    12  	"github.com/voedger/voedger/pkg/appdef"
    13  	"github.com/voedger/voedger/pkg/goutils/logger"
    14  	"github.com/voedger/voedger/pkg/istructs"
    15  	"github.com/voedger/voedger/pkg/istructsmem"
    16  	"github.com/voedger/voedger/pkg/state"
    17  	"github.com/voedger/voedger/pkg/sys/authnz"
    18  	"github.com/voedger/voedger/pkg/sys/uniques"
    19  	"github.com/voedger/voedger/pkg/sys/workspace"
    20  	coreutils "github.com/voedger/voedger/pkg/utils"
    21  )
    22  
    23  // wrong to use IAppPartitions to get total NumAppPartition because the app the cmd is called for is not deployed yet
    24  func provideExecDeployApp(asp istructs.IAppStructsProvider, timeFunc coreutils.TimeFunc) istructsmem.ExecCommandClosure {
    25  	return func(args istructs.ExecCommandArgs) (err error) {
    26  		appQNameStr := args.ArgumentObject.AsString(Field_AppQName)
    27  		appQName, err := istructs.ParseAppQName(appQNameStr)
    28  		if err != nil {
    29  			return coreutils.NewHTTPErrorf(http.StatusBadRequest, fmt.Sprintf("failed to parse AppQName %s: %s", appQNameStr, err.Error()))
    30  		}
    31  
    32  		if appQName == istructs.AppQName_sys_cluster {
    33  			return coreutils.NewHTTPErrorf(http.StatusBadRequest, fmt.Sprintf("%s app can not be deployed by c.cluster.DeployApp", istructs.AppQName_sys_cluster))
    34  		}
    35  
    36  		clusterAppStructs, err := asp.AppStructs(istructs.AppQName_sys_cluster)
    37  		if err != nil {
    38  			// notest
    39  			return err
    40  		}
    41  		wdocAppRecordID, err := uniques.GetRecordIDByUniqueCombination(args.WSID, qNameWDocApp, clusterAppStructs, map[string]interface{}{
    42  			Field_AppQName: appQNameStr,
    43  		})
    44  		if err != nil {
    45  			// notest
    46  			return err
    47  		}
    48  		numAppWorkspacesToDeploy := istructs.NumAppWorkspaces(args.ArgumentObject.AsInt32(Field_NumAppWorkspaces))
    49  		numAppPartitionsToDeploy := istructs.NumAppPartitions(args.ArgumentObject.AsInt32(Field_NumPartitions))
    50  		if wdocAppRecordID != istructs.NullRecordID {
    51  			kb, err := args.State.KeyBuilder(state.Record, qNameWDocApp)
    52  			if err != nil {
    53  				// notest
    54  				return err
    55  			}
    56  			kb.PutRecordID(state.Field_ID, wdocAppRecordID)
    57  			appRec, err := args.State.MustExist(kb)
    58  			if err != nil {
    59  				// notest
    60  				return err
    61  			}
    62  			numPartitionsDeployed := istructs.NumAppPartitions(appRec.AsInt32(Field_NumPartitions))
    63  			numAppWorkspacesDeployed := istructs.NumAppWorkspaces(appRec.AsInt32(Field_NumAppWorkspaces))
    64  
    65  			// Check application compatibility (409)
    66  			if numPartitionsDeployed != numAppPartitionsToDeploy {
    67  				return coreutils.NewHTTPErrorf(http.StatusConflict, fmt.Sprintf("%s: app %s declaring NumPartitions=%d but was previously deployed with NumPartitions=%d", ErrNumPartitionsChanged.Error(),
    68  					appQName, numAppPartitionsToDeploy, numPartitionsDeployed))
    69  			}
    70  			if numAppWorkspacesDeployed != numAppWorkspacesToDeploy {
    71  				return coreutils.NewHTTPErrorf(http.StatusConflict, fmt.Sprintf("%s: app %s declaring NumAppWorkspaces=%d but was previously deployed with NumAppWorksaces=%d", ErrNumAppWorkspacesChanged.Error(),
    72  					appQName, numAppWorkspacesToDeploy, numAppWorkspacesDeployed))
    73  			}
    74  
    75  			// idempotency: was deployed already and nothing changed -> do not initiaize app workspaces
    76  			return nil
    77  		}
    78  
    79  		kb, err := args.State.KeyBuilder(state.Record, qNameWDocApp)
    80  		if err != nil {
    81  			// notest
    82  			return err
    83  		}
    84  		vb, err := args.Intents.NewValue(kb)
    85  		if err != nil {
    86  			// notest
    87  			return err
    88  		}
    89  
    90  		vb.PutRecordID(appdef.SystemField_ID, 1)
    91  		vb.PutString(Field_AppQName, appQNameStr)
    92  		vb.PutInt32(Field_NumAppWorkspaces, int32(numAppWorkspacesToDeploy))
    93  		vb.PutInt32(Field_NumPartitions, int32(numAppPartitionsToDeploy))
    94  
    95  		// Create storage if not exists
    96  		// Initialize appstructs data
    97  		// note: for builtin apps that does nothing because IAppStructs is already initialized (including storage initialization) on VVM wiring
    98  		// note: it is good that it is done here, not before return if nothing changed because we're want to initialize (i.e. create) keyspace here - that must be done once
    99  		as, err := asp.AppStructs(appQName)
   100  		if err != nil {
   101  			// notest
   102  			return fmt.Errorf("failed to get IAppStructs for %s", appQName)
   103  		}
   104  
   105  		// Initialize app workspaces
   106  		if _, err = InitAppWSes(as, numAppWorkspacesToDeploy, numAppPartitionsToDeploy, istructs.UnixMilli(timeFunc().UnixMilli())); err != nil {
   107  			// notest
   108  			return fmt.Errorf("failed to deploy %s: %w", appQName, err)
   109  		}
   110  		logger.Info(fmt.Sprintf("app %s successfully deployed: NumPartitions=%d, NumAppWorkspaces=%d", appQName, numAppPartitionsToDeploy, numAppWorkspacesToDeploy))
   111  		return nil
   112  	}
   113  }
   114  
   115  // returns an array of inited AppWSIDs. Inited already -> AppWSID is not in the array. Need for testing only
   116  func InitAppWSes(as istructs.IAppStructs, numAppWorkspaces istructs.NumAppWorkspaces, numAppPartitions istructs.NumAppPartitions, currentMillis istructs.UnixMilli) ([]istructs.WSID, error) {
   117  	pLogOffsets := map[istructs.PartitionID]istructs.Offset{}
   118  	wLogOffset := istructs.FirstOffset
   119  	res := []istructs.WSID{}
   120  	for wsNum := 0; istructs.NumAppWorkspaces(wsNum) < numAppWorkspaces; wsNum++ {
   121  		appWSID := istructs.NewWSID(istructs.MainClusterID, istructs.WSID(wsNum+int(istructs.FirstBaseAppWSID)))
   122  		partitionID := coreutils.AppPartitionID(appWSID, numAppPartitions)
   123  		if _, ok := pLogOffsets[partitionID]; !ok {
   124  			pLogOffsets[partitionID] = istructs.FirstOffset
   125  		}
   126  		inited, err := InitAppWS(as, partitionID, appWSID, pLogOffsets[partitionID], wLogOffset, currentMillis)
   127  		if err != nil {
   128  			// notest
   129  			return nil, err
   130  		}
   131  		pLogOffsets[partitionID]++
   132  		wLogOffset++
   133  		if inited {
   134  			res = append(res, appWSID)
   135  		}
   136  	}
   137  	return res, nil
   138  }
   139  
   140  func InitAppWS(as istructs.IAppStructs, partitionID istructs.PartitionID, appWSID istructs.WSID, plogOffset, wlogOffset istructs.Offset, currentMillis istructs.UnixMilli) (inited bool, err error) {
   141  	existingCDocWSDesc, err := as.Records().GetSingleton(appWSID, authnz.QNameCDocWorkspaceDescriptor)
   142  	if err != nil {
   143  		// notest
   144  		return false, err
   145  	}
   146  	if existingCDocWSDesc.QName() != appdef.NullQName {
   147  		logger.Verbose("app workspace", as.AppQName(), appWSID-appWSID.BaseWSID(), "(", appWSID, ") inited already")
   148  		return false, nil
   149  	}
   150  
   151  	grebp := istructs.GenericRawEventBuilderParams{
   152  		HandlingPartition: partitionID,
   153  		Workspace:         appWSID,
   154  		QName:             istructs.QNameCommandCUD,
   155  		RegisteredAt:      currentMillis,
   156  		PLogOffset:        plogOffset,
   157  		WLogOffset:        wlogOffset,
   158  	}
   159  	reb := as.Events().GetSyncRawEventBuilder(
   160  		istructs.SyncRawEventBuilderParams{
   161  			GenericRawEventBuilderParams: grebp,
   162  			SyncedAt:                     currentMillis,
   163  		},
   164  	)
   165  	cdocWSDesc := reb.CUDBuilder().Create(authnz.QNameCDocWorkspaceDescriptor)
   166  	cdocWSDesc.PutRecordID(appdef.SystemField_ID, 1)
   167  	cdocWSDesc.PutString(authnz.Field_WSName, "appWS0")
   168  	cdocWSDesc.PutQName(authnz.Field_WSKind, authnz.QNameCDoc_WorkspaceKind_AppWorkspace)
   169  	cdocWSDesc.PutInt64(authnz.Field_CreatedAtMs, int64(currentMillis))
   170  	cdocWSDesc.PutInt64(workspace.Field_InitCompletedAtMs, int64(currentMillis))
   171  	rawEvent, err := reb.BuildRawEvent()
   172  	if err != nil {
   173  		// notest
   174  		return false, err
   175  	}
   176  	// ok to local IDGenerator here. Actual next record IDs will be determined on the partition recovery stage
   177  	pLogEvent, err := as.Events().PutPlog(rawEvent, nil, istructsmem.NewIDGenerator())
   178  	if err != nil {
   179  		// notest
   180  		return false, err
   181  	}
   182  	defer pLogEvent.Release()
   183  	if err := as.Records().Apply(pLogEvent); err != nil {
   184  		// notest
   185  		return false, err
   186  	}
   187  	if err = as.Events().PutWlog(pLogEvent); err != nil {
   188  		// notest
   189  		return false, err
   190  	}
   191  	logger.Verbose("app workspace", as.AppQName(), appWSID.BaseWSID()-istructs.FirstBaseAppWSID, "(", appWSID, ") initialized")
   192  	return true, nil
   193  }