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 ¬ificationVersion, 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 ¬ificationVersion, 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(¬ificationVersion) 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 = ¬ificationVersion 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 }