rippled
Loading...
Searching...
No Matches
SHAMapStore_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/envconfig.h>
3
4#include <xrpld/app/main/Application.h>
5#include <xrpld/app/main/NodeStoreScheduler.h>
6#include <xrpld/app/misc/SHAMapStore.h>
7#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
8#include <xrpld/core/ConfigSections.h>
9
10#include <xrpl/nodestore/detail/DatabaseRotatingImp.h>
11#include <xrpl/protocol/jss.h>
12
13namespace xrpl {
14namespace test {
15
17{
18 static auto const deleteInterval = 8;
19
20 static auto
22 {
23 cfg->LEDGER_HISTORY = deleteInterval;
24 auto& section = cfg->section(ConfigSection::nodeDatabase());
25 section.set("online_delete", std::to_string(deleteInterval));
26 return cfg;
27 }
28
29 static auto
31 {
32 cfg = onlineDelete(std::move(cfg));
33 cfg->section(ConfigSection::nodeDatabase()).set("advisory_delete", "1");
34 return cfg;
35 }
36
37 bool
38 goodLedger(jtx::Env& env, Json::Value const& json, std::string ledgerID, bool checkDB = false)
39 {
40 auto good = json.isMember(jss::result) && !RPC::contains_error(json[jss::result]) &&
41 json[jss::result][jss::ledger][jss::ledger_index] == ledgerID;
42 if (!good || !checkDB)
43 return good;
44
45 auto const seq = json[jss::result][jss::ledger_index].asUInt();
46
48 if (!oinfo)
49 return false;
50 LedgerHeader const& info = oinfo.value();
51
52 std::string const outHash = to_string(info.hash);
53 LedgerIndex const outSeq = info.seq;
54 std::string const outParentHash = to_string(info.parentHash);
55 std::string const outDrops = to_string(info.drops);
56 std::uint64_t const outCloseTime = info.closeTime.time_since_epoch().count();
57 std::uint64_t const outParentCloseTime = info.parentCloseTime.time_since_epoch().count();
58 std::uint64_t const outCloseTimeResolution = info.closeTimeResolution.count();
59 std::uint64_t const outCloseFlags = info.closeFlags;
60 std::string const outAccountHash = to_string(info.accountHash);
61 std::string const outTxHash = to_string(info.txHash);
62
63 auto const& ledger = json[jss::result][jss::ledger];
64 return outHash == ledger[jss::ledger_hash].asString() && outSeq == seq &&
65 outParentHash == ledger[jss::parent_hash].asString() && outDrops == ledger[jss::total_coins].asString() &&
66 outCloseTime == ledger[jss::close_time].asUInt() &&
67 outParentCloseTime == ledger[jss::parent_close_time].asUInt() &&
68 outCloseTimeResolution == ledger[jss::close_time_resolution].asUInt() &&
69 outCloseFlags == ledger[jss::close_flags].asUInt() &&
70 outAccountHash == ledger[jss::account_hash].asString() &&
71 outTxHash == ledger[jss::transaction_hash].asString();
72 }
73
74 bool
76 {
77 return json.isMember(jss::result) && RPC::contains_error(json[jss::result]) &&
78 json[jss::result][jss::error_code] == error;
79 }
80
83 {
84 BEAST_EXPECT(
85 json.isMember(jss::result) && json[jss::result].isMember(jss::ledger) &&
86 json[jss::result][jss::ledger].isMember(jss::ledger_hash) &&
87 json[jss::result][jss::ledger][jss::ledger_hash].isString());
88 return json[jss::result][jss::ledger][jss::ledger_hash].asString();
89 }
90
91 void
92 ledgerCheck(jtx::Env& env, int const rows, int const first)
93 {
94 auto const [actualRows, actualFirst, actualLast] =
95 dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())->getLedgerCountMinMax();
96
97 BEAST_EXPECT(actualRows == rows);
98 BEAST_EXPECT(actualFirst == first);
99 BEAST_EXPECT(actualLast == first + rows - 1);
100 }
101
102 void
103 transactionCheck(jtx::Env& env, int const rows)
104 {
105 BEAST_EXPECT(dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())->getTransactionCount() == rows);
106 }
107
108 void
109 accountTransactionCheck(jtx::Env& env, int const rows)
110 {
111 BEAST_EXPECT(
112 dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())->getAccountTransactionCount() == rows);
113 }
114
115 int
117 {
118 using namespace std::chrono_literals;
119
120 auto& store = env.app().getSHAMapStore();
121
122 int ledgerSeq = 3;
123 store.rendezvous();
124 BEAST_EXPECT(!store.getLastRotated());
125
126 env.close();
127 store.rendezvous();
128
129 auto ledger = env.rpc("ledger", "validated");
130 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++)));
131
132 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
133 return ledgerSeq;
134 }
135
136public:
137 void
139 {
140 using namespace std::chrono_literals;
141
142 testcase("clearPrior");
143 using namespace jtx;
144
145 Env env(*this, envconfig(onlineDelete));
146
147 auto& store = env.app().getSHAMapStore();
148 env.fund(XRP(10000), noripple("alice"));
149
150 ledgerCheck(env, 1, 2);
151 transactionCheck(env, 0);
153
155
156 auto ledgerTmp = env.rpc("ledger", "0");
157 BEAST_EXPECT(bad(ledgerTmp));
158
159 ledgers.emplace(std::make_pair(1, env.rpc("ledger", "1")));
160 BEAST_EXPECT(goodLedger(env, ledgers[1], "1"));
161
162 ledgers.emplace(std::make_pair(2, env.rpc("ledger", "2")));
163 BEAST_EXPECT(goodLedger(env, ledgers[2], "2"));
164
165 ledgerTmp = env.rpc("ledger", "current");
166 BEAST_EXPECT(goodLedger(env, ledgerTmp, "3"));
167
168 ledgerTmp = env.rpc("ledger", "4");
169 BEAST_EXPECT(bad(ledgerTmp));
170
171 ledgerTmp = env.rpc("ledger", "100");
172 BEAST_EXPECT(bad(ledgerTmp));
173
174 auto const firstSeq = waitForReady(env);
175 auto lastRotated = firstSeq - 1;
176
177 for (auto i = firstSeq + 1; i < deleteInterval + firstSeq; ++i)
178 {
179 env.fund(XRP(10000), noripple("test" + std::to_string(i)));
180 env.close();
181
182 ledgerTmp = env.rpc("ledger", "current");
183 BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i)));
184 }
185 BEAST_EXPECT(store.getLastRotated() == lastRotated);
186
187 for (auto i = 3; i < deleteInterval + lastRotated; ++i)
188 {
189 ledgers.emplace(std::make_pair(i, env.rpc("ledger", std::to_string(i))));
190 BEAST_EXPECT(goodLedger(env, ledgers[i], std::to_string(i), true) && getHash(ledgers[i]).length());
191 }
192
193 ledgerCheck(env, deleteInterval + 1, 2);
196
197 {
198 // Closing one more ledger triggers a rotate
199 env.close();
200
201 auto ledger = env.rpc("ledger", "current");
202 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(deleteInterval + 4)));
203 }
204
205 store.rendezvous();
206
207 BEAST_EXPECT(store.getLastRotated() == deleteInterval + 3);
208 lastRotated = store.getLastRotated();
209 BEAST_EXPECT(lastRotated == 11);
210
211 // That took care of the fake hashes
212 ledgerCheck(env, deleteInterval + 1, 3);
215
216 // The last iteration of this loop should trigger a rotate
217 for (auto i = lastRotated - 1; i < lastRotated + deleteInterval - 1; ++i)
218 {
219 env.close();
220
221 ledgerTmp = env.rpc("ledger", "current");
222 BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i + 3)));
223
224 ledgers.emplace(std::make_pair(i, env.rpc("ledger", std::to_string(i))));
225 BEAST_EXPECT(store.getLastRotated() == lastRotated || i == lastRotated + deleteInterval - 2);
226 BEAST_EXPECT(goodLedger(env, ledgers[i], std::to_string(i), true) && getHash(ledgers[i]).length());
227 }
228
229 store.rendezvous();
230
231 BEAST_EXPECT(store.getLastRotated() == deleteInterval + lastRotated);
232
233 ledgerCheck(env, deleteInterval + 1, lastRotated);
234 transactionCheck(env, 0);
236 }
237
238 void
240 {
241 testcase("automatic online_delete");
242 using namespace jtx;
243 using namespace std::chrono_literals;
244
245 Env env(*this, envconfig(onlineDelete));
246 auto& store = env.app().getSHAMapStore();
247
248 auto ledgerSeq = waitForReady(env);
249 auto lastRotated = ledgerSeq - 1;
250 BEAST_EXPECT(store.getLastRotated() == lastRotated);
251 BEAST_EXPECT(lastRotated != 2);
252
253 // Because advisory_delete is unset,
254 // "can_delete" is disabled.
255 auto const canDelete = env.rpc("can_delete");
256 BEAST_EXPECT(bad(canDelete, rpcNOT_ENABLED));
257
258 // Close ledgers without triggering a rotate
259 for (; ledgerSeq < lastRotated + deleteInterval; ++ledgerSeq)
260 {
261 env.close();
262
263 auto ledger = env.rpc("ledger", "validated");
264 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
265 }
266
267 store.rendezvous();
268
269 // The database will always have back to ledger 2,
270 // regardless of lastRotated.
271 ledgerCheck(env, ledgerSeq - 2, 2);
272 BEAST_EXPECT(lastRotated == store.getLastRotated());
273
274 {
275 // Closing one more ledger triggers a rotate
276 env.close();
277
278 auto ledger = env.rpc("ledger", "validated");
279 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
280 }
281
282 store.rendezvous();
283
284 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
285 BEAST_EXPECT(lastRotated != store.getLastRotated());
286
287 lastRotated = store.getLastRotated();
288
289 // Close enough ledgers to trigger another rotate
290 for (; ledgerSeq < lastRotated + deleteInterval + 1; ++ledgerSeq)
291 {
292 env.close();
293
294 auto ledger = env.rpc("ledger", "validated");
295 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
296 }
297
298 store.rendezvous();
299
300 ledgerCheck(env, deleteInterval + 1, lastRotated);
301 BEAST_EXPECT(lastRotated != store.getLastRotated());
302 }
303
304 void
306 {
307 testcase("online_delete with advisory_delete");
308 using namespace jtx;
309 using namespace std::chrono_literals;
310
311 // Same config with advisory_delete enabled
312 Env env(*this, envconfig(advisoryDelete));
313 auto& store = env.app().getSHAMapStore();
314
315 auto ledgerSeq = waitForReady(env);
316 auto lastRotated = ledgerSeq - 1;
317 BEAST_EXPECT(store.getLastRotated() == lastRotated);
318 BEAST_EXPECT(lastRotated != 2);
319
320 auto canDelete = env.rpc("can_delete");
321 BEAST_EXPECT(!RPC::contains_error(canDelete[jss::result]));
322 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0);
323
324 canDelete = env.rpc("can_delete", "never");
325 BEAST_EXPECT(!RPC::contains_error(canDelete[jss::result]));
326 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0);
327
328 auto const firstBatch = deleteInterval + ledgerSeq;
329 for (; ledgerSeq < firstBatch; ++ledgerSeq)
330 {
331 env.close();
332
333 auto ledger = env.rpc("ledger", "validated");
334 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
335 }
336
337 store.rendezvous();
338
339 ledgerCheck(env, ledgerSeq - 2, 2);
340 BEAST_EXPECT(lastRotated == store.getLastRotated());
341
342 // This does not kick off a cleanup
343 canDelete = env.rpc("can_delete", std::to_string(ledgerSeq + deleteInterval / 2));
344 BEAST_EXPECT(!RPC::contains_error(canDelete[jss::result]));
345 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + deleteInterval / 2);
346
347 store.rendezvous();
348
349 ledgerCheck(env, ledgerSeq - 2, 2);
350 BEAST_EXPECT(store.getLastRotated() == lastRotated);
351
352 {
353 // This kicks off a cleanup, but it stays small.
354 env.close();
355
356 auto ledger = env.rpc("ledger", "validated");
357 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
358 }
359
360 store.rendezvous();
361
362 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
363
364 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
365 lastRotated = ledgerSeq - 1;
366
367 for (; ledgerSeq < lastRotated + deleteInterval; ++ledgerSeq)
368 {
369 // No cleanups in this loop.
370 env.close();
371
372 auto ledger = env.rpc("ledger", "validated");
373 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
374 }
375
376 store.rendezvous();
377
378 BEAST_EXPECT(store.getLastRotated() == lastRotated);
379
380 {
381 // This kicks off another cleanup.
382 env.close();
383
384 auto ledger = env.rpc("ledger", "validated");
385 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
386 }
387
388 store.rendezvous();
389
390 ledgerCheck(env, ledgerSeq - firstBatch, firstBatch);
391
392 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
393 lastRotated = ledgerSeq - 1;
394
395 // This does not kick off a cleanup
396 canDelete = env.rpc("can_delete", "always");
397 BEAST_EXPECT(!RPC::contains_error(canDelete[jss::result]));
398 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == std::numeric_limits<unsigned int>::max());
399
400 for (; ledgerSeq < lastRotated + deleteInterval; ++ledgerSeq)
401 {
402 // No cleanups in this loop.
403 env.close();
404
405 auto ledger = env.rpc("ledger", "validated");
406 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
407 }
408
409 store.rendezvous();
410
411 BEAST_EXPECT(store.getLastRotated() == lastRotated);
412
413 {
414 // This kicks off another cleanup.
415 env.close();
416
417 auto ledger = env.rpc("ledger", "validated");
418 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
419 }
420
421 store.rendezvous();
422
423 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
424
425 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
426 lastRotated = ledgerSeq - 1;
427
428 // This does not kick off a cleanup
429 canDelete = env.rpc("can_delete", "now");
430 BEAST_EXPECT(!RPC::contains_error(canDelete[jss::result]));
431 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq - 1);
432
433 for (; ledgerSeq < lastRotated + deleteInterval; ++ledgerSeq)
434 {
435 // No cleanups in this loop.
436 env.close();
437
438 auto ledger = env.rpc("ledger", "validated");
439 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
440 }
441
442 store.rendezvous();
443
444 BEAST_EXPECT(store.getLastRotated() == lastRotated);
445
446 {
447 // This kicks off another cleanup.
448 env.close();
449
450 auto ledger = env.rpc("ledger", "validated");
451 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
452 }
453
454 store.rendezvous();
455
456 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
457
458 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
459 lastRotated = ledgerSeq - 1;
460 }
461
464 {
466 boost::filesystem::path newPath;
467
468 if (!BEAST_EXPECT(path.size()))
469 return {};
470 newPath = path;
471 section.set("path", newPath.string());
472
474 section,
476 scheduler,
477 env.app().logs().journal("NodeStoreTest"))};
478 backend->open();
479 return backend;
480 }
481
482 void
484 {
485 // The only purpose of this test is to ensure that if something that
486 // should never happen happens, we don't get a deadlock.
487 testcase("rotate with lock contention");
488
489 using namespace jtx;
490 Env env(*this, envconfig(onlineDelete));
491
493 // Create the backend. Normally, SHAMapStoreImp handles all these
494 // details
495 auto nscfg = env.app().config().section(ConfigSection::nodeDatabase());
496
497 // Provide default values:
498 if (!nscfg.exists("cache_size"))
499 nscfg.set(
501
502 if (!nscfg.exists("cache_age"))
503 nscfg.set(
505
506 NodeStoreScheduler scheduler(env.app().getJobQueue());
507
508 std::string const writableDb = "write";
509 std::string const archiveDb = "archive";
510 auto writableBackend = makeBackendRotating(env, scheduler, writableDb);
511 auto archiveBackend = makeBackendRotating(env, scheduler, archiveDb);
512
513 // Create NodeStore with two backends to allow online deletion of
514 // data
515 constexpr int readThreads = 4;
517 scheduler,
518 readThreads,
519 std::move(writableBackend),
520 std::move(archiveBackend),
521 nscfg,
522 env.app().logs().journal("NodeStoreTest"));
523
525 // Check basic functionality
526 using namespace std::chrono_literals;
527 std::atomic<int> threadNum = 0;
528
529 {
530 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
531
532 auto const cb = [&](std::string const& writableName, std::string const& archiveName) {
533 BEAST_EXPECT(writableName == "1");
534 BEAST_EXPECT(archiveName == "write");
535 // Ensure that dbr functions can be called from within the
536 // callback
537 BEAST_EXPECT(dbr->getName() == "1");
538 };
539
540 dbr->rotate(std::move(newBackend), cb);
541 }
542 BEAST_EXPECT(threadNum == 1);
543 BEAST_EXPECT(dbr->getName() == "1");
544
546 // Do something stupid. Try to re-enter rotate from inside the callback.
547 {
548 auto const cb = [&](std::string const& writableName, std::string const& archiveName) {
549 BEAST_EXPECT(writableName == "3");
550 BEAST_EXPECT(archiveName == "2");
551 // Ensure that dbr functions can be called from within the
552 // callback
553 BEAST_EXPECT(dbr->getName() == "3");
554 };
555 auto const cbReentrant = [&](std::string const& writableName, std::string const& archiveName) {
556 BEAST_EXPECT(writableName == "2");
557 BEAST_EXPECT(archiveName == "1");
558 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
559 // Reminder: doing this is stupid and should never happen
560 dbr->rotate(std::move(newBackend), cb);
561 };
562 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
563 dbr->rotate(std::move(newBackend), cbReentrant);
564 }
565
566 BEAST_EXPECT(threadNum == 3);
567 BEAST_EXPECT(dbr->getName() == "3");
568 }
569
570 void
571 run() override
572 {
573 testClear();
576 testRotate();
577 }
578};
579
580// VFALCO This test fails because of thread asynchronous issues
581BEAST_DEFINE_TESTSUITE(SHAMapStore, app, xrpl);
582
583} // namespace test
584} // namespace xrpl
Represents a JSON value.
Definition json_value.h:131
A testsuite class.
Definition suite.h:52
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:148
virtual Config & config()=0
virtual Logs & logs()=0
virtual SHAMapStore & getSHAMapStore()=0
virtual JobQueue & getJobQueue()=0
virtual RelationalDatabase & getRelationalDatabase()=0
Section & section(std::string const &name)
Returns the section with the given name.
int getValueFor(SizedItem item, std::optional< std::size_t > node=std::nullopt) const
Retrieve the default value for the item at the specified node size.
Definition Config.cpp:1013
beast::Journal journal(std::string const &name)
Definition Log.cpp:134
A NodeStore::Scheduler which uses the JobQueue.
static Manager & instance()
Returns the instance of the manager singleton.
virtual std::unique_ptr< Backend > make_Backend(Section const &parameters, std::size_t burstSize, Scheduler &scheduler, beast::Journal journal)=0
Create a backend.
virtual std::optional< LedgerHeader > getLedgerInfoByIndex(LedgerIndex ledgerSeq)=0
getLedgerInfoByIndex Returns a ledger by its sequence.
class to create database, launch online delete thread, and related SQLite database
Definition SHAMapStore.h:19
virtual void rendezvous() const =0
virtual std::size_t getAccountTransactionCount()=0
getAccountTransactionCount Returns the number of account transactions.
virtual std::size_t getTransactionCount()=0
getTransactionCount Returns the number of transactions.
Holds a collection of configuration values.
Definition BasicConfig.h:25
void set(std::string const &key, std::string const &value)
Set a key/value pair.
static auto onlineDelete(std::unique_ptr< Config > cfg)
static auto advisoryDelete(std::unique_ptr< Config > cfg)
bool bad(Json::Value const &json, error_code_i error=rpcLGR_NOT_FOUND)
std::string getHash(Json::Value const &json)
std::unique_ptr< NodeStore::Backend > makeBackendRotating(jtx::Env &env, NodeStoreScheduler &scheduler, std::string path)
void ledgerCheck(jtx::Env &env, int const rows, int const first)
void accountTransactionCheck(jtx::Env &env, int const rows)
void run() override
Runs the suite.
bool goodLedger(jtx::Env &env, Json::Value const &json, std::string ledgerID, bool checkDB=false)
void transactionCheck(jtx::Env &env, int const rows)
A transaction testing environment.
Definition Env.h:98
Application & app()
Definition Env.h:230
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:97
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:260
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:749
Inject raw JSON.
Definition jtx_json.h:14
Add a path.
Definition paths.h:38
T emplace(T... args)
T is_same_v
T make_pair(T... args)
bool contains_error(Json::Value const &json)
Returns true if the json contains an rpc error specification.
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:90
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
constexpr auto megabytes(T value) noexcept
error_code_i
Definition ErrorCodes.h:21
@ rpcLGR_NOT_FOUND
Definition ErrorCodes.h:53
@ rpcNOT_ENABLED
Definition ErrorCodes.h:40
static std::string nodeDatabase()
Information about the notional ledger backing the view.
NetClock::time_point parentCloseTime
NetClock::duration closeTimeResolution
NetClock::time_point closeTime
Set the sequence number on a JTx.
Definition seq.h:15
T time_since_epoch(T... args)
T to_string(T... args)
T value(T... args)