20 #include <boost/filesystem.hpp>
21 #include <boost/process.hpp>
22 #include <boost/range/combine.hpp>
23 #include <boost/uuid/uuid_generators.hpp>
24 #include <boost/uuid/uuid_io.hpp>
25 #include <boost/version.hpp>
35 #include <system_error>
65 #if BOOST_VERSION < 107300
68 template <
typename T,
typename U>
69 struct tuple_size<boost::tuples::cons<T, U>>
70 : boost::tuples::length<boost::tuples::cons<T, U>> {};
71 template <
size_t I,
typename T,
typename U>
72 struct tuple_element<I, boost::tuples::cons<T, U>>
73 : boost::tuples::element<I, boost::tuples::cons<T, U>> {};
83 return boost::filesystem::canonical(global_file_mgr->
getBasePath()).
string();
86 inline std::string
run(
const std::string& cmd,
87 const std::string& chdir =
"",
88 const bool log_failure =
true) {
89 VLOG(3) <<
"running cmd: " << cmd;
92 std::string output, errors;
94 using namespace boost::process;
95 ipstream stdout, stderr;
97 rcode = system(cmd, std_out > stdout, std_err > stderr, ec, start_dir = chdir);
99 rcode = system(cmd, std_out > stdout, std_err > stderr, ec);
101 std::ostringstream ss_output, ss_errors;
102 stdout >> ss_output.rdbuf();
103 stderr >> ss_errors.rdbuf();
104 output = ss_output.str();
105 errors = ss_errors.str();
109 LOG(
ERROR) <<
"failed cmd: " << cmd;
110 LOG(
ERROR) <<
"exit code: " << rcode;
111 LOG(
ERROR) <<
"error code: " << ec.value() <<
" - " << ec.message();
115 #if defined(__APPLE__)
120 if (1 == rcode && cmd.find(
"--fast-read") &&
121 (errors.find(
"cannot write decoded block") != std::string::npos ||
122 errors.find(
"Broken pipe") != std::string::npos)) {
124 LOG(
ERROR) <<
"tar error ignored on osx for --fast-read";
129 if (1 == rcode && errors.find(
"changed as we read") != std::string::npos) {
130 LOG(
ERROR) <<
"tar error ignored under concurrent inserts";
133 std::string error_message;
135 error_code = ec.value();
136 error_message = ec.message();
141 if (
to_lower(errors).find(
"permission denied") != std::string::npos) {
142 error_message =
"Insufficient file read/write permission.";
144 error_message = errors;
147 throw std::runtime_error(
148 "An error occurred while executing an internal command. Error code: " +
152 VLOG(3) <<
"finished cmd: " << cmd;
153 VLOG(3) <<
"time: " << time_ms <<
" ms";
154 VLOG(3) <<
"stdout: " << output;
160 const std::string& file_name,
161 const std::string& compression,
162 const bool log_failure =
true) {
165 #if defined(__APPLE__)
166 constexpr
static auto opt_occurrence =
"--fast-read";
168 constexpr
static auto opt_occurrence =
"--occurrence=1";
170 boost::filesystem::path temp_dir =
171 boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
172 boost::filesystem::create_directories(temp_dir);
174 opt_occurrence +
" " + file_name,
177 const auto output =
run(
"cat " + (temp_dir / file_name).
string());
178 boost::filesystem::remove_all(temp_dir);
183 const std::string& table,
184 const std::string& compression) {
185 const auto schema_str =
187 std::regex regex(
"@T");
188 return std::regex_replace(schema_str, regex, table);
194 const boost::filesystem::path& path,
195 const std::unordered_map<int, int>& column_ids_map,
196 const int32_t table_epoch,
197 const bool drop_not_update) {
198 const std::string file_path = path.string();
199 const std::string file_name = path.filename().string();
200 std::vector<std::string> tokens;
204 if (tokens.size() <= 2 || !(
DATA_FILE_EXT ==
"." + tokens[2] || tokens[2] ==
"mapd")) {
209 const auto page_size = boost::lexical_cast<int64_t>(tokens[1]);
211 std::unique_ptr<FILE, decltype(simple_file_closer)> fp(
214 throw std::runtime_error(
"Failed to open " + file_path +
215 " for update: " + std::strerror(errno));
221 for (
size_t page = 0; page < file_size / page_size; ++page) {
222 int32_t header_info[8];
223 if (0 != std::fseek(fp.get(), page * page_size, SEEK_SET)) {
224 throw std::runtime_error(
"Failed to seek to page# " +
std::to_string(page) +
225 file_path +
" for read: " + std::strerror(errno));
227 if (1 != fread(header_info,
sizeof header_info, 1, fp.get())) {
228 throw std::runtime_error(
"Failed to read " + file_path +
": " +
229 std::strerror(errno));
231 if (
const auto header_size = header_info[0]; header_size > 0) {
234 auto& contingent = header_info[1];
237 auto& epoch = header_info[2];
238 auto& col_id = header_info[3];
240 table_epoch, epoch, contingent)) {
243 auto column_map_it = column_ids_map.find(col_id);
244 bool rewrite_header =
false;
245 if (drop_not_update) {
250 if (column_map_it != column_ids_map.end()) {
252 std::memset(header_info, 0,
sizeof(header_info));
253 rewrite_header =
true;
256 if (column_map_it == column_ids_map.end()) {
257 throw std::runtime_error(
"Page " +
std::to_string(page) +
" in " + file_path +
259 ". Dump may be corrupt.");
263 if (
const auto dest_col_id = column_map_it->second; col_id != dest_col_id) {
264 col_id = dest_col_id;
265 rewrite_header =
true;
268 if (rewrite_header) {
269 if (0 != std::fseek(fp.get(), page * page_size, SEEK_SET)) {
270 throw std::runtime_error(
"Failed to seek to page# " +
std::to_string(page) +
271 file_path +
" for write: " + std::strerror(errno));
273 if (1 != fwrite(header_info,
sizeof header_info, 1, fp.get())) {
274 throw std::runtime_error(
"Failed to write " + file_path +
": " +
275 std::strerror(errno));
287 const int32_t table_epoch,
288 const std::string& temp_data_dir,
289 const std::unordered_map<int, int>& column_ids_map,
290 const bool drop_not_update) {
291 boost::filesystem::path base_path(temp_data_dir);
292 boost::filesystem::recursive_directory_iterator end_it;
294 for (boost::filesystem::recursive_directory_iterator fit(base_path); fit != end_it;
296 if (!boost::filesystem::is_symlink(fit->path()) &&
297 boost::filesystem::is_regular_file(fit->status())) {
306 thread_controller.
finish();
310 std::vector<boost::filesystem::path> symlinks;
311 for (boost::filesystem::directory_iterator it(table_data_dir), end_it; it != end_it;
313 if (boost::filesystem::is_symlink(it->path())) {
314 symlinks.emplace_back(it->path());
317 for (
const auto& symlink : symlinks) {
318 boost::filesystem::remove_all(symlink);
323 std::map<boost::filesystem::path, boost::filesystem::path> old_to_new_paths;
324 for (boost::filesystem::directory_iterator it(table_data_dir), end_it; it != end_it;
326 const auto path = boost::filesystem::canonical(it->path());
328 auto old_path = path;
331 if (!boost::filesystem::exists(old_path)) {
332 old_to_new_paths[old_path] = path;
336 for (
const auto& [old_path, new_path] : old_to_new_paths) {
337 boost::filesystem::create_symlink(new_path.filename(), old_path);
342 const std::string& temp_data_dir,
343 const std::vector<std::string>& target_paths,
344 const std::string& name_prefix) {
345 boost::filesystem::path base_path(temp_data_dir);
346 boost::filesystem::directory_iterator end_it;
347 int target_path_index = 0;
348 for (boost::filesystem::directory_iterator fit(base_path); fit != end_it; ++fit) {
349 if (!boost::filesystem::is_regular_file(fit->status())) {
350 const std::string file_path = fit->path().string();
351 const std::string file_name = fit->path().filename().string();
352 if (boost::istarts_with(file_name, name_prefix)) {
353 const std::string target_path =
354 abs_path(global_file_mgr) +
"/" + target_paths[target_path_index++];
355 if (std::rename(file_path.c_str(), target_path.c_str())) {
356 throw std::runtime_error(
"Failed to rename file " + file_path +
" to " +
357 target_path +
": " + std::strerror(errno));
371 const std::list<ColumnDescriptor>& src_columns,
372 std::vector<std::string>& src_oldinfo_strs,
373 const std::string& archive_path) {
375 std::vector<std::string> poly_column_names;
376 for (
auto const& src_column : src_columns) {
377 auto const sqltype = src_column.columnType.get_type();
379 poly_column_names.push_back(src_column.columnName);
385 std::unordered_map<int, int> column_ids_to_drop;
386 auto last_itr = std::remove_if(
387 src_oldinfo_strs.begin(),
388 src_oldinfo_strs.end(),
389 [&](
const std::string& v) ->
bool {
391 std::vector<std::string> tokens;
393 tokens, v, boost::is_any_of(
":"), boost::token_compress_on);
395 if (tokens.size() < 2) {
396 throw std::runtime_error(
397 "Dump " + archive_path +
398 " has invalid oldinfo file contents. Dump may be corrupt.");
400 auto const& column_name = tokens[0];
401 auto const column_id = std::stoi(tokens[1]);
402 for (
auto const& poly_column_name : poly_column_names) {
404 auto const render_group_column_name = poly_column_name +
"_render_group";
405 if (column_name == render_group_column_name) {
406 LOG(
INFO) <<
"RESTORE TABLE dropping render group column '"
407 << render_group_column_name <<
"' from dump " << archive_path;
409 column_ids_to_drop[column_id] = -1;
415 src_oldinfo_strs.erase(last_itr, src_oldinfo_strs.end());
417 return column_ids_to_drop;
421 const std::unordered_map<int, int>& render_group_column_ids,
422 const std::string& archive_path,
423 const std::string& temp_data_dir,
424 const std::string& compression) {
426 if (render_group_column_ids.size()) {
427 const auto epoch = boost::lexical_cast<int32_t>(
431 epoch, temp_data_dir, render_group_column_ids,
true );
433 VLOG(3) <<
"drop render group columns: " << time_ms <<
" ms";
440 const std::string& archive_path,
441 const std::string& compression) {
443 throw std::runtime_error(
"Dumping a system table is not supported.");
446 throw std::runtime_error(
"Dumping a foreign table is not supported.");
451 throw std::runtime_error(
"DUMP/RESTORE is not supported yet on distributed setup.");
453 if (boost::filesystem::exists(archive_path)) {
454 throw std::runtime_error(
"Archive " + archive_path +
" already exists.");
457 throw std::runtime_error(
"Dumping view or temporary table is not supported.");
464 std::vector<std::string> file_paths;
465 auto file_writer = [&file_paths, uuid](
const std::string& file_name,
466 const std::string& file_type,
467 const std::string& file_data) {
468 std::unique_ptr<FILE, decltype(simple_file_closer)> fp(
471 throw std::runtime_error(
"Failed to create " + file_type +
" file '" + file_name +
472 "': " + std::strerror(errno));
474 if (std::fwrite(file_data.data(), 1, file_data.size(), fp.get()) < file_data.size()) {
475 throw std::runtime_error(
"Failed to write " + file_type +
" file '" + file_name +
476 "': " + std::strerror(errno));
478 file_paths.push_back(uuid / std::filesystem::path(file_name).
filename());
481 const auto file_mgr_dir = std::filesystem::path(
abs_path(global_file_mgr));
482 const auto uuid_dir = file_mgr_dir / uuid;
484 if (!std::filesystem::create_directory(uuid_dir)) {
485 throw std::runtime_error(
"Failed to create work directory '" + uuid_dir.string() +
486 "' while dumping table.");
490 if (std::filesystem::exists(uuid_dir)) {
491 std::filesystem::remove_all(uuid_dir);
506 std::vector<std::string> column_oldinfo;
509 std::back_inserter(column_oldinfo),
510 [&](
const auto cd) -> std::string {
511 return cd->columnName +
":" +
std::to_string(cd->columnId) +
":" +
521 file_paths.insert(file_paths.end(), data_file_dirs.begin(), data_file_dirs.end());
524 file_paths.insert(file_paths.end(), dict_file_dirs.begin(), dict_file_dirs.end());
528 run(
"tar " + compression +
" --transform=s|" + uuid +
529 std::filesystem::path::preferred_separator +
"|| -cvf " +
537 const std::string& archive_path,
538 const std::string& compression) {
542 throw std::runtime_error(
"DUMP/RESTORE is not supported yet on distributed setup.");
544 if (!boost::filesystem::exists(archive_path)) {
545 throw std::runtime_error(
"Archive " + archive_path +
" does not exist.");
548 throw std::runtime_error(
"Restoring view or temporary table is not supported.");
552 const auto table_read_lock =
555 const auto insert_data_lock =
564 const auto uuid_dir = std::filesystem::path(
abs_path(global_file_mgr)) / uuid;
566 if (!std::filesystem::create_directory(uuid_dir)) {
567 throw std::runtime_error(
"Failed to create work directory '" + uuid_dir.string() +
568 "' while restoring table.");
572 if (std::filesystem::exists(uuid_dir)) {
573 std::filesystem::remove_all(uuid_dir);
578 constexpr
static const auto temp_data_basename =
"_data";
579 constexpr
static const auto temp_back_basename =
"_back";
580 const auto temp_data_dir = uuid_dir / temp_data_basename;
581 const auto temp_back_dir = uuid_dir / temp_back_basename;
587 CHECK(create_table_stmt);
591 std::list<ColumnDescriptor> src_columns;
592 std::vector<Parser::SharedDictionaryDef> shared_dict_defs;
593 create_table_stmt->executeDryRun(session, src_td, src_columns, shared_dict_defs);
598 throw std::runtime_error(
"Incompatible table VACCUM option");
603 throw std::runtime_error(
"Unmatched number of table shards");
606 const auto dst_columns =
608 if (dst_columns.size() != src_columns.size()) {
609 throw std::runtime_error(
"Unmatched number of table columns");
611 for (
const auto& [src_cd, dst_cd] : boost::combine(src_columns, dst_columns)) {
612 if (src_cd.columnType.get_type_name() != dst_cd->columnType.get_type_name() ||
613 src_cd.columnType.get_compression_name() !=
614 dst_cd->columnType.get_compression_name()) {
615 throw std::runtime_error(
"Incompatible types on column " + src_cd.columnName);
619 const auto all_src_oldinfo_str =
621 std::vector<std::string> src_oldinfo_strs;
624 boost::is_any_of(
" "),
625 boost::token_compress_on);
628 int dump_version = -1;
631 auto const dump_version_str =
633 dump_version = std::stoi(dump_version_str);
634 }
catch (std::runtime_error& e) {
638 LOG(
INFO) <<
"Dump Version: " << dump_version;
641 const bool do_drop_render_group_columns =
649 std::unordered_map<int, int> render_group_column_ids;
650 if (do_drop_render_group_columns) {
651 render_group_column_ids =
656 auto all_dst_columns =
658 if (src_oldinfo_strs.size() != all_dst_columns.size()) {
659 throw std::runtime_error(
"Source table has a unmatched number of columns: " +
669 std::unordered_map<int, int> column_ids_map;
670 std::unordered_map<std::string, std::string> dict_paths_map;
672 std::list<std::vector<std::string>> src_oldinfo_tokens;
674 src_oldinfo_strs.begin(),
675 src_oldinfo_strs.end(),
676 std::back_inserter(src_oldinfo_tokens),
677 [](
const auto& src_oldinfo_str) ->
auto{
678 std::vector<std::string> tokens;
680 tokens, src_oldinfo_str, boost::is_any_of(
":"), boost::token_compress_on);
683 src_oldinfo_tokens.sort(
684 [](
const auto& lhs,
const auto& rhs) {
return lhs[0].compare(rhs[0]) < 0; });
685 all_dst_columns.sort(
686 [](
auto a,
auto b) {
return a->columnName.compare(b->columnName) < 0; });
689 src_oldinfo_tokens.end(),
690 all_dst_columns.begin(),
691 std::inserter(column_ids_map, column_ids_map.end()),
692 [&](
const auto& tokens,
const auto& cd) -> std::pair<int, int> {
694 << cd->columnName <<
":" << cd->columnId;
696 return {boost::lexical_cast<
int>(tokens[1]), cd->columnId};
698 bool was_table_altered =
false;
699 std::for_each(column_ids_map.begin(), column_ids_map.end(), [&](
auto& it) {
700 was_table_altered = was_table_altered || it.first != it.second;
702 VLOG(3) <<
"was_table_altered = " << was_table_altered;
706 run(
"rm -rf " + temp_data_dir.string());
707 run(
"mkdir -p " + temp_data_dir.string());
711 if (do_drop_render_group_columns) {
713 render_group_column_ids, archive_path, temp_data_dir, compression);
717 if (was_table_altered) {
718 const auto epoch = boost::lexical_cast<int32_t>(
722 epoch, temp_data_dir, column_ids_map,
false );
724 VLOG(3) <<
"update_column_ids_table_files: " << time_ms <<
" ms";
731 std::vector<std::string> both_file_dirs;
732 std::merge(data_file_dirs.begin(),
733 data_file_dirs.end(),
734 dict_file_dirs.begin(),
735 dict_file_dirs.end(),
736 std::back_inserter(both_file_dirs));
737 bool backup_completed =
false;
739 run(
"rm -rf " + temp_back_dir.string());
740 run(
"mkdir -p " + temp_back_dir.string());
741 for (
const auto& dir : both_file_dirs) {
742 const auto dir_full_path =
abs_path(global_file_mgr) +
"/" + dir;
743 if (boost::filesystem::is_directory(dir_full_path)) {
744 run(
"mv " + dir_full_path +
" " + temp_back_dir.string());
747 backup_completed =
true;
751 for (
const auto& dit : dict_paths_map) {
752 if (!dit.first.empty() && !dit.second.empty()) {
753 const auto src_dict_path = temp_data_dir.string() +
"/" + dit.first;
754 const auto dst_dict_path =
abs_path(global_file_mgr) +
"/" + dit.second;
755 run(
"mv " + src_dict_path +
" " + dst_dict_path);
760 throw std::runtime_error(
"lol!");
765 if (backup_completed) {
770 boost::filesystem::path base_path(temp_back_dir);
771 boost::filesystem::directory_iterator end_it;
772 for (boost::filesystem::directory_iterator fit(base_path); fit != end_it; ++fit) {
773 run(
"mv " + fit->path().string() +
" .",
abs_path(global_file_mgr));
785 std::string get_restore_dir_path() {
787 auto restore_dir_path = std::filesystem::canonical(
g_base_path) /
789 return restore_dir_path;
792 std::string download_s3_file(
const std::string& s3_archive_path,
794 const std::string& restore_dir_path) {
802 std::optional<std::string>{},
803 std::optional<std::string>{},
804 std::optional<std::string>{},
806 s3_archive.init_for_read();
807 const auto& object_key = s3_archive.get_objkeys();
808 if (object_key.size() > 1) {
809 throw std::runtime_error(
810 "S3 URI references multiple files. Only one file can be restored at a time.");
812 CHECK_EQ(object_key.size(), size_t(1));
813 std::exception_ptr eptr;
814 return s3_archive.land(object_key[0], eptr,
false,
false,
false);
822 const std::string& table_name,
823 const std::string& archive_path,
824 const std::string& compression,
826 auto local_archive_path = archive_path;
828 const auto restore_dir_path = get_restore_dir_path();
829 ScopeGuard archive_cleanup_guard = [&archive_path, &restore_dir_path] {
830 if (
shared::is_s3_uri(archive_path) && std::filesystem::exists(restore_dir_path)) {
831 std::filesystem::remove_all(restore_dir_path);
835 local_archive_path = download_s3_file(archive_path, s3_options, restore_dir_path);
840 const auto schema_str =
get_table_schema(local_archive_path, table_name, compression);
843 CHECK(create_table_stmt);
844 create_table_stmt->execute(session,
false );
850 const auto schema_str =
"DROP TABLE IF EXISTS " + table_name +
";";
851 std::unique_ptr<Parser::Stmt> stmt =
854 CHECK(drop_table_stmt);
855 drop_table_stmt->execute(session,
false );
std::string s3_secret_key
std::string get_table_schema(const std::string &archive_path, const std::string &table, const std::string &compression)
void delete_old_symlinks(const std::string &table_data_dir)
std::string getBasePath() const
std::string getColumnDictDirectory(const ColumnDescriptor *cd, bool file_name_only=true) const
static constexpr char const * table_schema_filename
shared utility for globbing files, paths can be specified as either a single file, directory or wildcards
static TimeT::rep execution(F func, Args &&...args)
This file includes the class specification for the FILE manager (FileMgr), and related data structure...
std::string abs_path(const File_Namespace::GlobalFileMgr *global_file_mgr)
bool is_s3_uri(const std::string &file_path)
Data_Namespace::DataMgr & getDataMgr() const
std::unordered_map< int, int > find_render_group_columns(const std::list< ColumnDescriptor > &src_columns, std::vector< std::string > &src_oldinfo_strs, const std::string &archive_path)
void startThread(FuncType &&func, Args &&...args)
This file includes the class specification for the FILE manager (FileMgr), and related data structure...
virtual void checkThreadsStatus()
static constexpr char const * table_oldinfo_filename
int32_t getTableEpoch(const int32_t db_id, const int32_t table_id) const
void drop_render_group_columns(const std::unordered_map< int, int > &render_group_column_ids, const std::string &archive_path, const std::string &temp_data_dir, const std::string &compression)
std::vector< std::string > getTableDataDirectories(const TableDescriptor *td) const
bool isForeignTable() const
const std::string kDefaultImportDirName
Classes representing a parse tree.
static constexpr int kDumpVersion_remove_render_group_columns
const DBMetadata & getCurrentDB() const
void rename_table_directories(const File_Namespace::GlobalFileMgr *global_file_mgr, const std::string &temp_data_dir, const std::vector< std::string > &target_paths, const std::string &name_prefix)
::FILE * fopen(const char *filename, const char *mode)
OUTPUT transform(INPUT const &input, FUNC const &func)
File_Namespace::GlobalFileMgr * getGlobalFileMgr() const
void validate_allowed_file_path(const std::string &file_path, const DataTransferType data_transfer_type, const bool allow_wildcards)
void update_or_drop_column_ids_in_page_headers(const boost::filesystem::path &path, const std::unordered_map< int, int > &column_ids_map, const int32_t table_epoch, const bool drop_not_update)
std::string simple_file_cat(const std::string &archive_path, const std::string &file_name, const std::string &compression, const bool log_failure=true)
static WriteLock getWriteLockForTable(const Catalog_Namespace::Catalog &cat, const std::string &table_name)
void setTableEpoch(const int db_id, const int table_id, const int new_epoch)
std::vector< std::string > getTableDictDirectories(const TableDescriptor *td) const
bool is_page_deleted_with_checkpoint(int32_t table_epoch, int32_t page_epoch, int32_t contingent)
std::list< const ColumnDescriptor * > getAllColumnMetadataForTable(const int tableId, const bool fetchSystemColumns, const bool fetchVirtualColumns, const bool fetchPhysicalColumns) const
Returns a list of pointers to constant ColumnDescriptor structs for all the columns from a particular...
Data_Namespace::MemoryLevel persistenceLevel
std::string s3_session_token
torch::Tensor f(torch::Tensor x, torch::Tensor W_target, torch::Tensor b_target)
static constexpr char const * table_epoch_filename
static void renameAndSymlinkLegacyFiles(const std::string &table_data_dir)
std::string filename(char const *path)
static constexpr int kDumpVersion
void add_data_file_symlinks(const std::string &table_data_dir)
void update_or_drop_column_ids_in_table_files(const int32_t table_epoch, const std::string &temp_data_dir, const std::unordered_map< int, int > &column_ids_map, const bool drop_not_update)
const TableDescriptor * getMetadataForTable(const std::string &tableName, const bool populateFragmenter=true) const
Returns a pointer to a const TableDescriptor struct matching the provided tableName.
std::string s3_access_key
static constexpr char const * table_dumpversion_filename
std::string dumpSchema(const TableDescriptor *td) const
void restoreTable(const Catalog_Namespace::SessionInfo &session, const std::string &table_name, const std::string &archive_path, const std::string &compression, const TableArchiverS3Options &s3_options)
size_t file_size(const int fd)
void dumpTable(const TableDescriptor *td, const std::string &archive_path, const std::string &compression)
constexpr auto kLegacyDataFileExtension
std::unique_ptr< Parser::Stmt > create_stmt_for_query(const std::string &queryStr, const Catalog_Namespace::SessionInfo &session_info)
A selection of helper methods for File I/O.
static ReadLock getReadLockForTable(Catalog_Namespace::Catalog &cat, const std::string &table_name)
bool g_test_rollback_dump_restore
Catalog_Namespace::Catalog * cat_