github.com/weaviate/weaviate@v1.24.6/usecases/cluster/transactions_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package cluster 13 14 import ( 15 "context" 16 "fmt" 17 "sync" 18 "testing" 19 "time" 20 21 "github.com/sirupsen/logrus/hooks/test" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 func TestSuccessfulOutgoingWriteTransaction(t *testing.T) { 27 payload := "my-payload" 28 trType := TransactionType("my-type") 29 ctx := context.Background() 30 31 man := newTestTxManager() 32 33 tx, err := man.BeginTransaction(ctx, trType, payload, 0) 34 require.Nil(t, err) 35 36 err = man.CommitWriteTransaction(ctx, tx) 37 require.Nil(t, err) 38 } 39 40 func TestTryingToOpenTwoTransactions(t *testing.T) { 41 payload := "my-payload" 42 trType := TransactionType("my-type") 43 ctx := context.Background() 44 45 man := newTestTxManager() 46 47 tx1, err := man.BeginTransaction(ctx, trType, payload, 0) 48 require.Nil(t, err) 49 50 tx2, err := man.BeginTransaction(ctx, trType, payload, 0) 51 assert.Nil(t, tx2) 52 require.NotNil(t, err) 53 assert.Equal(t, "concurrent transaction", err.Error()) 54 55 err = man.CommitWriteTransaction(ctx, tx1) 56 assert.Nil(t, err, "original transaction can still be committed") 57 } 58 59 func TestTryingToCommitInvalidTransaction(t *testing.T) { 60 payload := "my-payload" 61 trType := TransactionType("my-type") 62 ctx := context.Background() 63 64 man := newTestTxManager() 65 66 tx1, err := man.BeginTransaction(ctx, trType, payload, 0) 67 require.Nil(t, err) 68 69 invalidTx := &Transaction{ID: "invalid"} 70 71 err = man.CommitWriteTransaction(ctx, invalidTx) 72 require.NotNil(t, err) 73 assert.Equal(t, "invalid transaction", err.Error()) 74 75 err = man.CommitWriteTransaction(ctx, tx1) 76 assert.Nil(t, err, "original transaction can still be committed") 77 } 78 79 func TestTryingToCommitTransactionPastTTL(t *testing.T) { 80 payload := "my-payload" 81 trType := TransactionType("my-type") 82 ctx := context.Background() 83 84 man := newTestTxManager() 85 86 tx1, err := man.BeginTransaction(ctx, trType, payload, time.Microsecond) 87 require.Nil(t, err) 88 89 expiredTx := &Transaction{ID: tx1.ID} 90 91 // give the cancel handler some time to run 92 time.Sleep(50 * time.Millisecond) 93 94 err = man.CommitWriteTransaction(ctx, expiredTx) 95 require.NotNil(t, err) 96 assert.Contains(t, err.Error(), "transaction TTL") 97 98 // make sure it is possible to open future transactions 99 _, err = man.BeginTransaction(context.Background(), trType, payload, 0) 100 require.Nil(t, err) 101 } 102 103 func TestTryingToCommitIncomingTransactionPastTTL(t *testing.T) { 104 payload := "my-payload" 105 trType := TransactionType("my-type") 106 ctx := context.Background() 107 108 man := newTestTxManager() 109 110 dl := time.Now().Add(1 * time.Microsecond) 111 112 tx := &Transaction{ 113 ID: "123456", 114 Type: trType, 115 Payload: payload, 116 Deadline: dl, 117 } 118 119 man.IncomingBeginTransaction(context.Background(), tx) 120 121 // give the cancel handler some time to run 122 time.Sleep(50 * time.Millisecond) 123 124 err := man.IncomingCommitTransaction(ctx, tx) 125 require.NotNil(t, err) 126 assert.Contains(t, err.Error(), "transaction TTL") 127 128 // make sure it is possible to open future transactions 129 _, err = man.BeginTransaction(context.Background(), trType, payload, 0) 130 require.Nil(t, err) 131 } 132 133 func TestLettingATransactionExpire(t *testing.T) { 134 payload := "my-payload" 135 trType := TransactionType("my-type") 136 ctx := context.Background() 137 138 man := newTestTxManager() 139 140 tx1, err := man.BeginTransaction(ctx, trType, payload, time.Microsecond) 141 require.Nil(t, err) 142 143 // give the cancel handler some time to run 144 time.Sleep(50 * time.Millisecond) 145 146 // try to open a new one 147 _, err = man.BeginTransaction(context.Background(), trType, payload, 0) 148 require.Nil(t, err) 149 150 // since the old one expired, we now expect a TTL error instead of a 151 // concurrent tx error when trying to refer to the old one 152 err = man.CommitWriteTransaction(context.Background(), tx1) 153 require.NotNil(t, err) 154 assert.Contains(t, err.Error(), "transaction TTL") 155 } 156 157 func TestRemoteDoesntAllowOpeningTransaction(t *testing.T) { 158 payload := "my-payload" 159 trType := TransactionType("my-type") 160 ctx := context.Background() 161 broadcaster := &fakeBroadcaster{ 162 openErr: ErrConcurrentTransaction, 163 } 164 165 man := newTestTxManagerWithRemote(broadcaster) 166 167 tx1, err := man.BeginTransaction(ctx, trType, payload, 0) 168 require.Nil(t, tx1) 169 require.NotNil(t, err) 170 assert.Contains(t, err.Error(), "open transaction") 171 172 assert.Len(t, broadcaster.abortCalledId, 36, "a valid uuid was aborted") 173 } 174 175 func TestRemoteDoesntAllowOpeningTransactionAbortFails(t *testing.T) { 176 payload := "my-payload" 177 trType := TransactionType("my-type") 178 ctx := context.Background() 179 broadcaster := &fakeBroadcaster{ 180 openErr: ErrConcurrentTransaction, 181 abortErr: fmt.Errorf("cannot abort"), 182 } 183 184 man, hook := newTestTxManagerWithRemoteLoggerHook(broadcaster) 185 186 tx1, err := man.BeginTransaction(ctx, trType, payload, 0) 187 require.Nil(t, tx1) 188 require.NotNil(t, err) 189 assert.Contains(t, err.Error(), "open transaction") 190 191 assert.Len(t, broadcaster.abortCalledId, 36, "a valid uuid was aborted") 192 193 require.Len(t, hook.Entries, 1) 194 assert.Equal(t, "broadcast tx abort failed", hook.Entries[0].Message) 195 } 196 197 type fakeBroadcaster struct { 198 openErr error 199 commitErr error 200 abortErr error 201 abortCalledId string 202 } 203 204 func (f *fakeBroadcaster) BroadcastTransaction(ctx context.Context, 205 tx *Transaction, 206 ) error { 207 return f.openErr 208 } 209 210 func (f *fakeBroadcaster) BroadcastAbortTransaction(ctx context.Context, 211 tx *Transaction, 212 ) error { 213 f.abortCalledId = tx.ID 214 return f.abortErr 215 } 216 217 func (f *fakeBroadcaster) BroadcastCommitTransaction(ctx context.Context, 218 tx *Transaction, 219 ) error { 220 return f.commitErr 221 } 222 223 func TestSuccessfulDistributedWriteTransaction(t *testing.T) { 224 ctx := context.Background() 225 226 var remoteState interface{} 227 remote := newTestTxManager() 228 remote.SetCommitFn(func(ctx context.Context, tx *Transaction) error { 229 remoteState = tx.Payload 230 return nil 231 }) 232 local := NewTxManager(&wrapTxManagerAsBroadcaster{remote}, 233 &fakeTxPersistence{}, remote.logger) 234 local.StartAcceptIncoming() 235 236 payload := "my-payload" 237 trType := TransactionType("my-type") 238 239 tx, err := local.BeginTransaction(ctx, trType, payload, 0) 240 require.Nil(t, err) 241 242 err = local.CommitWriteTransaction(ctx, tx) 243 require.Nil(t, err) 244 245 assert.Equal(t, "my-payload", remoteState) 246 } 247 248 func TestConcurrentDistributedTransaction(t *testing.T) { 249 ctx := context.Background() 250 251 var remoteState interface{} 252 remote := newTestTxManager() 253 remote.SetCommitFn(func(ctx context.Context, tx *Transaction) error { 254 remoteState = tx.Payload 255 return nil 256 }) 257 local := NewTxManager(&wrapTxManagerAsBroadcaster{remote}, 258 &fakeTxPersistence{}, remote.logger) 259 260 payload := "my-payload" 261 trType := TransactionType("my-type") 262 263 // open a transaction on the remote to simulate a concurrent transaction. 264 // Since it uses the fakeBroadcaster it does not tell anyone about it, this 265 // way we can be sure that the reason for failure is actually a concurrent 266 // transaction on the remote side, not on the local side. Compare this to a 267 // situation where broadcasting was bi-directional: Then this transaction 268 // would have been opened successfully and already be replicated to the 269 // "local" tx manager. So the next call on "local" would also fail, but for 270 // the wrong reason: It would fail because another transaction is already in 271 // place. We, however want to simulate a situation where due to network 272 // delays, etc. both sides try to open a transaction more or less in 273 // parallel. 274 _, err := remote.BeginTransaction(ctx, trType, "wrong payload", 0) 275 require.Nil(t, err) 276 277 tx, err := local.BeginTransaction(ctx, trType, payload, 0) 278 require.Nil(t, tx) 279 require.NotNil(t, err) 280 assert.Contains(t, err.Error(), "concurrent transaction") 281 282 assert.Equal(t, nil, remoteState, "remote state should not have been updated") 283 } 284 285 // This test simulates three nodes trying to open a tx at basically the same 286 // time with the simulated network being so slow that other nodes will try to 287 // open their own transactions before they receive the incoming tx. This is a 288 // situation where everyone thinks they were the first to open the tx and there 289 // is no clear winner. All attempts must fail! 290 func TestConcurrentOpenAttemptsOnSlowNetwork(t *testing.T) { 291 ctx := context.Background() 292 293 broadcaster := &slowMultiBroadcaster{delay: 100 * time.Millisecond} 294 node1 := newTestTxManagerWithRemote(broadcaster) 295 node2 := newTestTxManagerWithRemote(broadcaster) 296 node3 := newTestTxManagerWithRemote(broadcaster) 297 298 broadcaster.nodes = []*TxManager{node1, node2, node3} 299 300 trType := TransactionType("my-type") 301 302 wg := &sync.WaitGroup{} 303 wg.Add(1) 304 go func() { 305 defer wg.Done() 306 _, err := node1.BeginTransaction(ctx, trType, "payload-from-node-1", 0) 307 assert.NotNil(t, err, "open tx 1 must fail") 308 }() 309 310 wg.Add(1) 311 go func() { 312 defer wg.Done() 313 _, err := node2.BeginTransaction(ctx, trType, "payload-from-node-2", 0) 314 assert.NotNil(t, err, "open tx 2 must fail") 315 }() 316 317 wg.Add(1) 318 go func() { 319 defer wg.Done() 320 _, err := node3.BeginTransaction(ctx, trType, "payload-from-node-3", 0) 321 assert.NotNil(t, err, "open tx 3 must fail") 322 }() 323 324 wg.Wait() 325 } 326 327 type wrapTxManagerAsBroadcaster struct { 328 txManager *TxManager 329 } 330 331 func (w *wrapTxManagerAsBroadcaster) BroadcastTransaction(ctx context.Context, 332 tx *Transaction, 333 ) error { 334 _, err := w.txManager.IncomingBeginTransaction(ctx, tx) 335 return err 336 } 337 338 func (w *wrapTxManagerAsBroadcaster) BroadcastAbortTransaction(ctx context.Context, 339 tx *Transaction, 340 ) error { 341 w.txManager.IncomingAbortTransaction(ctx, tx) 342 return nil 343 } 344 345 func (w *wrapTxManagerAsBroadcaster) BroadcastCommitTransaction(ctx context.Context, 346 tx *Transaction, 347 ) error { 348 return w.txManager.IncomingCommitTransaction(ctx, tx) 349 } 350 351 type slowMultiBroadcaster struct { 352 delay time.Duration 353 nodes []*TxManager 354 } 355 356 func (b *slowMultiBroadcaster) BroadcastTransaction(ctx context.Context, 357 tx *Transaction, 358 ) error { 359 time.Sleep(b.delay) 360 for _, node := range b.nodes { 361 if _, err := node.IncomingBeginTransaction(ctx, tx); err != nil { 362 return err 363 } 364 } 365 return nil 366 } 367 368 func (b *slowMultiBroadcaster) BroadcastAbortTransaction(ctx context.Context, 369 tx *Transaction, 370 ) error { 371 time.Sleep(b.delay) 372 for _, node := range b.nodes { 373 node.IncomingAbortTransaction(ctx, tx) 374 } 375 376 return nil 377 } 378 379 func (b *slowMultiBroadcaster) BroadcastCommitTransaction(ctx context.Context, 380 tx *Transaction, 381 ) error { 382 time.Sleep(b.delay) 383 for _, node := range b.nodes { 384 if err := node.IncomingCommitTransaction(ctx, tx); err != nil { 385 return err 386 } 387 } 388 389 return nil 390 } 391 392 func TestSuccessfulDistributedReadTransaction(t *testing.T) { 393 ctx := context.Background() 394 payload := "my-payload" 395 396 remote := newTestTxManager() 397 remote.SetResponseFn(func(ctx context.Context, tx *Transaction) ([]byte, error) { 398 tx.Payload = payload 399 return nil, nil 400 }) 401 local := NewTxManager(&wrapTxManagerAsBroadcaster{remote}, 402 &fakeTxPersistence{}, remote.logger) 403 // TODO local.SetConsensusFn 404 405 trType := TransactionType("my-read-tx") 406 407 tx, err := local.BeginTransaction(ctx, trType, nil, 0) 408 require.Nil(t, err) 409 410 local.CloseReadTransaction(ctx, tx) 411 412 assert.Equal(t, "my-payload", tx.Payload) 413 } 414 415 func TestSuccessfulDistributedTransactionSetAllowUnready(t *testing.T) { 416 ctx := context.Background() 417 payload := "my-payload" 418 419 types := []TransactionType{"type0", "type1"} 420 remote := newTestTxManagerAllowUnready(types) 421 remote.SetResponseFn(func(ctx context.Context, tx *Transaction) ([]byte, error) { 422 tx.Payload = payload 423 return nil, nil 424 }) 425 local := NewTxManager(&wrapTxManagerAsBroadcaster{remote}, 426 &fakeTxPersistence{}, remote.logger) 427 local.SetAllowUnready(types) 428 429 trType := TransactionType("my-read-tx") 430 431 tx, err := local.BeginTransaction(ctx, trType, nil, 0) 432 require.Nil(t, err) 433 434 local.CloseReadTransaction(ctx, tx) 435 436 assert.ElementsMatch(t, types, remote.allowUnready) 437 assert.ElementsMatch(t, types, local.allowUnready) 438 assert.Equal(t, "my-payload", tx.Payload) 439 } 440 441 func TestTxWithDeadline(t *testing.T) { 442 t.Run("expired", func(t *testing.T) { 443 payload := "my-payload" 444 trType := TransactionType("my-type") 445 446 ctx := context.Background() 447 448 man := newTestTxManager() 449 450 tx, err := man.BeginTransaction(ctx, trType, payload, 1*time.Nanosecond) 451 require.Nil(t, err) 452 453 ctx, cancel := context.WithDeadline(context.Background(), tx.Deadline) 454 defer cancel() 455 456 assert.NotNil(t, ctx.Err()) 457 }) 458 459 t.Run("still valid", func(t *testing.T) { 460 payload := "my-payload" 461 trType := TransactionType("my-type") 462 463 ctx := context.Background() 464 465 man := newTestTxManager() 466 467 tx, err := man.BeginTransaction(ctx, trType, payload, 10*time.Second) 468 require.Nil(t, err) 469 470 ctx, cancel := context.WithDeadline(context.Background(), tx.Deadline) 471 defer cancel() 472 473 assert.Nil(t, ctx.Err()) 474 }) 475 } 476 477 func newTestTxManager() *TxManager { 478 logger, _ := test.NewNullLogger() 479 m := NewTxManager(&fakeBroadcaster{}, &fakeTxPersistence{}, logger) 480 m.StartAcceptIncoming() 481 return m 482 } 483 484 func newTestTxManagerWithRemote(remote Remote) *TxManager { 485 logger, _ := test.NewNullLogger() 486 m := NewTxManager(remote, &fakeTxPersistence{}, logger) 487 m.StartAcceptIncoming() 488 return m 489 } 490 491 func newTestTxManagerWithRemoteLoggerHook(remote Remote) (*TxManager, *test.Hook) { 492 logger, hook := test.NewNullLogger() 493 m := NewTxManager(remote, &fakeTxPersistence{}, logger) 494 m.StartAcceptIncoming() 495 return m, hook 496 } 497 498 func newTestTxManagerAllowUnready(types []TransactionType) *TxManager { 499 logger, _ := test.NewNullLogger() 500 m := NewTxManager(&fakeBroadcaster{}, &fakeTxPersistence{}, logger) 501 m.SetAllowUnready(types) 502 m.StartAcceptIncoming() 503 return m 504 } 505 506 // does nothing as these do not involve crashes 507 type fakeTxPersistence struct{} 508 509 func (f *fakeTxPersistence) StoreTx(ctx context.Context, 510 tx *Transaction, 511 ) error { 512 return nil 513 } 514 515 func (f *fakeTxPersistence) DeleteTx(ctx context.Context, 516 txID string, 517 ) error { 518 return nil 519 } 520 521 func (f *fakeTxPersistence) IterateAll(ctx context.Context, 522 cb func(tx *Transaction), 523 ) error { 524 return nil 525 }