/Users/deen/code/yugabyte-db/src/yb/integration-tests/alter_table-randomized-test.cc
Line | Count | Source (jump to first uncovered line) |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | // |
18 | | // The following only applies to changes made to this file as part of YugaByte development. |
19 | | // |
20 | | // Portions Copyright (c) YugaByte, Inc. |
21 | | // |
22 | | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
23 | | // in compliance with the License. You may obtain a copy of the License at |
24 | | // |
25 | | // http://www.apache.org/licenses/LICENSE-2.0 |
26 | | // |
27 | | // Unless required by applicable law or agreed to in writing, software distributed under the License |
28 | | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
29 | | // or implied. See the License for the specific language governing permissions and limitations |
30 | | // under the License. |
31 | | // |
32 | | |
33 | | #include <algorithm> |
34 | | #include <map> |
35 | | #include <vector> |
36 | | |
37 | | #include "yb/client/client-test-util.h" |
38 | | #include "yb/client/client.h" |
39 | | #include "yb/client/error.h" |
40 | | #include "yb/client/schema.h" |
41 | | #include "yb/client/session.h" |
42 | | #include "yb/client/table.h" |
43 | | #include "yb/client/table_alterer.h" |
44 | | #include "yb/client/table_creator.h" |
45 | | #include "yb/client/table_handle.h" |
46 | | #include "yb/client/yb_op.h" |
47 | | |
48 | | #include "yb/gutil/casts.h" |
49 | | #include "yb/gutil/map-util.h" |
50 | | #include "yb/gutil/stl_util.h" |
51 | | #include "yb/gutil/strings/substitute.h" |
52 | | |
53 | | #include "yb/integration-tests/cluster_verifier.h" |
54 | | #include "yb/integration-tests/external_mini_cluster.h" |
55 | | |
56 | | #include "yb/util/random.h" |
57 | | #include "yb/util/result.h" |
58 | | #include "yb/util/status_log.h" |
59 | | #include "yb/util/test_util.h" |
60 | | |
61 | | using namespace std::literals; |
62 | | |
63 | | namespace yb { |
64 | | |
65 | | using client::YBClient; |
66 | | using client::YBClientBuilder; |
67 | | using client::YBTableType; |
68 | | using client::YBError; |
69 | | using client::YBqlWriteOp; |
70 | | using client::YBSchema; |
71 | | using client::YBSchemaBuilder; |
72 | | using client::YBSession; |
73 | | using client::YBTable; |
74 | | using client::YBTableAlterer; |
75 | | using client::YBTableCreator; |
76 | | using client::YBTableName; |
77 | | using client::YBValue; |
78 | | using std::shared_ptr; |
79 | | using std::make_pair; |
80 | | using std::map; |
81 | | using std::pair; |
82 | | using std::vector; |
83 | | using strings::SubstituteAndAppend; |
84 | | |
85 | | static const YBTableName kTableName(YQL_DATABASE_CQL, "my_keyspace", "test-table"); |
86 | | static const int kMaxColumns = 30; |
87 | | |
88 | | class AlterTableRandomized : public YBTest { |
89 | | public: |
90 | 2 | void SetUp() override { |
91 | 2 | YBTest::SetUp(); |
92 | | |
93 | 2 | ExternalMiniClusterOptions opts; |
94 | 2 | opts.num_tablet_servers = 3; |
95 | | // Because this test performs a lot of alter tables, we end up flushing |
96 | | // and rewriting metadata files quite a bit. Globally disabling fsync |
97 | | // speeds the test runtime up dramatically. |
98 | 2 | opts.extra_tserver_flags.push_back("--never_fsync"); |
99 | 2 | cluster_.reset(new ExternalMiniCluster(opts)); |
100 | 2 | ASSERT_OK(cluster_->Start()); |
101 | | |
102 | 2 | client_ = ASSERT_RESULT(cluster_->CreateClient()); |
103 | 2 | } |
104 | | |
105 | 2 | void TearDown() override { |
106 | 2 | client_.reset(); |
107 | 2 | cluster_->Shutdown(); |
108 | 2 | YBTest::TearDown(); |
109 | 2 | } |
110 | | |
111 | 22 | void RestartTabletServer(size_t idx) { |
112 | 22 | LOG(INFO) << "Restarting TS " << idx; |
113 | 22 | cluster_->tablet_server(idx)->Shutdown(); |
114 | 22 | CHECK_OK(cluster_->tablet_server(idx)->Restart()); |
115 | 22 | CHECK_OK(cluster_->WaitForTabletsRunning( |
116 | 22 | cluster_->tablet_server(idx), MonoDelta::FromSeconds(60))); |
117 | 22 | } |
118 | | |
119 | | protected: |
120 | | std::unique_ptr<ExternalMiniCluster> cluster_; |
121 | | std::unique_ptr<YBClient> client_; |
122 | | }; |
123 | | |
124 | | typedef std::vector<std::pair<std::string, int32_t>> Row; |
125 | | |
126 | | // We use this special value to denote NULL values. |
127 | | // We ensure that we never insert or update to this value except in the case of NULLable columns. |
128 | | const int32_t kNullValue = 0xdeadbeef; |
129 | | |
130 | 47 | std::string RowToString(const Row& row) { |
131 | 47 | string ret = "{ "; |
132 | 47 | bool first = true; |
133 | 1.41k | for (const auto& e : row) { |
134 | 1.41k | if (!first) { |
135 | 1.36k | ret.append(", "); |
136 | 1.36k | } |
137 | 1.41k | first = false; |
138 | 1.41k | if (e.second == kNullValue) { |
139 | 357 | ret += "null"; |
140 | 1.05k | } else { |
141 | 1.05k | SubstituteAndAppend(&ret, "int32:$0", e.second); |
142 | 1.05k | } |
143 | 1.41k | } |
144 | 47 | ret += " }"; |
145 | | |
146 | 47 | return ret; |
147 | 47 | } |
148 | | |
149 | | struct TableState { |
150 | 2 | TableState() { |
151 | 2 | col_names_.push_back("key"); |
152 | 2 | col_nullable_.push_back(false); |
153 | 2 | } |
154 | | |
155 | | void GenRandomRow(int32_t key, int32_t seed, |
156 | 377 | vector<pair<string, int32_t>>* row) { |
157 | 377 | if (seed == kNullValue) { |
158 | 0 | seed++; |
159 | 0 | } |
160 | 377 | row->clear(); |
161 | 377 | row->push_back(make_pair("key", key)); |
162 | 7.42k | for (size_t i = 1; i < col_names_.size(); i++) { |
163 | 7.05k | int32_t val; |
164 | 7.05k | if (col_nullable_[i] && seed % 2 == 1) { |
165 | 1.77k | val = kNullValue; |
166 | 5.27k | } else { |
167 | 5.27k | val = seed; |
168 | 5.27k | } |
169 | 7.05k | row->push_back(make_pair(col_names_[i], val)); |
170 | 7.05k | } |
171 | 377 | } |
172 | | |
173 | 396 | bool Insert(const Row& data) { |
174 | 396 | DCHECK_EQ("key", data[0].first); |
175 | 396 | int32_t key = data[0].second; |
176 | | |
177 | 396 | return rows_.emplace(key, data).second; |
178 | 396 | } |
179 | | |
180 | 201 | bool Update(const vector<pair<string, int32_t>>& data) { |
181 | 201 | DCHECK_EQ("key", data[0].first); |
182 | 201 | int32_t key = data[0].second; |
183 | 201 | auto it = rows_.find(key); |
184 | 201 | if (it == rows_.end()) return false; |
185 | | |
186 | 201 | it->second = data; |
187 | 201 | return true; |
188 | 201 | } |
189 | | |
190 | 349 | void Delete(int32_t row_key) { |
191 | 0 | CHECK(rows_.erase(row_key)) << "row key " << row_key << " not found"; |
192 | 349 | } |
193 | | |
194 | 69 | void AddColumnWithDefault(const string& name, int32_t def, bool nullable) { |
195 | 69 | col_names_.push_back(name); |
196 | 69 | col_nullable_.push_back(nullable); |
197 | 1.41k | for (auto& e : rows_) { |
198 | 1.41k | e.second.emplace_back(name, def); |
199 | 1.41k | } |
200 | 69 | } |
201 | | |
202 | 39 | void DropColumn(const string& name) { |
203 | 39 | auto col_it = std::find(col_names_.begin(), col_names_.end(), name); |
204 | 39 | auto index = col_it - col_names_.begin(); |
205 | 39 | col_names_.erase(col_it); |
206 | 39 | col_nullable_.erase(col_nullable_.begin() + index); |
207 | 686 | for (auto& e : rows_) { |
208 | 686 | e.second.erase(e.second.begin() + index); |
209 | 686 | } |
210 | 39 | } |
211 | | |
212 | 536 | int32_t GetRandomRowKey(int32_t rand) { |
213 | 536 | CHECK(!rows_.empty()); |
214 | 536 | auto it = rows_.begin(); |
215 | 536 | std::advance(it, rand % rows_.size()); |
216 | 536 | return it->first; |
217 | 536 | } |
218 | | |
219 | 21 | void ToStrings(vector<string>* strs) { |
220 | 21 | strs->clear(); |
221 | 47 | for (const auto& e : rows_) { |
222 | 47 | strs->push_back(RowToString(e.second)); |
223 | 47 | } |
224 | 21 | } |
225 | | |
226 | | // The name of each column. |
227 | | vector<string> col_names_; |
228 | | |
229 | | // For each column, whether it is NULLable. |
230 | | // Has the same length as col_names_. |
231 | | vector<bool> col_nullable_; |
232 | | |
233 | | std::map<int32_t, Row> rows_; |
234 | | }; |
235 | | |
236 | | struct MirrorTable { |
237 | | explicit MirrorTable(client::YBClient* client) |
238 | 2 | : client_(client) {} |
239 | | |
240 | 2 | Status Create() { |
241 | 2 | RETURN_NOT_OK(client_->CreateNamespaceIfNotExists(kTableName.namespace_name(), |
242 | 2 | kTableName.namespace_type())); |
243 | 2 | YBSchema schema; |
244 | 2 | YBSchemaBuilder b; |
245 | 2 | b.AddColumn("key")->Type(INT32)->HashPrimaryKey()->NotNull(); |
246 | 2 | CHECK_OK(b.Build(&schema)); |
247 | 2 | std::unique_ptr<YBTableCreator> table_creator(client_->NewTableCreator()); |
248 | 2 | return table_creator->table_name(kTableName) |
249 | 2 | .schema(&schema) |
250 | 2 | .Create(); |
251 | 2 | } |
252 | | |
253 | 377 | bool TryInsert(int32_t row_key, int32_t rand) { |
254 | 377 | vector<pair<string, int32_t>> row; |
255 | 377 | ts_.GenRandomRow(row_key, rand, &row); |
256 | 377 | return TryInsert(row); |
257 | 377 | } |
258 | | |
259 | 396 | bool TryInsert(const Row& row) { |
260 | 396 | if (!ts_.Insert(row)) { |
261 | 0 | return false; |
262 | 0 | } |
263 | | |
264 | 396 | Status s = DoRealOp(row, INSERT); |
265 | 396 | CHECK_OK(s); |
266 | | |
267 | 396 | return true; |
268 | 396 | } |
269 | | |
270 | 331 | void DeleteRandomRow(uint32_t rand) { |
271 | 331 | if (ts_.rows_.empty()) return; |
272 | 330 | DeleteRow(ts_.GetRandomRowKey(rand)); |
273 | 330 | } |
274 | | |
275 | 349 | void DeleteRow(int32_t row_key) { |
276 | 349 | Row del = {{"key", row_key}}; |
277 | 349 | ts_.Delete(row_key); |
278 | 349 | CHECK_OK(DoRealOp(del, DELETE)); |
279 | 349 | } |
280 | | |
281 | 206 | void UpdateRandomRow(uint32_t rand) { |
282 | 206 | if (ts_.rows_.empty()) return; |
283 | 206 | int32_t row_key = ts_.GetRandomRowKey(rand); |
284 | | |
285 | 206 | vector<pair<string, int32_t>> update; |
286 | 206 | update.push_back(make_pair("key", row_key)); |
287 | 4.05k | for (size_t i = 1; i < num_columns(); i++) { |
288 | 3.84k | auto val = static_cast<int32_t>(rand * i); |
289 | 3.84k | if (val == kNullValue) val++; |
290 | 3.84k | if (ts_.col_nullable_[i] && val % 2 == 1) { |
291 | 377 | val = kNullValue; |
292 | 377 | } |
293 | 3.84k | update.push_back(make_pair(ts_.col_names_[i], val)); |
294 | 3.84k | } |
295 | | |
296 | 206 | if (update.size() == 1) { |
297 | | // No columns got updated. Just ignore this update. |
298 | 5 | return; |
299 | 5 | } |
300 | | |
301 | 201 | Status s = DoRealOp(update, UPDATE); |
302 | 201 | if (s.IsNotFound()) { |
303 | 0 | CHECK(!ts_.Update(update)) << "real table said not-found, fake table succeeded"; |
304 | 0 | return; |
305 | 0 | } |
306 | 201 | CHECK_OK(s); |
307 | | |
308 | 201 | CHECK(ts_.Update(update)); |
309 | 201 | } |
310 | | |
311 | 49 | void AddAColumn(const string& name) { |
312 | 49 | bool nullable = random() % 2 == 1; |
313 | 49 | return AddAColumn(name, nullable); |
314 | 49 | } |
315 | | |
316 | 69 | void AddAColumn(const string& name, bool nullable) { |
317 | | // Add to the real table. |
318 | 69 | std::unique_ptr<YBTableAlterer> table_alterer(client_->NewTableAlterer(kTableName)); |
319 | | |
320 | 69 | table_alterer->AddColumn(name)->Type(INT32); |
321 | 69 | ASSERT_OK(table_alterer->Alter()); |
322 | | |
323 | | // Add to the mirror state. |
324 | 69 | ts_.AddColumnWithDefault(name, kNullValue, nullable); |
325 | 69 | } |
326 | | |
327 | 39 | void DropAColumn(const string& name) { |
328 | 39 | std::unique_ptr<YBTableAlterer> table_alterer(client_->NewTableAlterer(kTableName)); |
329 | 39 | CHECK_OK(table_alterer->DropColumn(name)->Alter()); |
330 | 39 | ts_.DropColumn(name); |
331 | 39 | } |
332 | | |
333 | 20 | void DropRandomColumn(int seed) { |
334 | 20 | if (num_columns() == 1) return; |
335 | | |
336 | 20 | string name = ts_.col_names_[1 + (seed % (num_columns() - 1))]; |
337 | 20 | DropAColumn(name); |
338 | 20 | } |
339 | | |
340 | 4.15k | size_t num_columns() const { |
341 | 4.15k | return ts_.col_names_.size(); |
342 | 4.15k | } |
343 | | |
344 | 21 | void Verify() { |
345 | | // First scan the real table |
346 | 21 | vector<string> rows; |
347 | 21 | { |
348 | 21 | client::TableHandle table; |
349 | 21 | CHECK_OK(table.Open(kTableName, client_)); |
350 | 21 | client::ScanTableToStrings(table, &rows); |
351 | 21 | } |
352 | 21 | std::sort(rows.begin(), rows.end()); |
353 | | |
354 | | // Then get our mock table. |
355 | 21 | vector<string> expected; |
356 | 21 | ts_.ToStrings(&expected); |
357 | | |
358 | | // They should look the same. |
359 | 21 | LogVectorDiff(expected, rows); |
360 | 21 | ASSERT_EQ(expected, rows); |
361 | 21 | } |
362 | | |
363 | | private: |
364 | | enum OpType { |
365 | | INSERT, UPDATE, DELETE |
366 | | }; |
367 | | |
368 | 946 | Status DoRealOp(const vector<pair<string, int32_t>>& data, OpType op_type) { |
369 | 946 | auto deadline = MonoTime::Now() + 15s; |
370 | 946 | shared_ptr<YBSession> session = client_->NewSession(); |
371 | 946 | session->SetTimeout(15s); |
372 | 946 | shared_ptr<YBTable> table; |
373 | 946 | RETURN_NOT_OK(client_->OpenTable(kTableName, &table)); |
374 | 946 | for (;;) { |
375 | 946 | auto op = CreateOp(table, op_type); |
376 | 946 | auto* const req = op->mutable_request(); |
377 | 946 | bool first = true; |
378 | 946 | auto schema = table->schema(); |
379 | 11.8k | for (const auto& d : data) { |
380 | 11.8k | if (first) { |
381 | 946 | req->add_hashed_column_values()->mutable_value()->set_int32_value(d.second); |
382 | 946 | first = false; |
383 | 946 | continue; |
384 | 946 | } |
385 | 10.9k | auto column_value = req->add_column_values(); |
386 | 146k | for (size_t i = 0; i < schema.num_columns(); ++i) { |
387 | 146k | if (schema.Column(i).name() == d.first) { |
388 | 10.9k | column_value->set_column_id(schema.ColumnId(i)); |
389 | 10.9k | auto value = column_value->mutable_expr()->mutable_value(); |
390 | 10.9k | if (d.second != kNullValue) { |
391 | 8.77k | value->set_int32_value(d.second); |
392 | 8.77k | } |
393 | 10.9k | break; |
394 | 10.9k | } |
395 | 146k | } |
396 | 10.9k | } |
397 | 946 | session->Apply(op); |
398 | 946 | const auto flush_status = session->FlushAndGetOpsErrors(); |
399 | 946 | const auto& s = flush_status.status; |
400 | 946 | if (s.ok()) { |
401 | 946 | if (op->response().status() == QLResponsePB::YQL_STATUS_SCHEMA_VERSION_MISMATCH && |
402 | 0 | MonoTime::Now() < deadline) { |
403 | 0 | continue; |
404 | 0 | } |
405 | 0 | CHECK(op->succeeded()) << op->response().ShortDebugString(); |
406 | 946 | return s; |
407 | 946 | } |
408 | | |
409 | 0 | CHECK_EQ(flush_status.errors.size(), 1); |
410 | 0 | return flush_status.errors[0]->status(); |
411 | 0 | } |
412 | 946 | } |
413 | | |
414 | 946 | shared_ptr<YBqlWriteOp> CreateOp(const shared_ptr<YBTable>& table, OpType op_type) { |
415 | 946 | switch (op_type) { |
416 | 396 | case INSERT: |
417 | 396 | return shared_ptr<YBqlWriteOp>(table->NewQLInsert()); |
418 | 201 | case UPDATE: |
419 | 201 | return shared_ptr<YBqlWriteOp>(table->NewQLUpdate()); |
420 | 349 | case DELETE: |
421 | 349 | return shared_ptr<YBqlWriteOp>(table->NewQLDelete()); |
422 | 0 | } |
423 | 0 | return shared_ptr<YBqlWriteOp>(); |
424 | 0 | } |
425 | | |
426 | | YBClient* client_; |
427 | | TableState ts_; |
428 | | }; |
429 | | |
430 | | // Stress test for various alter table scenarios. This performs a random sequence of: |
431 | | // - insert a row (using the latest schema) |
432 | | // - delete a random row |
433 | | // - update a row (all columns with the latest schema) |
434 | | // - add a new column |
435 | | // - drop a column |
436 | | // - restart the tablet server |
437 | | // |
438 | | // During the sequence of operations, a "mirror" of the table in memory is kept up to |
439 | | // date. We periodically scan the actual table, and ensure that the data in YB |
440 | | // matches our in-memory "mirror". |
441 | 1 | TEST_F(AlterTableRandomized, TestRandomSequence) { |
442 | 1 | MirrorTable t(client_.get()); |
443 | 1 | ASSERT_OK(t.Create()); |
444 | | |
445 | 1 | Random rng(SeedRandom()); |
446 | | |
447 | 1 | const int n_iters = AllowSlowTests() ? 2000 : 1000; |
448 | 1.00k | for (int i = 0; i < n_iters; i++) { |
449 | | // Perform different operations with varying probability. |
450 | | // We mostly insert and update, with occasional deletes, |
451 | | // and more occasional table alterations or restarts. |
452 | 1.00k | int r = rng.Uniform(1000); |
453 | 1.00k | if (r < 400) { |
454 | 377 | bool inserted = t.TryInsert(1000000 + rng.Uniform(1000000), rng.Next()); |
455 | 377 | if (!inserted) { |
456 | 0 | continue; |
457 | 0 | } |
458 | 623 | } else if (r < 600) { |
459 | 206 | t.UpdateRandomRow(rng.Next()); |
460 | 417 | } else if (r < 920) { |
461 | 331 | t.DeleteRandomRow(rng.Next()); |
462 | 86 | } else if (r < 970) { |
463 | 63 | if (t.num_columns() < kMaxColumns) { |
464 | 49 | t.AddAColumn(strings::Substitute("c$0", i)); |
465 | 49 | } |
466 | 23 | } else if (r < 995) { |
467 | 20 | t.DropRandomColumn(rng.Next()); |
468 | 3 | } else { |
469 | 3 | RestartTabletServer(rng.Uniform64(cluster_->num_tablet_servers())); |
470 | 3 | } |
471 | | |
472 | 1.00k | if (i % 1000 == 0) { |
473 | 1 | LOG(INFO) << "Verifying iteration " << i; |
474 | 1 | ASSERT_NO_FATALS(t.Verify()); |
475 | 1 | LOG(INFO) << "Verification of iteration " << i << " successful"; |
476 | 1 | } |
477 | 1.00k | } |
478 | | |
479 | 1 | LOG(INFO) << "About to do the last verification"; |
480 | 1 | ASSERT_NO_FATALS(t.Verify()); |
481 | 1 | LOG(INFO) << "Last verification succeeded"; |
482 | | |
483 | | // Not only should the data returned by a scanner match what we expect, |
484 | | // we also expect all of the replicas to agree with each other. |
485 | 1 | ClusterVerifier cluster_verifier(cluster_.get()); |
486 | 1 | ASSERT_NO_FATALS(cluster_verifier.CheckCluster()); |
487 | 1 | } |
488 | | |
489 | 1 | TEST_F(AlterTableRandomized, AddDropRestart) { |
490 | 1 | MirrorTable t(client_.get()); |
491 | 1 | ASSERT_OK(t.Create()); |
492 | | |
493 | 1 | t.AddAColumn("value", true); |
494 | 20 | for (int key = 1; key != 20; ++key) { |
495 | 19 | LOG(INFO) << "================================================================================"; |
496 | 19 | auto column = Format("c_$0", key); |
497 | 19 | t.AddAColumn(column, true); |
498 | 19 | t.TryInsert({{ "key", key }, { "value", 666 }, { column, 1111 }}); |
499 | 19 | t.DropAColumn(column); |
500 | 19 | RestartTabletServer(0); |
501 | 19 | t.DeleteRow(key); |
502 | 19 | ASSERT_NO_FATALS(t.Verify()); |
503 | 19 | } |
504 | 1 | } |
505 | | |
506 | | } // namespace yb |