github.com/weaviate/weaviate@v1.24.6/test/acceptance/replication/crud_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 replication
    13  
    14  import (
    15  	"context"
    16  	"encoding/json"
    17  	"fmt"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/go-openapi/strfmt"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  	"github.com/weaviate/weaviate/client/objects"
    25  	"github.com/weaviate/weaviate/entities/models"
    26  	"github.com/weaviate/weaviate/entities/schema/crossref"
    27  	"github.com/weaviate/weaviate/test/docker"
    28  	"github.com/weaviate/weaviate/test/helper"
    29  	"github.com/weaviate/weaviate/test/helper/sample-schema/articles"
    30  	"github.com/weaviate/weaviate/usecases/replica"
    31  )
    32  
    33  var (
    34  	paragraphIDs = []strfmt.UUID{
    35  		strfmt.UUID("3bf331ac-8c86-4f95-b127-2f8f96bbc093"),
    36  		strfmt.UUID("47b26ba1-6bc9-41f8-a655-8b9a5b60e1a3"),
    37  		strfmt.UUID("5fef6289-28d2-4ea2-82a9-48eb501200cd"),
    38  		strfmt.UUID("34a673b4-8859-4cb4-bb30-27f5622b47e9"),
    39  		strfmt.UUID("9fa362f5-c2dc-4fb8-b5b2-11701adc5f75"),
    40  		strfmt.UUID("63735238-6723-4caf-9eaa-113120968ff4"),
    41  		strfmt.UUID("2236744d-b2d2-40e5-95d8-2574f20a7126"),
    42  		strfmt.UUID("1a54e25d-aaf9-48d2-bc3c-bef00b556297"),
    43  		strfmt.UUID("0b8a0e70-a240-44b2-ac6d-26dda97523b9"),
    44  		strfmt.UUID("50566856-5d0a-4fb1-a390-e099bc236f66"),
    45  	}
    46  
    47  	articleIDs = []strfmt.UUID{
    48  		strfmt.UUID("aeaf8743-5a8f-4149-b960-444181d3131a"),
    49  		strfmt.UUID("2a1e9834-064e-4ca8-9efc-35707c6bae6d"),
    50  		strfmt.UUID("8d101c0c-4deb-48d0-805c-d9c691042a1a"),
    51  		strfmt.UUID("b9715fec-ef6c-4e8d-a89e-55e2eebee3f6"),
    52  		strfmt.UUID("faf520f2-f6c3-4cdf-9c16-0348ffd0f8ac"),
    53  		strfmt.UUID("d4c695dd-4dc7-4e49-bc73-089ef5f90fc8"),
    54  		strfmt.UUID("c7949324-e07f-4ffc-8be0-194f0470d375"),
    55  		strfmt.UUID("9c112e01-7759-43ed-a6e8-5defb267c8ee"),
    56  		strfmt.UUID("9bf847f3-3a1a-45a5-b656-311163e536b5"),
    57  		strfmt.UUID("c1975388-d67c-404a-ae77-5983fbaea4bb"),
    58  	}
    59  )
    60  
    61  func immediateReplicaCRUD(t *testing.T) {
    62  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    63  	defer cancel()
    64  
    65  	compose, err := docker.New().
    66  		WithWeaviateCluster().
    67  		WithText2VecContextionary().
    68  		Start(ctx)
    69  	require.Nil(t, err)
    70  	defer func() {
    71  		if err := compose.Terminate(ctx); err != nil {
    72  			t.Fatalf("failed to terminate test containers: %s", err.Error())
    73  		}
    74  	}()
    75  
    76  	helper.SetupClient(compose.GetWeaviate().URI())
    77  	paragraphClass := articles.ParagraphsClass()
    78  	articleClass := articles.ArticlesClass()
    79  
    80  	t.Run("create schema", func(t *testing.T) {
    81  		paragraphClass.ReplicationConfig = &models.ReplicationConfig{
    82  			Factor: 2,
    83  		}
    84  		helper.CreateClass(t, paragraphClass)
    85  		articleClass.ReplicationConfig = &models.ReplicationConfig{
    86  			Factor: 2,
    87  		}
    88  		helper.CreateClass(t, articleClass)
    89  	})
    90  
    91  	t.Run("insert paragraphs batch", func(t *testing.T) {
    92  		t.Run("create objects on node 1", func(t *testing.T) {
    93  			batch := make([]*models.Object, len(paragraphIDs))
    94  			for i, id := range paragraphIDs {
    95  				batch[i] = articles.NewParagraph().
    96  					WithID(id).
    97  					WithContents(fmt.Sprintf("paragraph#%d", i)).
    98  					Object()
    99  			}
   100  			createObjects(t, compose.GetWeaviate().URI(), batch)
   101  		})
   102  
   103  		t.Run("stop node 1", func(t *testing.T) {
   104  			stopNode(ctx, t, compose, compose.GetWeaviate().Name())
   105  		})
   106  
   107  		t.Run("assert objects exist on node 2", func(t *testing.T) {
   108  			resp := gqlGet(t, compose.GetWeaviateNode2().URI(), "Paragraph", replica.One)
   109  			assert.Len(t, resp, len(paragraphIDs))
   110  		})
   111  
   112  		t.Run("restart node 1", func(t *testing.T) {
   113  			restartNode1(ctx, t, compose)
   114  		})
   115  	})
   116  
   117  	t.Run("insert articles individually", func(t *testing.T) {
   118  		t.Run("create objects on node 2", func(t *testing.T) {
   119  			for i, id := range articleIDs {
   120  				obj := articles.NewArticle().
   121  					WithID(id).
   122  					WithTitle(fmt.Sprintf("Article#%d", i)).
   123  					Object()
   124  				createObject(t, compose.GetWeaviateNode2().URI(), obj)
   125  			}
   126  		})
   127  
   128  		t.Run("stop node 2", func(t *testing.T) {
   129  			stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   130  		})
   131  
   132  		t.Run("assert objects exist on node 1", func(t *testing.T) {
   133  			resp := gqlGet(t, compose.GetWeaviate().URI(), "Article", replica.One)
   134  			assert.Len(t, resp, len(articleIDs))
   135  		})
   136  
   137  		t.Run("restart node 2", func(t *testing.T) {
   138  			err = compose.Start(ctx, compose.GetWeaviateNode2().Name())
   139  			require.Nil(t, err)
   140  		})
   141  	})
   142  
   143  	t.Run("add references", func(t *testing.T) {
   144  		refs := make([]*models.BatchReference, len(articleIDs))
   145  		for i := range articleIDs {
   146  			refs[i] = &models.BatchReference{
   147  				From: strfmt.URI(crossref.NewSource("Article", "hasParagraphs", articleIDs[i]).String()),
   148  				To:   strfmt.URI(crossref.NewLocalhost("Paragraph", paragraphIDs[i]).String()),
   149  			}
   150  		}
   151  
   152  		t.Run("add references to node 1", func(t *testing.T) {
   153  			addReferences(t, compose.GetWeaviate().URI(), refs)
   154  		})
   155  
   156  		t.Run("stop node 1", func(t *testing.T) {
   157  			stopNode(ctx, t, compose, compose.GetWeaviate().Name())
   158  		})
   159  
   160  		t.Run("assert references were added successfully to node 2", func(t *testing.T) {
   161  			type additional struct {
   162  				ID strfmt.UUID `json:"id"`
   163  			}
   164  
   165  			type article struct {
   166  				Additional    additional `json:"_additional"`
   167  				HasParagraphs []struct {
   168  					Additional additional `json:"_additional"`
   169  				} `json:"hasParagraphs"`
   170  			}
   171  
   172  			// maps article id to referenced paragraph id
   173  			refPairs := make(map[strfmt.UUID]strfmt.UUID)
   174  			resp := gqlGet(t, compose.GetWeaviateNode2().URI(), "Article", replica.One,
   175  				"_additional{id}", "hasParagraphs {... on Paragraph {_additional{id}}}")
   176  			assert.Len(t, resp, len(articleIDs))
   177  
   178  			for _, r := range resp {
   179  				b, err := json.Marshal(r)
   180  				require.Nil(t, err)
   181  				var art article
   182  				err = json.Unmarshal(b, &art)
   183  				require.Nil(t, err)
   184  				require.Len(t, art.HasParagraphs, 1)
   185  				refPairs[art.Additional.ID] = art.HasParagraphs[0].Additional.ID
   186  			}
   187  
   188  			for i := range articleIDs {
   189  				paragraphID, ok := refPairs[articleIDs[i]]
   190  				require.True(t, ok, "expected %q to be in refPairs: %+v", articleIDs[i], refPairs)
   191  				assert.Equal(t, paragraphIDs[i], paragraphID)
   192  			}
   193  		})
   194  
   195  		t.Run("restart node 1", func(t *testing.T) {
   196  			restartNode1(ctx, t, compose)
   197  		})
   198  	})
   199  
   200  	t.Run("patch an object", func(t *testing.T) {
   201  		before, err := getObject(t, compose.GetWeaviate().URI(), "Article", articleIDs[0], false)
   202  		require.Nil(t, err)
   203  		newTitle := "Article#9000"
   204  
   205  		t.Run("execute object patch on node 2", func(t *testing.T) {
   206  			patch := &models.Object{
   207  				ID:         before.ID,
   208  				Class:      "Article",
   209  				Properties: map[string]interface{}{"title": newTitle},
   210  			}
   211  			patchObject(t, compose.GetWeaviateNode2().URI(), patch)
   212  		})
   213  
   214  		t.Run("stop node 2", func(t *testing.T) {
   215  			stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   216  		})
   217  
   218  		t.Run("assert object is patched on node 1", func(t *testing.T) {
   219  			after, err := getObjectFromNode(t, compose.GetWeaviate().URI(), "Article", articleIDs[0], "node1")
   220  			require.Nil(t, err)
   221  
   222  			newVal, ok := after.Properties.(map[string]interface{})["title"]
   223  			require.True(t, ok)
   224  			assert.Equal(t, newTitle, newVal)
   225  		})
   226  
   227  		t.Run("restart node 2", func(t *testing.T) {
   228  			err = compose.Start(ctx, compose.GetWeaviateNode2().Name())
   229  			require.Nil(t, err)
   230  		})
   231  	})
   232  
   233  	t.Run("delete an object", func(t *testing.T) {
   234  		t.Run("execute delete object on node 1", func(t *testing.T) {
   235  			deleteObject(t, compose.GetWeaviate().URI(), "Article", articleIDs[0])
   236  		})
   237  
   238  		t.Run("stop node 1", func(t *testing.T) {
   239  			stopNode(ctx, t, compose, compose.GetWeaviate().Name())
   240  		})
   241  
   242  		t.Run("assert object removed from node 2", func(t *testing.T) {
   243  			_, err := getObjectFromNode(t, compose.GetWeaviateNode2().URI(), "Article", articleIDs[0], "node2")
   244  			assert.Equal(t, &objects.ObjectsClassGetNotFound{}, err)
   245  		})
   246  
   247  		t.Run("restart node 1", func(t *testing.T) {
   248  			restartNode1(ctx, t, compose)
   249  		})
   250  	})
   251  
   252  	t.Run("batch delete all objects", func(t *testing.T) {
   253  		t.Run("execute batch delete on node 2", func(t *testing.T) {
   254  			deleteObjects(t, compose.GetWeaviateNode2().URI(),
   255  				"Article", []string{"title"}, "Article#*")
   256  		})
   257  
   258  		t.Run("stop node 2", func(t *testing.T) {
   259  			stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   260  		})
   261  
   262  		t.Run("assert objects are removed from node 1", func(t *testing.T) {
   263  			resp := gqlGet(t, compose.GetWeaviate().URI(), "Article", replica.One)
   264  			assert.Empty(t, resp)
   265  		})
   266  
   267  		t.Run("restart node 2", func(t *testing.T) {
   268  			err = compose.Start(ctx, compose.GetWeaviateNode2().Name())
   269  			require.Nil(t, err)
   270  		})
   271  	})
   272  }
   273  
   274  func eventualReplicaCRUD(t *testing.T) {
   275  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
   276  	defer cancel()
   277  
   278  	compose, err := docker.New().
   279  		WithWeaviateCluster().
   280  		WithText2VecContextionary().
   281  		Start(ctx)
   282  	require.Nil(t, err)
   283  	defer func() {
   284  		if err := compose.Terminate(ctx); err != nil {
   285  			t.Fatalf("failed to terminate test containers: %s", err.Error())
   286  		}
   287  	}()
   288  
   289  	helper.SetupClient(compose.GetWeaviate().URI())
   290  	paragraphClass := articles.ParagraphsClass()
   291  	articleClass := articles.ArticlesClass()
   292  
   293  	t.Run("create schema on node 1", func(t *testing.T) {
   294  		paragraphClass.ShardingConfig = map[string]interface{}{"desiredCount": 1}
   295  		helper.CreateClass(t, paragraphClass)
   296  		articleClass.ShardingConfig = map[string]interface{}{"desiredCount": 1}
   297  		helper.CreateClass(t, articleClass)
   298  	})
   299  
   300  	t.Run("insert paragraphs batch on node 1", func(t *testing.T) {
   301  		batch := make([]*models.Object, len(paragraphIDs))
   302  		for i, id := range paragraphIDs {
   303  			batch[i] = articles.NewParagraph().
   304  				WithID(id).
   305  				WithContents(fmt.Sprintf("paragraph#%d", i)).
   306  				Object()
   307  		}
   308  		createObjects(t, compose.GetWeaviate().URI(), batch)
   309  	})
   310  
   311  	t.Run("insert articles batch on node 1", func(t *testing.T) {
   312  		batch := make([]*models.Object, len(articleIDs))
   313  		for i, id := range articleIDs {
   314  			batch[i] = articles.NewArticle().
   315  				WithID(id).
   316  				WithTitle(fmt.Sprintf("Article#%d", i)).
   317  				Object()
   318  		}
   319  		createObjects(t, compose.GetWeaviate().URI(), batch)
   320  	})
   321  
   322  	t.Run("configure classes to replicate to node 2", func(t *testing.T) {
   323  		ac := helper.GetClass(t, "Article")
   324  		ac.ReplicationConfig = &models.ReplicationConfig{
   325  			Factor: 2,
   326  		}
   327  		helper.UpdateClass(t, ac)
   328  
   329  		pc := helper.GetClass(t, "Paragraph")
   330  		pc.ReplicationConfig = &models.ReplicationConfig{
   331  			Factor: 2,
   332  		}
   333  		helper.UpdateClass(t, pc)
   334  	})
   335  
   336  	t.Run("stop node 1", func(t *testing.T) {
   337  		stopNode(ctx, t, compose, compose.GetWeaviate().Name())
   338  	})
   339  
   340  	t.Run("assert all previous data replicated to node 2", func(t *testing.T) {
   341  		resp := gqlGet(t, compose.GetWeaviateNode2().URI(), "Article", replica.One)
   342  		assert.Len(t, resp, len(articleIDs))
   343  		resp = gqlGet(t, compose.GetWeaviateNode2().URI(), "Paragraph", replica.One)
   344  		assert.Len(t, resp, len(paragraphIDs))
   345  	})
   346  
   347  	t.Run("restart node 1", func(t *testing.T) {
   348  		restartNode1(ctx, t, compose)
   349  	})
   350  
   351  	t.Run("assert any future writes are replicated", func(t *testing.T) {
   352  		t.Run("patch an object", func(t *testing.T) {
   353  			before, err := getObject(t, compose.GetWeaviate().URI(), "Article", articleIDs[0], false)
   354  			require.Nil(t, err)
   355  			newTitle := "Article#9000"
   356  
   357  			t.Run("execute object patch on node 2", func(t *testing.T) {
   358  				patch := &models.Object{
   359  					ID:         before.ID,
   360  					Class:      "Article",
   361  					Properties: map[string]interface{}{"title": newTitle},
   362  				}
   363  				patchObject(t, compose.GetWeaviateNode2().URI(), patch)
   364  			})
   365  
   366  			t.Run("stop node 2", func(t *testing.T) {
   367  				stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   368  			})
   369  
   370  			t.Run("assert object is patched on node 1", func(t *testing.T) {
   371  				after, err := getObjectFromNode(t, compose.GetWeaviate().URI(), "Article", articleIDs[0], "node1")
   372  				require.Nil(t, err)
   373  
   374  				newVal, ok := after.Properties.(map[string]interface{})["title"]
   375  				require.True(t, ok)
   376  				assert.Equal(t, newTitle, newVal)
   377  			})
   378  
   379  			t.Run("restart node 2", func(t *testing.T) {
   380  				err = compose.Start(ctx, compose.GetWeaviateNode2().Name())
   381  				require.Nil(t, err)
   382  			})
   383  		})
   384  
   385  		t.Run("delete an object", func(t *testing.T) {
   386  			t.Run("execute delete object on node 1", func(t *testing.T) {
   387  				deleteObject(t, compose.GetWeaviate().URI(), "Article", articleIDs[0])
   388  			})
   389  
   390  			t.Run("stop node 1", func(t *testing.T) {
   391  				stopNode(ctx, t, compose, compose.GetWeaviate().Name())
   392  			})
   393  
   394  			t.Run("assert object removed from node 2", func(t *testing.T) {
   395  				_, err := getObjectFromNode(t, compose.GetWeaviateNode2().URI(), "Article", articleIDs[0], "node2")
   396  				assert.Equal(t, &objects.ObjectsClassGetNotFound{}, err)
   397  			})
   398  
   399  			t.Run("restart node 1", func(t *testing.T) {
   400  				restartNode1(ctx, t, compose)
   401  			})
   402  		})
   403  
   404  		t.Run("batch delete all objects", func(t *testing.T) {
   405  			t.Run("execute batch delete on node 2", func(t *testing.T) {
   406  				deleteObjects(t, compose.GetWeaviateNode2().URI(),
   407  					"Article", []string{"title"}, "Article#*")
   408  			})
   409  
   410  			t.Run("stop node 2", func(t *testing.T) {
   411  				stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   412  			})
   413  
   414  			t.Run("assert objects are removed from node 1", func(t *testing.T) {
   415  				resp := gqlGet(t, compose.GetWeaviate().URI(), "Article", replica.One)
   416  				assert.Empty(t, resp)
   417  			})
   418  
   419  			t.Run("restart node 2", func(t *testing.T) {
   420  				err = compose.Start(ctx, compose.GetWeaviateNode2().Name())
   421  				require.Nil(t, err)
   422  			})
   423  		})
   424  	})
   425  }
   426  
   427  func restartNode1(ctx context.Context, t *testing.T, compose *docker.DockerCompose) {
   428  	// since node1 is the gossip "leader", node 2 must be stopped and restarted
   429  	// after node1 to re-facilitate internode communication
   430  	stopNode(ctx, t, compose, compose.GetWeaviateNode2().Name())
   431  	require.Nil(t, compose.Start(ctx, compose.GetWeaviate().Name()))
   432  	require.Nil(t, compose.Start(ctx, compose.GetWeaviateNode2().Name()))
   433  	<-time.After(1 * time.Second) // wait for initialization
   434  }
   435  
   436  func stopNode(ctx context.Context, t *testing.T, compose *docker.DockerCompose, container string) {
   437  	require.Nil(t, compose.Stop(ctx, container, nil))
   438  	<-time.After(1 * time.Second) // give time for shutdown
   439  }