go.temporal.io/server@v1.23.0/common/persistence/cassandra/metadata_store.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package cassandra
    26  
    27  import (
    28  	"context"
    29  	"fmt"
    30  
    31  	"go.temporal.io/api/serviceerror"
    32  
    33  	"go.temporal.io/server/common/log"
    34  	"go.temporal.io/server/common/log/tag"
    35  	p "go.temporal.io/server/common/persistence"
    36  	"go.temporal.io/server/common/persistence/nosql/nosqlplugin/cassandra/gocql"
    37  	"go.temporal.io/server/common/primitives"
    38  )
    39  
    40  const (
    41  	constNamespacePartition     = 0
    42  	namespaceMetadataRecordName = "temporal-namespace-metadata"
    43  )
    44  
    45  const (
    46  	templateCreateNamespaceQuery = `INSERT INTO namespaces_by_id (` +
    47  		`id, name) ` +
    48  		`VALUES(?, ?) IF NOT EXISTS`
    49  
    50  	templateGetNamespaceQuery = `SELECT name ` +
    51  		`FROM namespaces_by_id ` +
    52  		`WHERE id = ?`
    53  
    54  	templateDeleteNamespaceQuery = `DELETE FROM namespaces_by_id ` +
    55  		`WHERE id = ?`
    56  
    57  	templateNamespaceColumns = `id, name, detail, detail_encoding, notification_version, is_global_namespace`
    58  
    59  	templateCreateNamespaceByNameQueryWithinBatchV2 = `INSERT INTO namespaces ` +
    60  		`( namespaces_partition, ` + templateNamespaceColumns + `) ` +
    61  		`VALUES(?, ?, ?, ?, ?, ?, ?) IF NOT EXISTS`
    62  
    63  	templateGetNamespaceByNameQueryV2 = templateListNamespaceQueryV2 + `and name = ?`
    64  
    65  	templateUpdateNamespaceByNameQueryWithinBatchV2 = `UPDATE namespaces ` +
    66  		`SET detail = ? ,` +
    67  		`detail_encoding = ? ,` +
    68  		`is_global_namespace = ? ,` +
    69  		`notification_version = ? ` +
    70  		`WHERE namespaces_partition = ? ` +
    71  		`and name = ?`
    72  
    73  	templateGetMetadataQueryV2 = `SELECT notification_version ` +
    74  		`FROM namespaces ` +
    75  		`WHERE namespaces_partition = ? ` +
    76  		`and name = ? `
    77  
    78  	templateUpdateMetadataQueryWithinBatchV2 = `UPDATE namespaces ` +
    79  		`SET notification_version = ? ` +
    80  		`WHERE namespaces_partition = ? ` +
    81  		`and name = ? ` +
    82  		`IF notification_version = ? `
    83  
    84  	templateDeleteNamespaceByNameQueryV2 = `DELETE FROM namespaces ` +
    85  		`WHERE namespaces_partition = ? ` +
    86  		`and name = ?`
    87  
    88  	templateListNamespaceQueryV2 = `SELECT ` +
    89  		templateNamespaceColumns +
    90  		` FROM namespaces ` +
    91  		`WHERE namespaces_partition = ? `
    92  
    93  	templateUpdateNamespaceByIdQuery = `UPDATE namespaces_by_id ` +
    94  		`SET name = ? ` +
    95  		`WHERE id = ?`
    96  )
    97  
    98  type (
    99  	MetadataStore struct {
   100  		session            gocql.Session
   101  		logger             log.Logger
   102  		currentClusterName string
   103  	}
   104  )
   105  
   106  // NewMetadataStore is used to create an instance of the Namespace MetadataStore implementation
   107  func NewMetadataStore(
   108  	currentClusterName string,
   109  	session gocql.Session,
   110  	logger log.Logger,
   111  ) (p.MetadataStore, error) {
   112  	return &MetadataStore{
   113  		currentClusterName: currentClusterName,
   114  		session:            session,
   115  		logger:             logger,
   116  	}, nil
   117  }
   118  
   119  // CreateNamespace create a namespace
   120  // Cassandra does not support conditional updates across multiple tables.  For this reason we have to first insert into
   121  // 'Namespaces' table and then do a conditional insert into namespaces_by_name table.  If the conditional write fails we
   122  // delete the orphaned entry from namespaces table.  There is a chance delete entry could fail and we never delete the
   123  // orphaned entry from namespaces table.  We might need a background job to delete those orphaned record.
   124  func (m *MetadataStore) CreateNamespace(
   125  	ctx context.Context,
   126  	request *p.InternalCreateNamespaceRequest,
   127  ) (*p.CreateNamespaceResponse, error) {
   128  
   129  	query := m.session.Query(templateCreateNamespaceQuery, request.ID, request.Name).WithContext(ctx)
   130  	existingRow := make(map[string]interface{})
   131  	applied, err := query.MapScanCAS(existingRow)
   132  	if err != nil {
   133  		return nil, serviceerror.NewUnavailable(fmt.Sprintf("CreateNamespace operation failed. Inserting into namespaces table. Error: %v", err))
   134  	}
   135  
   136  	if !applied {
   137  		// if the id with the same name exists in `namespaces_by_id`, fall through and either add a row in `namespaces` table
   138  		// or fail if name exists in that table already. This is to make sure we do not end up with a row in `namespaces_by_id`
   139  		// table and no entry in `namespaces` table
   140  		matched, err := hasNameConflict(existingRow, "name", request.Name)
   141  		if err != nil {
   142  			return nil, err
   143  		}
   144  		if !matched {
   145  			msg := fmt.Sprintf("CreateNamespace with name %v and id %v failed because another namespace with name %v already exists with the same id.", request.Name, request.ID, existingRow["name"])
   146  			return nil, serviceerror.NewNamespaceAlreadyExists(msg)
   147  		}
   148  	}
   149  	return m.CreateNamespaceInV2Table(ctx, request)
   150  }
   151  
   152  // CreateNamespaceInV2Table is the temporary function used by namespace v1 -> v2 migration
   153  func (m *MetadataStore) CreateNamespaceInV2Table(
   154  	ctx context.Context,
   155  	request *p.InternalCreateNamespaceRequest,
   156  ) (*p.CreateNamespaceResponse, error) {
   157  	metadata, err := m.GetMetadata(ctx)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	batch := m.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
   163  	batch.Query(templateCreateNamespaceByNameQueryWithinBatchV2,
   164  		constNamespacePartition,
   165  		request.ID,
   166  		request.Name,
   167  		request.Namespace.Data,
   168  		request.Namespace.EncodingType.String(),
   169  		metadata.NotificationVersion,
   170  		request.IsGlobal,
   171  	)
   172  	m.updateMetadataBatch(batch, metadata.NotificationVersion)
   173  
   174  	previous := make(map[string]interface{})
   175  	applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous)
   176  	if err != nil {
   177  		return nil, serviceerror.NewUnavailable(fmt.Sprintf("CreateNamespace operation failed. Inserting into namespaces table. Error: %v", err))
   178  	}
   179  	defer func() { _ = iter.Close() }()
   180  	deleteOrphanNamespace := func() {
   181  		// Delete namespace from `namespaces_by_id`
   182  		if errDelete := m.session.Query(templateDeleteNamespaceQuery, request.ID).WithContext(ctx).Exec(); errDelete != nil {
   183  			m.logger.Warn("Unable to delete orphan namespace record. Error", tag.Error(errDelete))
   184  		}
   185  	}
   186  
   187  	if !applied {
   188  
   189  		// if both conditions fail, find the one related to the first query
   190  		matched, err := hasNameConflict(previous, "name", request.Name)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		if !matched {
   195  			m := make(map[string]interface{})
   196  			if iter.MapScan(m) {
   197  				previous = m
   198  			}
   199  		}
   200  
   201  		// if conditional failure is due to a duplicate name in namespaces table
   202  		matched, err = hasNameConflict(previous, "name", request.Name)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  		if matched {
   207  			var existingID string
   208  			if id, ok := previous["id"]; ok {
   209  				existingID = gocql.UUIDToString(id)
   210  				if existingID != request.ID {
   211  					// Delete orphan namespace record before returning back to user
   212  					deleteOrphanNamespace()
   213  				}
   214  			}
   215  
   216  			msg := fmt.Sprintf("Namespace already exists.  NamespaceId: %v", existingID)
   217  			return nil, serviceerror.NewNamespaceAlreadyExists(msg)
   218  
   219  		}
   220  
   221  		// If namespace does not exist already and applied is false,
   222  		// notification_version does not match our expectations and it's conditional failure.
   223  		// Delete orphan namespace record before returning back to user
   224  		deleteOrphanNamespace()
   225  		return nil, serviceerror.NewUnavailable("CreateNamespace operation failed because of conditional failure.")
   226  	}
   227  
   228  	return &p.CreateNamespaceResponse{ID: request.ID}, nil
   229  }
   230  
   231  func (m *MetadataStore) UpdateNamespace(
   232  	ctx context.Context,
   233  	request *p.InternalUpdateNamespaceRequest,
   234  ) error {
   235  	batch := m.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
   236  	batch.Query(templateUpdateNamespaceByNameQueryWithinBatchV2,
   237  		request.Namespace.Data,
   238  		request.Namespace.EncodingType.String(),
   239  		request.IsGlobal,
   240  		request.NotificationVersion,
   241  		constNamespacePartition,
   242  		request.Name,
   243  	)
   244  	m.updateMetadataBatch(batch, request.NotificationVersion)
   245  
   246  	previous := make(map[string]interface{})
   247  	applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous)
   248  	if err != nil {
   249  		return serviceerror.NewUnavailable(fmt.Sprintf("UpdateNamespace operation failed. Error: %v", err))
   250  	}
   251  	defer func() { _ = iter.Close() }()
   252  
   253  	if !applied {
   254  		return serviceerror.NewUnavailable("UpdateNamespace operation failed because of conditional failure.")
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  // RenameNamespace should be used with caution.
   261  // Not every namespace can be renamed because namespace name are stored in the database.
   262  // It may leave database in inconsistent state and must be retried until success.
   263  // Step 1. Update row in `namespaces_by_id` table with the new name.
   264  // Step 2. Batch of:
   265  //
   266  //	Insert row into `namespaces` table with new name and new `notification_version`.
   267  //	Delete row from `namespaces` table with old name.
   268  //	Update `notification_version` in metadata row.
   269  //
   270  // NOTE: `namespaces_by_id` is currently used only for `DescribeNamespace` API and namespace Id collision check.
   271  func (m *MetadataStore) RenameNamespace(
   272  	ctx context.Context,
   273  	request *p.InternalRenameNamespaceRequest,
   274  ) error {
   275  	// Step 1.
   276  	if updateErr := m.session.Query(templateUpdateNamespaceByIdQuery,
   277  		request.Name,
   278  		request.Id,
   279  	).WithContext(ctx).Exec(); updateErr != nil {
   280  		return serviceerror.NewUnavailable(fmt.Sprintf("RenameNamespace operation failed to update 'namespaces_by_id' table. Error: %v", updateErr))
   281  	}
   282  
   283  	// Step 2.
   284  	batch := m.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
   285  	batch.Query(templateCreateNamespaceByNameQueryWithinBatchV2,
   286  		constNamespacePartition,
   287  		request.Id,
   288  		request.Name,
   289  		request.Namespace.Data,
   290  		request.Namespace.EncodingType.String(),
   291  		request.NotificationVersion,
   292  		request.IsGlobal,
   293  	)
   294  	batch.Query(templateDeleteNamespaceByNameQueryV2,
   295  		constNamespacePartition,
   296  		request.PreviousName,
   297  	)
   298  	m.updateMetadataBatch(batch, request.NotificationVersion)
   299  
   300  	previous := make(map[string]interface{})
   301  	applied, iter, err := m.session.MapExecuteBatchCAS(batch, previous)
   302  	if err != nil {
   303  		return serviceerror.NewUnavailable(fmt.Sprintf("RenameNamespace operation failed. Error: %v", err))
   304  	}
   305  	defer func() { _ = iter.Close() }()
   306  
   307  	if !applied {
   308  		return serviceerror.NewUnavailable("RenameNamespace operation failed because of conditional failure.")
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  func (m *MetadataStore) GetNamespace(
   315  	ctx context.Context,
   316  	request *p.GetNamespaceRequest,
   317  ) (*p.InternalGetNamespaceResponse, error) {
   318  	var query gocql.Query
   319  	var err error
   320  	var detail []byte
   321  	var detailEncoding string
   322  	var notificationVersion int64
   323  	var isGlobalNamespace bool
   324  
   325  	if len(request.ID) > 0 && len(request.Name) > 0 {
   326  		return nil, serviceerror.NewInvalidArgument("GetNamespace operation failed.  Both ID and Name specified in request.Namespace.")
   327  	} else if len(request.ID) == 0 && len(request.Name) == 0 {
   328  		return nil, serviceerror.NewInvalidArgument("GetNamespace operation failed.  Both ID and Name are empty.")
   329  	}
   330  
   331  	handleError := func(name string, ID string, err error) error {
   332  		identity := name
   333  		if gocql.IsNotFoundError(err) {
   334  			if len(ID) > 0 {
   335  				identity = ID
   336  			}
   337  			return serviceerror.NewNamespaceNotFound(identity)
   338  		}
   339  		return serviceerror.NewUnavailable(fmt.Sprintf("GetNamespace operation failed. Error %v", err))
   340  	}
   341  
   342  	namespace := request.Name
   343  	if len(request.ID) > 0 {
   344  		query = m.session.Query(templateGetNamespaceQuery, request.ID).WithContext(ctx)
   345  		err = query.Scan(&namespace)
   346  		if err != nil {
   347  			return nil, handleError(request.Name, request.ID, err)
   348  		}
   349  	}
   350  
   351  	query = m.session.Query(templateGetNamespaceByNameQueryV2, constNamespacePartition, namespace).WithContext(ctx)
   352  	err = query.Scan(
   353  		nil,
   354  		nil,
   355  		&detail,
   356  		&detailEncoding,
   357  		&notificationVersion,
   358  		&isGlobalNamespace,
   359  	)
   360  
   361  	if err != nil {
   362  		return nil, handleError(request.Name, request.ID, err)
   363  	}
   364  
   365  	return &p.InternalGetNamespaceResponse{
   366  		Namespace:           p.NewDataBlob(detail, detailEncoding),
   367  		IsGlobal:            isGlobalNamespace,
   368  		NotificationVersion: notificationVersion,
   369  	}, nil
   370  }
   371  
   372  func (m *MetadataStore) ListNamespaces(
   373  	ctx context.Context,
   374  	request *p.InternalListNamespacesRequest,
   375  ) (*p.InternalListNamespacesResponse, error) {
   376  	query := m.session.Query(templateListNamespaceQueryV2, constNamespacePartition).WithContext(ctx)
   377  	pageSize := request.PageSize
   378  	nextPageToken := request.NextPageToken
   379  	response := &p.InternalListNamespacesResponse{}
   380  
   381  	for {
   382  		iter := query.PageSize(pageSize).PageState(nextPageToken).Iter()
   383  		skippedRows := 0
   384  
   385  		for {
   386  			var name string
   387  			var detail []byte
   388  			var detailEncoding string
   389  			var notificationVersion int64
   390  			var isGlobal bool
   391  			if !iter.Scan(
   392  				nil,
   393  				&name,
   394  				&detail,
   395  				&detailEncoding,
   396  				&notificationVersion,
   397  				&isGlobal,
   398  			) {
   399  				// done iterating over all namespaces in this page
   400  				break
   401  			}
   402  
   403  			// do not include the metadata record
   404  			if name == namespaceMetadataRecordName {
   405  				skippedRows++
   406  				continue
   407  			}
   408  			response.Namespaces = append(response.Namespaces, &p.InternalGetNamespaceResponse{
   409  				Namespace:           p.NewDataBlob(detail, detailEncoding),
   410  				IsGlobal:            isGlobal,
   411  				NotificationVersion: notificationVersion,
   412  			})
   413  		}
   414  		if len(iter.PageState()) > 0 {
   415  			nextPageToken = iter.PageState()
   416  		} else {
   417  			nextPageToken = nil
   418  		}
   419  		if err := iter.Close(); err != nil {
   420  			return nil, serviceerror.NewUnavailable(fmt.Sprintf("ListNamespaces operation failed. Error: %v", err))
   421  		}
   422  
   423  		if len(nextPageToken) == 0 {
   424  			// No more records in DB.
   425  			break
   426  		}
   427  		if skippedRows == 0 {
   428  			break
   429  		}
   430  		pageSize = skippedRows
   431  	}
   432  
   433  	response.NextPageToken = nextPageToken
   434  	return response, nil
   435  }
   436  
   437  func (m *MetadataStore) DeleteNamespace(
   438  	ctx context.Context,
   439  	request *p.DeleteNamespaceRequest,
   440  ) error {
   441  	var name string
   442  	query := m.session.Query(templateGetNamespaceQuery, request.ID).WithContext(ctx)
   443  	err := query.Scan(&name)
   444  	if err != nil {
   445  		if gocql.IsNotFoundError(err) {
   446  			return nil
   447  		}
   448  		return err
   449  	}
   450  
   451  	parsedID, err := primitives.ParseUUID(request.ID)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	return m.deleteNamespace(ctx, name, parsedID)
   456  }
   457  
   458  func (m *MetadataStore) DeleteNamespaceByName(
   459  	ctx context.Context,
   460  	request *p.DeleteNamespaceByNameRequest,
   461  ) error {
   462  	var ID []byte
   463  	query := m.session.Query(templateGetNamespaceByNameQueryV2, constNamespacePartition, request.Name).WithContext(ctx)
   464  	err := query.Scan(&ID, nil, nil, nil, nil, nil)
   465  	if err != nil {
   466  		if gocql.IsNotFoundError(err) {
   467  			return nil
   468  		}
   469  		return err
   470  	}
   471  	return m.deleteNamespace(ctx, request.Name, ID)
   472  }
   473  
   474  func (m *MetadataStore) GetMetadata(
   475  	ctx context.Context,
   476  ) (*p.GetMetadataResponse, error) {
   477  	var notificationVersion int64
   478  	query := m.session.Query(templateGetMetadataQueryV2, constNamespacePartition, namespaceMetadataRecordName).WithContext(ctx)
   479  	err := query.Scan(&notificationVersion)
   480  	if err != nil {
   481  		if gocql.IsNotFoundError(err) {
   482  			// this error can be thrown in the very beginning,
   483  			// i.e. when namespaces is initialized
   484  			return &p.GetMetadataResponse{NotificationVersion: 0}, nil
   485  		}
   486  		return nil, err
   487  	}
   488  	return &p.GetMetadataResponse{NotificationVersion: notificationVersion}, nil
   489  }
   490  
   491  func (m *MetadataStore) updateMetadataBatch(
   492  	batch gocql.Batch,
   493  	notificationVersion int64,
   494  ) {
   495  	var nextVersion int64 = 1
   496  	var currentVersion *int64
   497  	if notificationVersion > 0 {
   498  		nextVersion = notificationVersion + 1
   499  		currentVersion = &notificationVersion
   500  	}
   501  	batch.Query(templateUpdateMetadataQueryWithinBatchV2,
   502  		nextVersion,
   503  		constNamespacePartition,
   504  		namespaceMetadataRecordName,
   505  		currentVersion,
   506  	)
   507  }
   508  
   509  func (m *MetadataStore) deleteNamespace(ctx context.Context, name string, ID []byte) error {
   510  	query := m.session.Query(templateDeleteNamespaceByNameQueryV2, constNamespacePartition, name).WithContext(ctx)
   511  	if err := query.Exec(); err != nil {
   512  		return serviceerror.NewUnavailable(fmt.Sprintf("DeleteNamespaceByName operation failed. Error %v", err))
   513  	}
   514  
   515  	query = m.session.Query(templateDeleteNamespaceQuery, ID).WithContext(ctx)
   516  	if err := query.Exec(); err != nil {
   517  		return serviceerror.NewUnavailable(fmt.Sprintf("DeleteNamespace operation failed. Error %v", err))
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  func (m *MetadataStore) GetName() string {
   524  	return cassandraPersistenceName
   525  }
   526  
   527  func (m *MetadataStore) Close() {
   528  	if m.session != nil {
   529  		m.session.Close()
   530  	}
   531  }
   532  
   533  func hasNameConflict[T comparable](row map[string]interface{}, column string, value T) (bool, error) {
   534  	existingValue, ok := row[column]
   535  	if !ok {
   536  		msg := fmt.Sprintf("Unexpected error: column not found %q", column)
   537  		return false, serviceerror.NewInternal(msg)
   538  	}
   539  	return existingValue == value, nil
   540  }