github.com/weaviate/weaviate@v1.24.6/adapters/clients/cluster_schema_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 clients 13 14 import ( 15 "context" 16 "encoding/json" 17 "io" 18 "net/http" 19 "net/http/httptest" 20 "net/url" 21 "testing" 22 "time" 23 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 "github.com/weaviate/weaviate/usecases/cluster" 27 ) 28 29 func TestOpenTransactionNoReturnPayload(t *testing.T) { 30 // The No-Return-Payload is the situation that existed prior to v1.17 where 31 // the only option for transactions was to broadcast updates. By keeping this 32 // test around, we can make sure that we are not breaking backward 33 // compatibility. 34 35 handler := func(w http.ResponseWriter, r *http.Request) { 36 defer r.Body.Close() 37 body, err := io.ReadAll(r.Body) 38 require.Nil(t, err) 39 40 var pl txPayload 41 err = json.Unmarshal(body, &pl) 42 require.Nil(t, err) 43 44 assert.Equal(t, "mamma-mia-paylodia-belissima", pl.Payload.(string)) 45 w.WriteHeader(http.StatusCreated) 46 } 47 server := httptest.NewServer(http.HandlerFunc(handler)) 48 defer server.Close() 49 50 u, _ := url.Parse(server.URL) 51 52 c := NewClusterSchema(&http.Client{}) 53 54 tx := &cluster.Transaction{ 55 ID: "12345", 56 Type: "the best", 57 Payload: "mamma-mia-paylodia-belissima", 58 } 59 60 err := c.OpenTransaction(context.Background(), u.Host, tx) 61 assert.Nil(t, err) 62 } 63 64 func TestOpenTransactionWithReturnPayload(t *testing.T) { 65 // Newly added as part of v1.17 where read-transactions were introduced which 66 // are used to sync up cluster schema state when nodes join 67 handler := func(w http.ResponseWriter, r *http.Request) { 68 resTx := txPayload{ 69 Type: "my-tx", 70 ID: "987", 71 Payload: "gracie-mille-per-payload", 72 } 73 resBody, err := json.Marshal(resTx) 74 require.Nil(t, err) 75 76 w.WriteHeader(http.StatusCreated) 77 w.Write(resBody) 78 } 79 server := httptest.NewServer(http.HandlerFunc(handler)) 80 defer server.Close() 81 82 u, _ := url.Parse(server.URL) 83 84 c := NewClusterSchema(&http.Client{}) 85 86 txIn := &cluster.Transaction{ 87 ID: "987", 88 Type: "my-tx", 89 } 90 91 err := c.OpenTransaction(context.Background(), u.Host, txIn) 92 assert.Nil(t, err) 93 94 expectedTxOut := &cluster.Transaction{ 95 ID: "987", 96 Type: "my-tx", 97 Payload: json.RawMessage("\"gracie-mille-per-payload\""), 98 } 99 100 assert.Equal(t, expectedTxOut, txIn) 101 } 102 103 func TestOpenTransactionWithTTL(t *testing.T) { 104 deadline, err := time.Parse(time.RFC3339Nano, "2040-01-02T15:04:05.00Z") 105 require.Nil(t, err) 106 107 handler := func(w http.ResponseWriter, r *http.Request) { 108 defer r.Body.Close() 109 body, err := io.ReadAll(r.Body) 110 require.Nil(t, err) 111 112 var pl txPayload 113 err = json.Unmarshal(body, &pl) 114 require.Nil(t, err) 115 116 parsedDL := time.UnixMilli(pl.DeadlineMilli) 117 assert.Equal(t, deadline.UnixNano(), parsedDL.UnixNano()) 118 w.WriteHeader(http.StatusCreated) 119 } 120 server := httptest.NewServer(http.HandlerFunc(handler)) 121 defer server.Close() 122 123 u, _ := url.Parse(server.URL) 124 125 c := NewClusterSchema(&http.Client{}) 126 127 tx := &cluster.Transaction{ 128 ID: "12345", 129 Type: "the best", 130 Payload: "mamma-mia-paylodia-belissima", 131 Deadline: deadline, 132 } 133 134 err = c.OpenTransaction(context.Background(), u.Host, tx) 135 assert.Nil(t, err) 136 } 137 138 func TestOpenTransactionUnhappyPaths(t *testing.T) { 139 type test struct { 140 name string 141 handler http.HandlerFunc 142 expectedErr error 143 expectedErrContains string 144 ctx context.Context 145 shutdownPrematurely bool 146 } 147 148 expiredCtx, cancel := context.WithCancel(context.Background()) 149 cancel() 150 151 tests := []test{ 152 { 153 name: "concurrent transaction", 154 handler: func(w http.ResponseWriter, r *http.Request) { 155 defer r.Body.Close() 156 w.WriteHeader(http.StatusConflict) 157 }, 158 expectedErr: cluster.ErrConcurrentTransaction, 159 }, 160 { 161 name: "arbitrary 500", 162 handler: func(w http.ResponseWriter, r *http.Request) { 163 defer r.Body.Close() 164 w.WriteHeader(http.StatusInternalServerError) 165 w.Write([]byte("nope!")) 166 }, 167 expectedErrContains: "nope!", 168 }, 169 { 170 name: "invalid json", 171 handler: func(w http.ResponseWriter, r *http.Request) { 172 defer r.Body.Close() 173 w.WriteHeader(http.StatusCreated) 174 w.Write([]byte("<<<!@#*)!@#****@!''")) 175 }, 176 expectedErrContains: "error unmarshalling", 177 }, 178 { 179 name: "expired ctx", 180 ctx: expiredCtx, 181 handler: func(w http.ResponseWriter, r *http.Request) { 182 }, 183 expectedErrContains: "context", 184 }, 185 { 186 name: "remote server shut down", 187 shutdownPrematurely: true, 188 handler: func(w http.ResponseWriter, r *http.Request) { 189 }, 190 expectedErrContains: "refused", 191 }, 192 { 193 name: "tx id mismatch", 194 handler: func(w http.ResponseWriter, r *http.Request) { 195 resTx := txPayload{ 196 Type: "wrong-tx-id", 197 ID: "987", 198 Payload: "gracie-mille-per-payload", 199 } 200 resBody, err := json.Marshal(resTx) 201 require.Nil(t, err) 202 203 w.WriteHeader(http.StatusCreated) 204 w.Write(resBody) 205 }, 206 expectedErrContains: "mismatch between outgoing and incoming tx ids", 207 }, 208 } 209 210 for _, test := range tests { 211 t.Run(test.name, func(t *testing.T) { 212 server := httptest.NewServer(http.HandlerFunc(test.handler)) 213 if test.shutdownPrematurely { 214 server.Close() 215 } else { 216 defer server.Close() 217 } 218 219 u, _ := url.Parse(server.URL) 220 221 c := NewClusterSchema(&http.Client{}) 222 223 tx := &cluster.Transaction{ 224 ID: "12345", 225 Type: "the best", 226 Payload: "mamma-mia-paylodia-belissima", 227 } 228 229 if test.ctx == nil { 230 test.ctx = context.Background() 231 } 232 233 err := c.OpenTransaction(test.ctx, u.Host, tx) 234 assert.NotNil(t, err) 235 236 if test.expectedErr != nil { 237 assert.Equal(t, test.expectedErr, err) 238 } 239 240 if test.expectedErrContains != "" { 241 assert.Contains(t, err.Error(), test.expectedErrContains) 242 } 243 }) 244 } 245 } 246 247 func TestAbortTransaction(t *testing.T) { 248 handler := func(w http.ResponseWriter, r *http.Request) { 249 defer r.Body.Close() 250 w.WriteHeader(http.StatusNoContent) 251 } 252 server := httptest.NewServer(http.HandlerFunc(handler)) 253 defer server.Close() 254 255 u, _ := url.Parse(server.URL) 256 257 c := NewClusterSchema(&http.Client{}) 258 259 tx := &cluster.Transaction{ 260 ID: "am-i-going-to-be-cancelled", 261 Type: "the worst", 262 Payload: "", 263 } 264 265 err := c.AbortTransaction(context.Background(), u.Host, tx) 266 assert.Nil(t, err) 267 } 268 269 func TestAbortTransactionUnhappyPaths(t *testing.T) { 270 type test struct { 271 name string 272 handler http.HandlerFunc 273 expectedErr error 274 expectedErrContains string 275 ctx context.Context 276 shutdownPrematurely bool 277 } 278 279 expiredCtx, cancel := context.WithCancel(context.Background()) 280 cancel() 281 282 tests := []test{ 283 { 284 name: "arbitrary 500", 285 handler: func(w http.ResponseWriter, r *http.Request) { 286 defer r.Body.Close() 287 w.WriteHeader(http.StatusInternalServerError) 288 w.Write([]byte("nope!")) 289 }, 290 expectedErrContains: "nope!", 291 }, 292 { 293 name: "expired ctx", 294 ctx: expiredCtx, 295 handler: func(w http.ResponseWriter, r *http.Request) { 296 }, 297 expectedErrContains: "context", 298 }, 299 { 300 name: "remote server shut down", 301 shutdownPrematurely: true, 302 handler: func(w http.ResponseWriter, r *http.Request) { 303 }, 304 expectedErrContains: "refused", 305 }, 306 } 307 308 for _, test := range tests { 309 t.Run(test.name, func(t *testing.T) { 310 server := httptest.NewServer(http.HandlerFunc(test.handler)) 311 if test.shutdownPrematurely { 312 server.Close() 313 } else { 314 defer server.Close() 315 } 316 317 u, _ := url.Parse(server.URL) 318 319 c := NewClusterSchema(&http.Client{}) 320 321 tx := &cluster.Transaction{ 322 ID: "12345", 323 Type: "the best", 324 Payload: "mamma-mia-paylodia-belissima", 325 } 326 327 if test.ctx == nil { 328 test.ctx = context.Background() 329 } 330 331 err := c.AbortTransaction(test.ctx, u.Host, tx) 332 assert.NotNil(t, err) 333 334 if test.expectedErr != nil { 335 assert.Equal(t, test.expectedErr, err) 336 } 337 338 if test.expectedErrContains != "" { 339 assert.Contains(t, err.Error(), test.expectedErrContains) 340 } 341 }) 342 } 343 } 344 345 func TestCommitTransaction(t *testing.T) { 346 handler := func(w http.ResponseWriter, r *http.Request) { 347 defer r.Body.Close() 348 w.WriteHeader(http.StatusNoContent) 349 } 350 server := httptest.NewServer(http.HandlerFunc(handler)) 351 defer server.Close() 352 353 u, _ := url.Parse(server.URL) 354 355 c := NewClusterSchema(&http.Client{}) 356 357 tx := &cluster.Transaction{ 358 ID: "am-i-going-to-be-cancelled", 359 Type: "the worst", 360 Payload: "", 361 } 362 363 err := c.CommitTransaction(context.Background(), u.Host, tx) 364 assert.Nil(t, err) 365 } 366 367 func TestCommitTransactionUnhappyPaths(t *testing.T) { 368 type test struct { 369 name string 370 handler http.HandlerFunc 371 expectedErr error 372 expectedErrContains string 373 ctx context.Context 374 shutdownPrematurely bool 375 } 376 377 expiredCtx, cancel := context.WithCancel(context.Background()) 378 cancel() 379 380 tests := []test{ 381 { 382 name: "arbitrary 500", 383 handler: func(w http.ResponseWriter, r *http.Request) { 384 defer r.Body.Close() 385 w.WriteHeader(http.StatusInternalServerError) 386 w.Write([]byte("nope!")) 387 }, 388 expectedErrContains: "nope!", 389 }, 390 { 391 name: "expired ctx", 392 ctx: expiredCtx, 393 handler: func(w http.ResponseWriter, r *http.Request) { 394 }, 395 expectedErrContains: "context", 396 }, 397 { 398 name: "remote server shut down", 399 shutdownPrematurely: true, 400 handler: func(w http.ResponseWriter, r *http.Request) { 401 }, 402 expectedErrContains: "refused", 403 }, 404 } 405 406 for _, test := range tests { 407 t.Run(test.name, func(t *testing.T) { 408 server := httptest.NewServer(http.HandlerFunc(test.handler)) 409 if test.shutdownPrematurely { 410 server.Close() 411 } else { 412 defer server.Close() 413 } 414 415 u, _ := url.Parse(server.URL) 416 417 c := NewClusterSchema(&http.Client{}) 418 419 tx := &cluster.Transaction{ 420 ID: "12345", 421 Type: "the best", 422 Payload: "mamma-mia-paylodia-belissima", 423 } 424 425 if test.ctx == nil { 426 test.ctx = context.Background() 427 } 428 429 err := c.CommitTransaction(test.ctx, u.Host, tx) 430 assert.NotNil(t, err) 431 432 if test.expectedErr != nil { 433 assert.Equal(t, test.expectedErr, err) 434 } 435 436 if test.expectedErrContains != "" { 437 assert.Contains(t, err.Error(), test.expectedErrContains) 438 } 439 }) 440 } 441 }