github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/session_write_test.go (about) 1 // Copyright (c) 2016 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package client 22 23 import ( 24 "errors" 25 "fmt" 26 "strconv" 27 "strings" 28 "sync" 29 "testing" 30 "time" 31 32 "github.com/m3db/m3/src/dbnode/generated/thrift/rpc" 33 "github.com/m3db/m3/src/dbnode/topology" 34 xmetrics "github.com/m3db/m3/src/dbnode/x/metrics" 35 xerrors "github.com/m3db/m3/src/x/errors" 36 "github.com/m3db/m3/src/x/ident" 37 "github.com/m3db/m3/src/x/instrument" 38 xretry "github.com/m3db/m3/src/x/retry" 39 xtest "github.com/m3db/m3/src/x/test" 40 xtime "github.com/m3db/m3/src/x/time" 41 42 "github.com/golang/mock/gomock" 43 "github.com/stretchr/testify/assert" 44 "github.com/stretchr/testify/require" 45 "github.com/uber-go/tally" 46 ) 47 48 func TestSessionWriteNotOpenError(t *testing.T) { 49 ctrl := gomock.NewController(t) 50 defer ctrl.Finish() 51 52 s := newDefaultTestSession(t) 53 54 err := s.Write(ident.StringID("namespace"), ident.StringID("foo"), xtime.Now(), 55 1.337, xtime.Second, nil) 56 assert.Equal(t, ErrSessionStatusNotOpen, err) 57 } 58 59 func TestSessionWrite(t *testing.T) { 60 testSessionWrite(t, testOptions{ 61 opts: newSessionTestOptions(), 62 }) 63 } 64 65 func testSessionWrite(t *testing.T, testOpts testOptions) { 66 ctrl := gomock.NewController(t) 67 defer ctrl.Finish() 68 69 session := newTestSession(t, testOpts.opts).(*session) 70 71 w := newWriteStub() 72 if testOpts.setWriteAnn != nil { 73 testOpts.setWriteAnn(&w) 74 } 75 76 var completionFn completionFn 77 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) { 78 completionFn = op.CompletionFn() 79 write, ok := op.(*writeOperation) 80 assert.True(t, ok) 81 assert.Equal(t, w.id.String(), string(write.request.ID)) 82 assert.Equal(t, w.value, write.request.Datapoint.Value) 83 assert.Equal(t, w.t.Seconds(), write.request.Datapoint.Timestamp) 84 assert.Equal(t, rpc.TimeType_UNIX_SECONDS, write.request.Datapoint.TimestampTimeType) 85 assert.NotNil(t, write.completionFn) 86 if testOpts.annEqual != nil { 87 testOpts.annEqual(t, w.annotation, write.request.Datapoint.Annotation) 88 } 89 }}) 90 91 assert.NoError(t, session.Open()) 92 93 // Ensure consecutive opens cause errors 94 consecutiveOpenErr := session.Open() 95 assert.Error(t, consecutiveOpenErr) 96 assert.Equal(t, errSessionStatusNotInitial, consecutiveOpenErr) 97 98 // Begin write 99 var resultErr error 100 var writeWg sync.WaitGroup 101 writeWg.Add(1) 102 go func() { 103 resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation) 104 writeWg.Done() 105 }() 106 107 // Callback 108 enqueueWg.Wait() 109 for i := 0; i < session.state.topoMap.Replicas(); i++ { 110 completionFn(session.state.topoMap.Hosts()[0], nil) 111 } 112 113 // Wait for write to complete 114 writeWg.Wait() 115 assert.Nil(t, resultErr) 116 117 assert.NoError(t, session.Close()) 118 } 119 120 func TestSessionWriteDoesNotCloneNoFinalize(t *testing.T) { 121 ctrl := gomock.NewController(t) 122 defer ctrl.Finish() 123 124 session := newDefaultTestSession(t).(*session) 125 w := newWriteStub() 126 var completionFn completionFn 127 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) { 128 completionFn = op.CompletionFn() 129 write, ok := op.(*writeOperation) 130 require.True(t, ok) 131 require.True(t, 132 xtest.ByteSlicesBackedBySameData( 133 w.ns.Bytes(), 134 write.namespace.Bytes())) 135 require.True(t, 136 xtest.ByteSlicesBackedBySameData( 137 w.id.Bytes(), 138 write.request.ID)) 139 }}) 140 141 require.NoError(t, session.Open()) 142 143 // Begin write 144 var resultErr error 145 var writeWg sync.WaitGroup 146 writeWg.Add(1) 147 go func() { 148 resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation) 149 writeWg.Done() 150 }() 151 152 // Callback 153 enqueueWg.Wait() 154 for i := 0; i < session.state.topoMap.Replicas(); i++ { 155 completionFn(session.state.topoMap.Hosts()[0], nil) 156 } 157 158 writeWg.Wait() 159 require.NoError(t, resultErr) 160 require.NoError(t, session.Close()) 161 } 162 163 func TestSessionWriteBadUnitErr(t *testing.T) { 164 ctrl := gomock.NewController(t) 165 defer ctrl.Finish() 166 167 session := newDefaultTestSession(t).(*session) 168 169 w := struct { 170 ns ident.ID 171 id ident.ID 172 value float64 173 t xtime.UnixNano 174 unit xtime.Unit 175 annotation []byte 176 }{ 177 ns: ident.StringID("testNs"), 178 id: ident.StringID("foo"), 179 value: 1.0, 180 t: xtime.Now(), 181 unit: xtime.Unit(byte(255)), 182 annotation: nil, 183 } 184 185 mockHostQueues(ctrl, session, sessionTestReplicas, nil) 186 187 assert.NoError(t, session.Open()) 188 189 assert.Error(t, session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation)) 190 191 assert.NoError(t, session.Close()) 192 } 193 194 func TestSessionWriteBadRequestErrorIsNonRetryable(t *testing.T) { 195 ctrl := gomock.NewController(t) 196 defer ctrl.Finish() 197 198 scope := tally.NewTestScope("", nil) 199 opts := newSessionTestOptions(). 200 SetInstrumentOptions(instrument.NewOptions().SetMetricsScope(scope)) 201 session := newTestSession(t, opts).(*session) 202 203 w := struct { 204 ns ident.ID 205 id ident.ID 206 value float64 207 t xtime.UnixNano 208 unit xtime.Unit 209 annotation []byte 210 }{ 211 ns: ident.StringID("testNs"), 212 id: ident.StringID("foo"), 213 value: 1.0, 214 t: xtime.Now(), 215 unit: xtime.Second, 216 annotation: nil, 217 } 218 219 var hosts []topology.Host 220 221 mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{ 222 func(idx int, op op) { 223 go func() { 224 op.CompletionFn()(hosts[idx], &rpc.Error{ 225 Type: rpc.ErrorType_BAD_REQUEST, 226 Message: "expected bad request error", 227 }) 228 }() 229 }, 230 }) 231 232 assert.NoError(t, session.Open()) 233 234 session.state.RLock() 235 hosts = session.state.topoMap.Hosts() 236 session.state.RUnlock() 237 238 err := session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation) 239 assert.Error(t, err) 240 assert.True(t, xerrors.IsNonRetryableError(err)) 241 242 // Assert counting bad request errors by number of nodes 243 counters := scope.Snapshot().Counters() 244 nodesBadRequestErrors, ok := counters["write.nodes-responding-error+error_type=bad_request_error,nodes=3"] 245 require.True(t, ok) 246 assert.Equal(t, int64(1), nodesBadRequestErrors.Value()) 247 248 assert.NoError(t, session.Close()) 249 } 250 251 func TestSessionWriteRetry(t *testing.T) { 252 ctrl := gomock.NewController(t) 253 defer ctrl.Finish() 254 255 scope := tally.NewTestScope("", nil) 256 opts := newSessionTestOptions(). 257 SetInstrumentOptions(instrument.NewOptions().SetMetricsScope(scope)) 258 session := newRetryEnabledTestSession(t, opts).(*session) 259 260 w := struct { 261 ns ident.ID 262 id ident.ID 263 value float64 264 t xtime.UnixNano 265 unit xtime.Unit 266 annotation []byte 267 }{ 268 ns: ident.StringID("testNs"), 269 id: ident.StringID("foo"), 270 value: 1.0, 271 t: xtime.Now(), 272 unit: xtime.Second, 273 annotation: nil, 274 } 275 276 var hosts []topology.Host 277 var completionFn completionFn 278 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{ 279 func(idx int, op op) { 280 go func() { 281 op.CompletionFn()(hosts[idx], &rpc.Error{ 282 Type: rpc.ErrorType_INTERNAL_ERROR, 283 Message: "random internal issue", 284 }) 285 }() 286 }, 287 func(idx int, op op) { 288 write, ok := op.(*writeOperation) 289 assert.True(t, ok) 290 assert.Equal(t, w.id.String(), string(write.request.ID)) 291 assert.Equal(t, w.value, write.request.Datapoint.Value) 292 assert.Equal(t, w.t.Seconds(), write.request.Datapoint.Timestamp) 293 assert.Equal(t, rpc.TimeType_UNIX_SECONDS, write.request.Datapoint.TimestampTimeType) 294 assert.NotNil(t, write.completionFn) 295 completionFn = write.completionFn 296 }, 297 }) 298 299 assert.NoError(t, session.Open()) 300 301 session.state.RLock() 302 hosts = session.state.topoMap.Hosts() 303 session.state.RUnlock() 304 305 // Begin write 306 var resultErr error 307 var writeWg sync.WaitGroup 308 writeWg.Add(1) 309 go func() { 310 resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation) 311 writeWg.Done() 312 }() 313 314 // Callback 315 enqueueWg.Wait() 316 for i := 0; i < session.state.topoMap.Replicas(); i++ { 317 completionFn(session.state.topoMap.Hosts()[0], nil) 318 } 319 320 // Wait for write to complete 321 writeWg.Wait() 322 assert.Nil(t, resultErr) 323 324 // Assert counting bad request errors by number of nodes 325 counters := scope.Snapshot().Counters() 326 nodesBadRequestErrors, ok := counters["write.nodes-responding-error+error_type=server_error,nodes=3"] 327 require.True(t, ok) 328 assert.Equal(t, int64(1), nodesBadRequestErrors.Value()) 329 330 assert.NoError(t, session.Close()) 331 } 332 333 func TestSessionWriteConsistencyLevelAll(t *testing.T) { 334 ctrl := gomock.NewController(t) 335 defer ctrl.Finish() 336 337 level := topology.ConsistencyLevelAll 338 testWriteConsistencyLevel(t, ctrl, level, 3, 0, outcomeSuccess) 339 for i := 1; i <= 3; i++ { 340 testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeFail) 341 } 342 } 343 344 func TestSessionWriteConsistencyLevelMajority(t *testing.T) { 345 ctrl := gomock.NewController(t) 346 defer ctrl.Finish() 347 348 level := topology.ConsistencyLevelMajority 349 for i := 0; i <= 1; i++ { 350 testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeSuccess) 351 testWriteConsistencyLevel(t, ctrl, level, 3-i, 0, outcomeSuccess) 352 } 353 for i := 2; i <= 3; i++ { 354 testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeFail) 355 } 356 } 357 358 func TestSessionWriteConsistencyLevelOne(t *testing.T) { 359 ctrl := gomock.NewController(t) 360 defer ctrl.Finish() 361 362 level := topology.ConsistencyLevelOne 363 for i := 0; i <= 2; i++ { 364 testWriteConsistencyLevel(t, ctrl, level, 3-i, i, outcomeSuccess) 365 testWriteConsistencyLevel(t, ctrl, level, 3-i, 0, outcomeSuccess) 366 } 367 testWriteConsistencyLevel(t, ctrl, level, 0, 3, outcomeFail) 368 } 369 370 func testWriteConsistencyLevel( 371 t *testing.T, 372 ctrl *gomock.Controller, 373 level topology.ConsistencyLevel, 374 success, failures int, 375 expected outcome, 376 ) { 377 opts := newSessionTestOptions() 378 opts = opts.SetWriteConsistencyLevel(level) 379 380 reporterOpts := xmetrics.NewTestStatsReporterOptions(). 381 SetCaptureEvents(true) 382 reporter := xmetrics.NewTestStatsReporter(reporterOpts) 383 scope, closer := tally.NewRootScope(tally.ScopeOptions{Reporter: reporter}, time.Millisecond) 384 385 defer closer.Close() 386 387 opts = opts.SetInstrumentOptions(opts.InstrumentOptions(). 388 SetMetricsScope(scope)) 389 390 session := newTestSession(t, opts).(*session) 391 392 w := struct { 393 ns ident.ID 394 id ident.ID 395 value float64 396 t xtime.UnixNano 397 unit xtime.Unit 398 annotation []byte 399 }{ 400 ns: ident.StringID("testNs"), 401 id: ident.StringID("foo"), 402 value: 1.0, 403 t: xtime.Now(), 404 unit: xtime.Second, 405 annotation: nil, 406 } 407 408 var completionFn completionFn 409 enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{func(idx int, op op) { 410 completionFn = op.CompletionFn() 411 }}) 412 413 assert.NoError(t, session.Open()) 414 415 // Begin write 416 var resultErr error 417 var writeWg sync.WaitGroup 418 writeWg.Add(1) 419 go func() { 420 resultErr = session.Write(w.ns, w.id, w.t, w.value, w.unit, w.annotation) 421 writeWg.Done() 422 }() 423 424 // Callback 425 enqueueWg.Wait() 426 host := session.state.topoMap.Hosts()[0] // any host 427 writeErr := "a specific write error" 428 for i := 0; i < success; i++ { 429 completionFn(host, nil) 430 } 431 for i := 0; i < failures; i++ { 432 completionFn(host, fmt.Errorf(writeErr)) 433 } 434 435 // Wait for write to complete or timeout 436 doneCh := make(chan struct{}) 437 go func() { 438 writeWg.Wait() 439 close(doneCh) 440 }() 441 442 // NB(bl): Check whether we're correctly signaling in 443 // write_state.completionFn. If not, the write won't complete. 444 select { 445 case <-time.After(time.Second): 446 require.NoError(t, errors.New("session write failed to signal")) 447 case <-doneCh: 448 // continue 449 } 450 451 switch expected { 452 case outcomeSuccess: 453 assert.NoError(t, resultErr) 454 case outcomeFail: 455 assert.Error(t, resultErr) 456 457 resultErrStr := fmt.Sprintf("%v", resultErr) 458 assert.True(t, strings.Contains(resultErrStr, 459 fmt.Sprintf("failed to meet consistency level %s", level.String()))) 460 assert.True(t, strings.Contains(resultErrStr, 461 writeErr)) 462 } 463 464 assert.NoError(t, session.Close()) 465 466 counters := reporter.Counters() 467 for counters["write.success"] == 0 && counters["write.errors"] == 0 { 468 time.Sleep(time.Millisecond) 469 counters = reporter.Counters() 470 } 471 if expected == outcomeSuccess { 472 assert.Equal(t, 1, int(counters["write.success"])) 473 assert.Equal(t, 0, int(counters["write.errors"])) 474 } else { 475 assert.Equal(t, 0, int(counters["write.success"])) 476 assert.Equal(t, 1, int(counters["write.errors"])) 477 } 478 if failures > 0 { 479 for _, event := range reporter.Events() { 480 if event.Name() == "write.nodes-responding-error" { 481 nodesFailing, convErr := strconv.Atoi(event.Tags()["nodes"]) 482 require.NoError(t, convErr) 483 assert.True(t, 0 < nodesFailing && nodesFailing <= failures) 484 assert.Equal(t, int64(1), event.Value()) 485 break 486 } 487 } 488 } 489 } 490 491 type writeStub struct { 492 ns ident.ID 493 id ident.ID 494 value float64 495 t xtime.UnixNano 496 unit xtime.Unit 497 annotation []byte 498 } 499 500 func newTestSession(t *testing.T, opts Options) clientSession { 501 s, err := newSession(opts) 502 assert.NoError(t, err) 503 return s 504 } 505 506 func newDefaultTestSession(t *testing.T) clientSession { 507 return newTestSession(t, newSessionTestOptions()) 508 } 509 510 func newRetryEnabledTestSession(t *testing.T, opts Options) clientSession { 511 opts = opts. 512 SetWriteRetrier( 513 xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1))) 514 return newTestSession(t, opts) 515 } 516 517 func newWriteStub() writeStub { 518 return writeStub{ 519 ns: ident.StringID("testNs"), 520 id: ident.StringID("foo"), 521 value: 1.0, 522 t: xtime.Now(), 523 unit: xtime.Second, 524 annotation: nil} 525 }