From 873987793b05fc0d6e9908f5030b2bca145c4e6d Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sun, 28 Oct 2018 01:01:53 +0300 Subject: Add tenant object --- INSTALL | 13 +- clean/buildfile | 1 - clean/clean.cli | 60 +++-- clean/clean.cxx | 573 +++++++++++++++++++++++++++++------------ clean/options-types.hxx | 18 -- clean/types-parsers.cxx | 60 ----- clean/types-parsers.hxx | 28 -- etc/systemd/brep-clean.service | 6 +- libbrep/build-extra.sql | 10 + libbrep/build-package.hxx | 82 +++--- libbrep/build.hxx | 14 +- libbrep/build.xml | 2 + libbrep/package.cxx | 9 + libbrep/package.hxx | 52 +++- libbrep/package.xml | 145 +++++++++++ load/load.cxx | 28 +- migrate/migrate.cxx | 41 ++- mod/mod-build-task.cxx | 2 +- mod/mod-builds.cxx | 59 +++-- 19 files changed, 824 insertions(+), 379 deletions(-) delete mode 100644 clean/options-types.hxx delete mode 100644 clean/types-parsers.cxx delete mode 100644 clean/types-parsers.hxx diff --git a/INSTALL b/INSTALL index 8c25e99..8110cb4 100644 --- a/INSTALL +++ b/INSTALL @@ -394,21 +394,26 @@ our setup. If you still would like to use systemd to run the loader and the cleaner, then you can set it up as a system-wide service which runs the utilities as the brep user/group. Otherwise, a cron job is a natural choice. -Note that the cleaner execution is optional and is only required if the build2 -build bot functionality is enabled (see the build bot documentation for +Note that the builds cleaner execution is optional and is only required if the +build2 build bot functionality is enabled (see the build bot documentation for details). If it is disabled in you setup, then skip the cleaner-related parts in the subsequent subsections. +If the CI request functionality is enabled you most likely will want to +additionally setup the tenants cleanup. + + 8.a Setup Periodic Loader and Cleaner Execution with cron The following crontab entries will execute the loader every five minutes -and the cleaner once a day at midnight: +and the tenants and builds cleaners once a day at midnight: $ crontab -l MAILTO= PATH=/usr/local/bin:/bin:/usr/bin */5 * * * * $HOME/install/bin/brep-load $HOME/config/loadtab -0 0 * * * $HOME/install/bin/brep-clean $HOME/config/buildtab +0 0 * * * $HOME/install/bin/brep-clean tenants 240 +0 0 * * * $HOME/install/bin/brep-clean builds $HOME/config/buildtab ^D Note that here we assume that bpkg (which is executed by brep-load) is in one diff --git a/clean/buildfile b/clean/buildfile index 31a03df..d46a874 100644 --- a/clean/buildfile +++ b/clean/buildfile @@ -20,7 +20,6 @@ if $cli.configured cli.options += -I $src_root --include-with-brackets --include-prefix clean \ --guard-prefix CLEAN --generate-specifier --page-usage print_ --ansi-color \ ---cxx-prologue "#include " \ --long-usage # Include the generated cli files into the distribution and don't remove diff --git a/clean/clean.cli b/clean/clean.cli index 01bc57c..0e36386 100644 --- a/clean/clean.cli +++ b/clean/clean.cli @@ -6,28 +6,41 @@ include ; include ; include ; // uint16_t -include ; - "\section=1" "\name=brep-clean" -"\summary=clean brep build database" +"\summary=clean brep databases" { - " ", + " ", "\h|SYNOPSIS| \c{\b{brep-clean --help}\n \b{brep-clean --version}\n - \b{brep-clean} [] } + \b{brep-clean} [] builds [...]\n + \b{brep-clean} [] tenants } \h|DESCRIPTION| \cb{brep-clean} deletes expired package builds from the brep \cb{build} - database. The build is considered expired if the package version is not - in the \cb{package} database, or the configuration is not listed in the - file, or the timestamp is older than the one specified for - this build's toolchain (see \cb{--stale-timeout}). + database or deletes/archives tenants from the brep \cb{package} database. + + The first form considers a build as expired if the corresponding package + version is not in the \cb{package} database, or the configuration is not + listed in the file, or its age is older than the specified + timeout for this build toolchain. + + Build , if specified, should have the \c{[=]} form. + Specify zero for to make builds for a toolchain to never expire. + Omit (including \cb{=}) to specify the default timeout. It will + apply to all the toolchains that don't have a toolchain-specific timeout. + + The second form considers a tenant as expired if its age is older than the + specified . + + If the \cb{--archive} option is specified, then the tenant is archived + rather than deleted. In this state the tenant packages (and their builds) + are still visible in \cb{brep} but are not (re-)built by build bots. Note that \cb{brep-clean} expects the \cb{build} and \cb{package} database schemas to have already been created using \l{brep-migrate(1)}." @@ -37,49 +50,44 @@ class options { "\h|OPTIONS|" - brep::toolchain_timeouts --stale-timeout + bool --archive { - "[=]", - "Number of days to wait before considering builds for the named toolchain - as stale. Specify zero for to make builds for a toolchain never - expire. Omit (including \cb{=}) to specify the default timeout. - It will apply to all the toolchains that don't have a timeout specified - explicitly. If unspecified, the default timeout is zero (never expire)." + "Archive old tenants." } std::string --db-user { "", - "Build database user name. If not specified, then operating system (login) - name is used." + "Database user name. If not specified, then operating system (login) name + is used." } std::string --db-password { "", - "Build database password. If not specified, then login without password is + "Database password. If not specified, then login without password is expected to work." } - std::string --db-name = "brep_build" + std::string --db-name { "", - "Build database name. If not specified, then \cb{brep_build} is used by - default." + "Database name. If not specified, then \cb{brep_build} is used for the + first form and \cb{brep_package} for the second." } std::string --db-host { "", - "Build database host name, address, or socket. If not specified, then - connect to \cb{localhost} using the operating system-default mechanism - (Unix-domain socket, etc)." + "Database host name, address, or socket. If not specified, then connect to + \cb{localhost} using the operating system-default mechanism (Unix-domain + socket, etc)." } std::uint16_t --db-port = 0 { "", - "Build database port number. If not specified, the default port is used." + "Database port number. If not specified, the default port is used." } std::string --pager // String to allow empty value. diff --git a/clean/clean.cxx b/clean/clean.cxx index 102b165..deba1db 100644 --- a/clean/clean.cxx +++ b/clean/clean.cxx @@ -2,7 +2,9 @@ // copyright : Copyright (c) 2014-2018 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file +#include #include +#include #include #include @@ -17,6 +19,8 @@ #include #include +#include +#include #include #include #include @@ -25,226 +29,459 @@ using namespace std; using namespace bbot; -using namespace brep; using namespace odb::core; -// Operation failed, diagnostics has already been issued. -// -struct failed {}; +namespace brep +{ + // Operation failed, diagnostics has already been issued. + // + struct failed {}; -static const char* help_info ( - " info: run 'brep-clean --help' for more information"); + static const char* help_info ( + " info: run 'brep-clean --help' for more information"); -int -main (int argc, char* argv[]) -try -{ - cli::argv_scanner scan (argc, argv, true); - options ops (scan); + static int + clean_builds (const options&, cli::argv_scanner&, odb::pgsql::database&); - // Version. - // - if (ops.version ()) + static int + clean_tenants (const options&, cli::argv_scanner&, odb::pgsql::database&); + + static int + main (int argc, char* argv[]) + try { - cout << "brep-clean " << BREP_VERSION_ID << endl - << "libbrep " << LIBBREP_VERSION_ID << endl - << "libbbot " << LIBBBOT_VERSION_ID << endl - << "libbpkg " << LIBBPKG_VERSION_ID << endl - << "libbutl " << LIBBUTL_VERSION_ID << endl - << "Copyright (c) 2014-2018 Code Synthesis Ltd" << endl - << "This is free software released under the MIT license." << endl; + cli::argv_scanner scan (argc, argv, true); + options ops (scan); - return 0; - } + // Version. + // + if (ops.version ()) + { + cout << "brep-clean " << BREP_VERSION_ID << endl + << "libbrep " << LIBBREP_VERSION_ID << endl + << "libbbot " << LIBBBOT_VERSION_ID << endl + << "libbpkg " << LIBBPKG_VERSION_ID << endl + << "libbutl " << LIBBUTL_VERSION_ID << endl + << "Copyright (c) 2014-2018 Code Synthesis Ltd" << endl + << "This is free software released under the MIT license." << endl; + + return 0; + } - // Help. - // - if (ops.help ()) - { - butl::pager p ("brep-clean help", - false, - ops.pager_specified () ? &ops.pager () : nullptr, - &ops.pager_option ()); + // Help. + // + if (ops.help ()) + { + butl::pager p ("brep-clean help", + false, + ops.pager_specified () ? &ops.pager () : nullptr, + &ops.pager_option ()); + + print_usage (p.stream ()); - print_usage (p.stream ()); + // If the pager failed, assume it has issued some diagnostics. + // + return p.wait () ? 0 : 1; + } - // If the pager failed, assume it has issued some diagnostics. + // Detect the mode. // - return p.wait () ? 0 : 1; - } + if (!scan.more ()) + { + cerr << "error: 'builds' or 'tenants' is expected" << endl + << help_info << endl; + return 1; + } - const toolchain_timeouts& timeouts (ops.stale_timeout ()); + const string mode (scan.next ()); - auto i (timeouts.find (string ())); - timestamp default_timeout (i != timeouts.end () - ? i->second - : timestamp_nonexistent); + if (mode != "builds" && mode != "tenants") + throw cli::unknown_argument (mode); - // Load configurations names. - // - if (!scan.more ()) - { - cerr << "error: configuration file expected" << endl - << help_info << endl; - return 1; - } + const string db_schema (mode == "builds" ? "build" : "package"); - set configs; - for (auto& c: parse_buildtab (path (scan.next ()))) - configs.emplace (move (c.name)); + const string& db_name (!ops.db_name ().empty () + ? ops.db_name () + : "brep_" + db_schema); - if (scan.more ()) - { - cerr << "error: unexpected argument encountered" << endl - << help_info << endl; - return 1; - } + odb::pgsql::database db ( + ops.db_user (), + ops.db_password (), + db_name, + ops.db_host (), + ops.db_port (), + "options='-c default_transaction_isolation=serializable'"); - odb::pgsql::database build_db ( - ops.db_user (), - ops.db_password (), - ops.db_name (), - ops.db_host (), - ops.db_port (), - "options='-c default_transaction_isolation=serializable'"); + // Prevent several brep-clean/migrate instances from updating build + // database simultaneously. + // + database_lock l (db); - // Prevent several brep-clean/migrate instances from updating build database - // simultaneously. - // - database_lock l (build_db); + // Check that the database schema matches the current one. + // + if (schema_catalog::current_version (db, db_schema) != + db.schema_version (db_schema)) + { + cerr << "error: " << db_schema << " database schema differs from the " + << "current one" << endl + << " info: use brep-migrate to migrate the database" << endl; + return 1; + } - // Check that the build database schema matches the current one. + return mode == "builds" + ? clean_builds (ops, scan, db) + : clean_tenants (ops, scan, db); + } + catch (const database_locked&) + { + cerr << "brep-clean or brep-migrate is running" << endl; + return 2; + } + catch (const recoverable& e) + { + cerr << "recoverable database error: " << e << endl; + return 3; + } + catch (const cli::exception& e) + { + cerr << "error: " << e << endl << help_info << endl; + return 1; + } + catch (const failed&) + { + return 1; // Diagnostics has already been issued. + } + // Fully qualified to avoid ambiguity with odb exception. // - const string bs ("build"); - if (schema_catalog::current_version (build_db, bs) != - build_db.schema_version (bs)) + catch (const std::exception& e) { - cerr << "error: build database schema differs from the current one" - << endl << " info: use brep-migrate to migrate the database" << endl; + cerr << "error: " << e << endl; return 1; } - // Prepare the build prepared query. + // Convert timeout duration into the time point. Return + // timestamp_nonexistent (never expire) for zero argument. Return nullopt if + // the argument is invalid. // - // Query package builds in chunks in order not to hold locks for too long. - // Sort the result by package version to minimize number of queries to the - // package database. - // - using bld_query = query; - using prep_bld_query = prepared_query; + static optional + timeout (const string& tm) + { + char* e (nullptr); + uint64_t t (strtoull (tm.c_str (), &e, 10)); - size_t offset (0); - bld_query bq ("ORDER BY" + - bld_query::id.package.tenant + "," + - bld_query::id.package.name + - order_by_version_desc (bld_query::id.package.version, false) + - "OFFSET" + bld_query::_ref (offset) + "LIMIT 100"); + if (*e != '\0' || tm.empty ()) + return nullopt; - connection_ptr conn (build_db.connection ()); + if (t == 0) + return timestamp_nonexistent; - prep_bld_query bld_prep_query ( - conn->prepare_query ("build-query", bq)); + return system_clock::now () - chrono::hours (t); + } - // Prepare the package version query. - // - // Query buildable packages every time the new package name is encountered - // during iterating over the package builds. Such a query will be made once - // per package name due to the builds query sorting criteria (see above). - // - using pkg_query = query; - using prep_pkg_query = prepared_query; + static int + clean_builds (const options& ops, + cli::argv_scanner& scan, + odb::pgsql::database& db) + { + // Load configurations names. + // + if (!scan.more ()) + { + cerr << "error: configuration file expected" << endl + << help_info << endl; + return 1; + } - package_name pkg_name; - set package_versions; + path cp; - pkg_query pq ( - pkg_query::build_package::id.name == pkg_query::_ref (pkg_name)); + try + { + cp = path (scan.next ()); + } + catch (const invalid_path& e) + { + cerr << "error: configuration file expected instead of '" << e.path + << "'" << endl + << help_info << endl; + return 1; + } - prep_pkg_query pkg_prep_query ( - conn->prepare_query ("package-query", pq)); + set configs; - for (bool ne (true); ne; ) - { - transaction t (conn->begin ()); + try + { + for (auto& c: parse_buildtab (cp)) + configs.emplace (move (c.name)); + } + catch (const io_error& e) + { + cerr << "error: unable to read '" << cp << "': " << e << endl; + return 1; + } - // Query builds. + // Parse timestamps. // - auto builds (bld_prep_query.execute ()); + map timeouts; // Toolchain timeouts. + timestamp default_timeout; // timestamp_nonexistent - if ((ne = !builds.empty ())) + while (scan.more ()) { - for (const auto& b: builds) + string a (scan.next ()); + + string tc; + optional to; + + size_t p (a.find ('=')); + + if (p == string::npos) + to = timeout (a); + else if (p > 0) // Note: toolchain name can't be empty. { - auto i (timeouts.find (b.toolchain_name)); + tc = string (a, 0, p); + to = timeout (string (a, p + 1)); + } - timestamp et (i != timeouts.end () - ? i->second - : default_timeout); + // Note that the default timeout can't be zero. + // + if (!to || (*to == timestamp_nonexistent && tc.empty ())) + { + cerr << "error: timeout expected instead of '" << a << "'" << endl + << help_info << endl; + return 1; + } - bool cleanup ( - // Check that the build is not stale. - // - b.timestamp <= et || + if (tc.empty ()) + default_timeout = *to; - // Check that the build configuration is still present. - // - // Note that we unable to detect configuration changes and rely on - // periodic rebuilds to take care of that. - // - configs.find (b.configuration) == configs.end ()); + timeouts[move (tc)] = move (*to); + } - // Check that the build package still exists. - // - if (!cleanup) + // Prepare the build prepared query. + // + // Query package builds in chunks in order not to hold locks for too long. + // Sort the result by package version to minimize number of queries to the + // package database. + // + using bld_query = query; + using prep_bld_query = prepared_query; + + size_t offset (0); + bld_query bq ("ORDER BY" + + bld_query::id.package.tenant + "," + + bld_query::id.package.name + + order_by_version_desc (bld_query::id.package.version, + false) + + "OFFSET" + bld_query::_ref (offset) + "LIMIT 100"); + + connection_ptr conn (db.connection ()); + + prep_bld_query bld_prep_query ( + conn->prepare_query ("build-query", bq)); + + // Prepare the package version query. + // + // Query buildable packages every time the new tenant or package name is + // encountered during iterating over the package builds. Such a query will + // be made once per tenant package name due to the builds query sorting + // criteria (see above). + // + using pkg_query = query; + using prep_pkg_query = prepared_query; + + string tnt; + package_name pkg_name; + set package_versions; + + pkg_query pq ( + pkg_query::build_package::id.tenant == pkg_query::_ref (tnt) && + pkg_query::build_package::id.name == pkg_query::_ref (pkg_name)); + + prep_pkg_query pkg_prep_query ( + conn->prepare_query ("package-query", pq)); + + for (bool ne (true); ne; ) + { + transaction t (conn->begin ()); + + // Query builds. + // + auto builds (bld_prep_query.execute ()); + + if ((ne = !builds.empty ())) + { + for (const auto& b: builds) { - if (pkg_name != b.package_name) - { - pkg_name = b.package_name; - package_versions.clear (); + auto i (timeouts.find (b.toolchain_name)); - for (auto& p: pkg_prep_query.execute ()) - package_versions.emplace (move (p.version)); + timestamp et (i != timeouts.end () + ? i->second + : default_timeout); + + bool cleanup ( + // Check that the build is not stale. + // + b.timestamp <= et || + + // Check that the build configuration is still present. + // + // Note that we unable to detect configuration changes and rely on + // periodic rebuilds to take care of that. + // + configs.find (b.configuration) == configs.end ()); + + // Check that the build package still exists. + // + if (!cleanup) + { + if (tnt != b.tenant || pkg_name != b.package_name) + { + tnt = b.tenant; + pkg_name = b.package_name; + package_versions.clear (); + + for (auto& p: pkg_prep_query.execute ()) + package_versions.emplace (move (p.version)); + } + + cleanup = package_versions.find (b.package_version) == + package_versions.end (); } - cleanup = package_versions.find (b.package_version) == - package_versions.end (); + if (cleanup) + db.erase (b); + else + ++offset; } - - if (cleanup) - build_db.erase (b); - else - ++offset; } + + t.commit (); } - t.commit (); + return 0; } - return 0; -} -catch (const database_locked&) -{ - cerr << "brep-clean or brep-migrate is running" << endl; - return 2; -} -catch (const recoverable& e) -{ - cerr << "recoverable database error: " << e << endl; - return 3; -} -catch (const cli::exception& e) -{ - cerr << "error: " << e << endl << help_info << endl; - return 1; -} -catch (const failed&) -{ - return 1; // Diagnostics has already been issued. + static int + clean_tenants (const options& ops, + cli::argv_scanner& scan, + odb::pgsql::database& db) + { + if (!scan.more ()) + { + cerr << "error: timeout expected" << endl + << help_info << endl; + return 1; + } + + string a (scan.next ()); + optional to (timeout (a)); + + // Note that the timeout can't be zero. + // + if (!to || *to == timestamp_nonexistent) + { + cerr << "error: timeout expected instead of '" << a << "'" << endl + << help_info << endl; + return 1; + } + + if (scan.more ()) + { + cerr << "error: unexpected argument encountered" << endl + << help_info << endl; + return 1; + } + + uint64_t ns ( + chrono::duration_cast ( + to->time_since_epoch ()).count ()); + + // Query tenants in chunks in order not to hold locks for too long. + // + connection_ptr conn (db.connection ()); + + // Archive (rather then delete) old tenants, if requested. + // + if (ops.archive ()) + { + using query = query; + using pquery = prepared_query; + + query q ((query::creation_timestamp < ns && !query::archived) + + "LIMIT 100"); + + pquery pq (conn->prepare_query ("tenant-query", q)); + + for (bool ne (true); ne; ) + { + transaction t (conn->begin ()); + + auto tenants (pq.execute ()); + if ((ne = !tenants.empty ())) + { + for (auto& t: tenants) + { + t.archived = true; + db.update (t); + } + } + + t.commit (); + } + + return 0; + } + + // Delete old tenants. + // + // Note that we don't delete dangling builds for the deleted packages. + // Doing so would require to operate on two databases, complicating the + // code and the utility interface. Note that dangling builds are never + // considered in the web interface and are always deleted with the + // 'brep-clean builds' command. + // + using query = query; + using pquery = prepared_query; + + query q ((query::creation_timestamp < ns) + "LIMIT 100"); + pquery pq (conn->prepare_query ("tenant-id-query", q)); + + for (bool ne (true); ne; ) + { + transaction t (conn->begin ()); + + auto tenant_ids (pq.execute ()); + if ((ne = !tenant_ids.empty ())) + { + // Cache tenant ids and erase packages, repositories, and tenants at + // once. + // + strings tids; + tids.reserve (tenant_ids.size ()); + + for (auto& tid: tenant_ids) + tids.push_back (move (tid.value)); + + using odb::query; + + db.erase_query ( + query::id.tenant.in_range (tids.begin (), tids.end ())); + + db.erase_query ( + query::id.tenant.in_range (tids.begin (), tids.end ())); + + db.erase_query ( + query::id.in_range (tids.begin (), tids.end ())); + } + + t.commit (); + } + + return 0; + } } -// Fully qualified to avoid ambiguity with odb exception. -// -catch (const std::exception& e) + +int +main (int argc, char* argv[]) { - cerr << "error: " << e << endl; - return 1; + return brep::main (argc, argv); } diff --git a/clean/options-types.hxx b/clean/options-types.hxx deleted file mode 100644 index 7190396..0000000 --- a/clean/options-types.hxx +++ /dev/null @@ -1,18 +0,0 @@ -// file : clean/options-types.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef CLEAN_OPTIONS_TYPES_HXX -#define CLEAN_OPTIONS_TYPES_HXX - -#include - -#include -#include - -namespace brep -{ - struct toolchain_timeouts: std::map {}; -} - -#endif // CLEAN_OPTIONS_TYPES_HXX diff --git a/clean/types-parsers.cxx b/clean/types-parsers.cxx deleted file mode 100644 index 31f3a8d..0000000 --- a/clean/types-parsers.cxx +++ /dev/null @@ -1,60 +0,0 @@ -// file : clean/types-parsers.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include -#include // strtoull() - -#include - -#include -#include // cli namespace - -using namespace std; -using namespace brep; - -namespace cli -{ - void parser:: - parse (toolchain_timeouts& x, bool& xs, scanner& s) - { - const char* o (s.next ()); - - if (!s.more ()) - throw missing_value (o); - - string ov (s.next ()); - size_t p (ov.find ('=')); - - timestamp now (system_clock::now ()); - - // Convert timeout duration into the time point. - // - auto timeout = [o, &ov, &now] (const string& tm) -> timestamp - { - char* e (nullptr); - uint64_t t (strtoull (tm.c_str (), &e, 10)); - - if (*e != '\0' || tm.empty ()) - throw invalid_value (o, ov); - - if (t == 0) - return timestamp_nonexistent; - - return now - chrono::duration> (t); - }; - - if (p == string::npos) - x[string ()] = timeout (ov); // Default timeout. - else - { - string k (ov, 0, p); - if (k.empty ()) - throw invalid_value (o, ov); - - x[k] = timeout (string (ov, p + 1)); - } - - xs = true; - } -} diff --git a/clean/types-parsers.hxx b/clean/types-parsers.hxx deleted file mode 100644 index fe7c77c..0000000 --- a/clean/types-parsers.hxx +++ /dev/null @@ -1,28 +0,0 @@ -// file : clean/types-parsers.hxx -*- C++ -*- -// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -// CLI parsers, included into the generated source files. -// - -#ifndef CLEAN_TYPES_PARSERS_HXX -#define CLEAN_TYPES_PARSERS_HXX - -#include - -namespace cli -{ - class scanner; - - template - struct parser; - - template <> - struct parser - { - static void - parse (brep::toolchain_timeouts&, bool&, scanner&); - }; -} - -#endif // CLEAN_TYPES_PARSERS_HXX diff --git a/etc/systemd/brep-clean.service b/etc/systemd/brep-clean.service index 0099d62..739a54a 100644 --- a/etc/systemd/brep-clean.service +++ b/etc/systemd/brep-clean.service @@ -5,7 +5,11 @@ Description=brep build database cleaner service Type=oneshot #User=brep #Group=brep -ExecStart=/home/brep/install/bin/brep-clean /home/brep/config/buildtab + +# Run both tenants and builds cleaners if CI request functionality is enabled. +# +#ExecStart=/home/brep/install/bin/brep-clean tenants 240 +ExecStart=/home/brep/install/bin/brep-clean builds /home/brep/config/buildtab [Install] WantedBy=default.target diff --git a/libbrep/build-extra.sql b/libbrep/build-extra.sql index 35ba361..6c0d6ef 100644 --- a/libbrep/build-extra.sql +++ b/libbrep/build-extra.sql @@ -12,6 +12,16 @@ DROP FOREIGN TABLE IF EXISTS build_package; DROP FOREIGN TABLE IF EXISTS build_repository; +DROP FOREIGN TABLE IF EXISTS build_tenant; + +-- The foreign table for build_tenant object. +-- +-- +CREATE FOREIGN TABLE build_tenant ( + id TEXT NOT NULL, + archived BOOLEAN NOT NULL) +SERVER package_server OPTIONS (table_name 'tenant'); + -- The foreign table for build_repository object. -- -- diff --git a/libbrep/build-package.hxx b/libbrep/build-package.hxx index 0fed500..0d6b5bf 100644 --- a/libbrep/build-package.hxx +++ b/libbrep/build-package.hxx @@ -23,7 +23,26 @@ namespace brep // The mapping is established in build-extra.sql. We also explicitly mark // non-primary key foreign-mapped members in the source object. // - // Foreign object that is mapped to a subset of repository object. + // Foreign object that is mapped to a subset of the tenant object. + // + #pragma db object table("build_tenant") pointer(shared_ptr) readonly + class build_tenant + { + public: + string id; + + bool archived; + + // Database mapping. + // + #pragma db member(id) id + + private: + friend class odb::access; + build_tenant () = default; + }; + + // Foreign object that is mapped to a subset of the repository object. // #pragma db object table("build_repository") pointer(shared_ptr) readonly class build_repository @@ -61,7 +80,7 @@ namespace brep optional target; }; - // Foreign object that is mapped to a subset of package object. + // Foreign object that is mapped to a subset of the package object. // #pragma db object table("build_package") pointer(shared_ptr) readonly class build_package @@ -92,14 +111,15 @@ namespace brep // Note that ADL can't find the equal operator, so we use the function call // notation. // - #pragma db view \ - object(build_package) \ - object(build_repository inner: \ - brep::operator== (build_package::internal_repository, \ - build_repository::id) && \ - brep::compare_version_ne (build_package::id.version, \ - brep::wildcard_version, \ - false)) + #pragma db view \ + object(build_package) \ + object(build_repository inner: \ + brep::operator== (build_package::internal_repository, \ + build_repository::id) && \ + brep::compare_version_ne (build_package::id.version, \ + brep::wildcard_version, \ + false)) \ + object(build_tenant: build_package::id.tenant == build_tenant::id) struct buildable_package { package_id id; @@ -110,14 +130,15 @@ namespace brep #pragma db member(version) set(this.version.init (this.id.version, (?))) }; - #pragma db view \ - object(build_package) \ - object(build_repository inner: \ - brep::operator== (build_package::internal_repository, \ - build_repository::id) && \ - brep::compare_version_ne (build_package::id.version, \ - brep::wildcard_version, \ - false)) + #pragma db view \ + object(build_package) \ + object(build_repository inner: \ + brep::operator== (build_package::internal_repository, \ + build_repository::id) && \ + brep::compare_version_ne (build_package::id.version, \ + brep::wildcard_version, \ + false)) \ + object(build_tenant: build_package::id.tenant == build_tenant::id) struct buildable_package_count { size_t result; @@ -133,18 +154,19 @@ namespace brep // (internal and non-stub) packages can have such constraints, so there is // no need for additional checks. // - #pragma db view \ - table("build_package_constraints" = "c") \ - object(build_package = package inner: \ - "c.exclusion AND " \ - "c.tenant = " + package::id.tenant + "AND" + \ - "c.name = " + package::id.name + "AND" + \ - "c.version_epoch = " + package::id.version.epoch + "AND" + \ - "c.version_canonical_upstream = " + \ - package::id.version.canonical_upstream + "AND" + \ - "c.version_canonical_release = " + \ - package::id.version.canonical_release + "AND" + \ - "c.version_revision = " + package::id.version.revision) \ + #pragma db view \ + table("build_package_constraints" = "c") \ + object(build_package inner: \ + "c.exclusion AND " \ + "c.tenant = " + build_package::id.tenant + "AND" + \ + "c.name = " + build_package::id.name + "AND" + \ + "c.version_epoch = " + build_package::id.version.epoch + "AND" + \ + "c.version_canonical_upstream = " + \ + build_package::id.version.canonical_upstream + "AND" + \ + "c.version_canonical_release = " + \ + build_package::id.version.canonical_release + "AND" + \ + "c.version_revision = " + build_package::id.version.revision) \ + object(build_tenant: build_package::id.tenant == build_tenant::id) \ query(distinct) struct build_constrained_package { diff --git a/libbrep/build.hxx b/libbrep/build.hxx index fc314eb..279c1d7 100644 --- a/libbrep/build.hxx +++ b/libbrep/build.hxx @@ -26,7 +26,7 @@ // #define LIBBREP_BUILD_SCHEMA_VERSION_BASE 4 -#pragma db model version(LIBBREP_BUILD_SCHEMA_VERSION_BASE, 4, closed) +#pragma db model version(LIBBREP_BUILD_SCHEMA_VERSION_BASE, 5, open) // We have to keep these mappings at the global scope instead of inside // the brep namespace because they need to be also effective in the @@ -251,7 +251,11 @@ namespace brep } }; - #pragma db view object(build) query(distinct) + #pragma db view object(build) \ + object(build_package inner: \ + brep::operator== (build::id.package, build_package::id) && \ + build_package::internal_repository.canonical_name.is_not_null ()) \ + query(distinct) struct toolchain { string name; @@ -297,7 +301,8 @@ namespace brep object(build) \ object(build_package inner: \ brep::operator== (build::id.package, build_package::id) && \ - build_package::internal_repository.canonical_name.is_not_null ()) + build_package::internal_repository.canonical_name.is_not_null ()) \ + object(build_tenant: build_package::id.tenant == build_tenant::id) struct package_build { shared_ptr build; @@ -307,7 +312,8 @@ namespace brep object(build) \ object(build_package inner: \ brep::operator== (build::id.package, build_package::id) && \ - build_package::internal_repository.canonical_name.is_not_null ()) + build_package::internal_repository.canonical_name.is_not_null ()) \ + object(build_tenant: build_package::id.tenant == build_tenant::id) struct package_build_count { size_t result; diff --git a/libbrep/build.xml b/libbrep/build.xml index 0116374..13b47a6 100644 --- a/libbrep/build.xml +++ b/libbrep/build.xml @@ -1,4 +1,6 @@ + + diff --git a/libbrep/package.cxx b/libbrep/package.cxx index e3921fe..d10186c 100644 --- a/libbrep/package.cxx +++ b/libbrep/package.cxx @@ -38,6 +38,15 @@ namespace brep return !(x == y); } + // tenant + // + tenant:: + tenant (string i) + : id (move (i)), + creation_timestamp (timestamp::clock::now ()) + { + } + // package // package:: diff --git a/libbrep/package.hxx b/libbrep/package.hxx index af4581c..fb44bf3 100644 --- a/libbrep/package.hxx +++ b/libbrep/package.hxx @@ -21,7 +21,7 @@ // #define LIBBREP_PACKAGE_SCHEMA_VERSION_BASE 7 -#pragma db model version(LIBBREP_PACKAGE_SCHEMA_VERSION_BASE, 8, open) +#pragma db model version(LIBBREP_PACKAGE_SCHEMA_VERSION_BASE, 9, open) namespace brep { @@ -185,6 +185,42 @@ namespace brep }; #pragma db object pointer(shared_ptr) session + class tenant + { + public: + // Create the tenant object with the timestamp set to now and the archived + // flag set to false. + // + explicit + tenant (string id); + + string id; + + timestamp creation_timestamp; + bool archived = false; // Note: foreign-mapped in build. + + // Database mapping. + // + #pragma db member(id) id + + private: + friend class odb::access; + tenant () = default; + }; + + #pragma db view object(tenant) + struct tenant_id + { + #pragma db column("id") + string value; + }; + + // Tweak repository_id mapping to include a constraint (this only affects + // the database schema). + // + #pragma db member(repository_id::tenant) points_to(tenant) + + #pragma db object pointer(shared_ptr) session class repository { public: @@ -269,6 +305,15 @@ namespace brep repository (): tenant (id.tenant), canonical_name (id.canonical_name) {} }; + // Used for data migration (see migrate/migrate.cxx for details). + // + #pragma db view object(repository) query(distinct) + struct repository_tenant + { + #pragma db column("tenant") + string id; + }; + // The 'to' expression calls the PostgreSQL to_tsvector(weighted_text) // function overload (package-extra.sql). Since we are only interested // in "write-only" members of this type, make the 'from' expression @@ -289,6 +334,11 @@ namespace brep string d; }; + // Tweak package_id mapping to include a constraint (this only affects the + // database schema). + // + #pragma db member(package_id::tenant) points_to(tenant) + #pragma db object pointer(shared_ptr) session class package { diff --git a/libbrep/package.xml b/libbrep/package.xml index 07c1be7..2b9ebab 100644 --- a/libbrep/package.xml +++ b/libbrep/package.xml @@ -1,4 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/load/load.cxx b/load/load.cxx index 24a3bf3..3036daf 100644 --- a/load/load.cxx +++ b/load/load.cxx @@ -1101,9 +1101,9 @@ try // By default the tenant is empty and assumes a single-tenant mode. Let's // require the specified tenant to be non-empty. // - const string& tenant (ops.tenant ()); + const string& tnt (ops.tenant ()); - if (ops.tenant_specified () && tenant.empty ()) + if (ops.tenant_specified () && tnt.empty ()) { cerr << "error: empty tenant" << endl << help_info << endl; @@ -1140,7 +1140,7 @@ try // internal_repositories irs (load_repositories (path (argv[1]))); - if (ops.force () || changed (tenant, irs, db)) + if (ops.force () || changed (tnt, irs, db)) { // Rebuild repositories persistent state from scratch. // @@ -1150,22 +1150,30 @@ try // multiple tenants). Otherwise, cleanup the specified and the empty // tenants only. // - if (tenant.empty ()) // Single-tenant mode. + if (tnt.empty ()) // Single-tenant mode. { db.erase_query (); db.erase_query (); + db.erase_query (); } else // Multi-tenant mode. { - cstrings ts ({tenant.c_str (), ""}); + cstrings ts ({tnt.c_str (), ""}); db.erase_query ( query::id.tenant.in_range (ts.begin (), ts.end ())); db.erase_query ( query::id.tenant.in_range (ts.begin (), ts.end ())); + + db.erase_query ( + query::id.in_range (ts.begin (), ts.end ())); } + // Persist the tenant. + // + db.persist (tenant (tnt)); + // On the first pass over the internal repositories we load their // certificate information and packages. // @@ -1181,7 +1189,7 @@ try ir.fingerprint); shared_ptr r ( - make_shared (tenant, + make_shared (tnt, ir.location, move (ir.display_name), move (ir.cache_location), @@ -1198,8 +1206,8 @@ try for (const auto& ir: irs) { shared_ptr r ( - db.load (repository_id (tenant, - ir.location.canonical_name ()))); + db.load ( + repository_id (tnt, ir.location.canonical_name ()))); load_repositories (r, db, ops.shallow ()); } @@ -1213,7 +1221,7 @@ try for (auto& p: db.query ( - query::id.tenant == tenant && + query::id.tenant == tnt && query::internal_repository.canonical_name.is_not_null ())) resolve_dependencies (p, db); @@ -1222,7 +1230,7 @@ try package_ids chain; for (const auto& p: db.query ( - query::id.tenant == tenant && + query::id.tenant == tnt && query::internal_repository.canonical_name.is_not_null ())) detect_dependency_cycle (p.id, chain, db); } diff --git a/migrate/migrate.cxx b/migrate/migrate.cxx index d84e282..d33578a 100644 --- a/migrate/migrate.cxx +++ b/migrate/migrate.cxx @@ -15,6 +15,8 @@ #include +#include +#include #include #include @@ -203,6 +205,31 @@ create (database& db, bool extra_only) const db.execute (s); } +// Register the data migration functions for the package database schema. +// +template +using package_migration_entry_base = + odb::data_migration_entry; + +template +struct package_migration_entry: package_migration_entry_base +{ + package_migration_entry (void (*f) (odb::database& db)) + : package_migration_entry_base (f, "package") {} +}; + +// Don't forget to drop the repository_tenant view when stop supporting +// data migration for this schema version. +// +static const package_migration_entry<9> +package_migrate_v9 ([] (odb::database& db) +{ + // Add tenant objects. + // + for (const auto& t: db.query ()) + db.persist (tenant (t.id)); +}); + // main() function // int @@ -284,6 +311,15 @@ try // database_lock l (db); + // Currently we don't support data migration for the manual database scheme + // migration. + // + if (db.schema_migration (db_schema)) + { + cerr << "error: manual database schema migration is not supported" << endl; + throw failed (); + } + // Need to obtain schema version out of the transaction. If the // schema_version table does not exist, the SQL query fails, which makes the // transaction useless as all consequitive queries in that transaction will @@ -363,11 +399,6 @@ try // s.drop (db, true /* extra_only */); - // Register the data migration functions. - // - // static const data_migration_entry<2, LIBBREP_XXX_SCHEMA_VERSION_BASE> - // migrate_v2_entry (&migrate_v2); - // schema_catalog::migrate (db, 0, db_schema); s.create (db, true /* extra_only */); diff --git a/mod/mod-build-task.cxx b/mod/mod-build-task.cxx index 77652ce..d3efb41 100644 --- a/mod/mod-build-task.cxx +++ b/mod/mod-build-task.cxx @@ -328,7 +328,7 @@ handle (request& rq, response& rs) using pkg_query = query; using prep_pkg_query = prepared_query; - pkg_query pq (true); + pkg_query pq (!pkg_query::build_tenant::archived); // Filter by repositories canonical names (if requested). // diff --git a/mod/mod-builds.cxx b/mod/mod-builds.cxx index a55b6f9..4d03987 100644 --- a/mod/mod-builds.cxx +++ b/mod/mod-builds.cxx @@ -94,7 +94,8 @@ template static inline query build_query (const brep::cstrings& configs, const brep::params::builds& params, - const brep::optional& tenant) + const brep::optional& tenant, + const brep::optional& archived) { using namespace brep; using query = query; @@ -109,6 +110,9 @@ build_query (const brep::cstrings& configs, if (tenant) q = q && pid.tenant == *tenant; + if (archived) + q = q && query::build_tenant::archived == *archived; + // Note that there is no error reported if the filter parameters parsing // fails. Instead, it is considered that no package builds match such a // query. @@ -205,18 +209,23 @@ build_query (const brep::cstrings& configs, return q; } -template ::build_package> +template static inline query package_query (const brep::params::builds& params, - const brep::optional& tenant) + const brep::optional& tenant, + const brep::optional& archived) { using namespace brep; using query = query; + using qp = typename query::build_package; query q (true); if (tenant) - q = q && P::id.tenant == *tenant; + q = q && qp::id.tenant == *tenant; + + if (archived) + q = q && query::build_tenant::archived == *archived; // Note that there is no error reported if the filter parameters parsing // fails. Instead, it is considered that no packages match such a query. @@ -226,13 +235,13 @@ package_query (const brep::params::builds& params, // Package name. // if (!params.name ().empty ()) - q = q && P::id.name.like ( + q = q && qp::id.name.like ( package_name (transform (params.name ()), package_name::raw_string)); // Package version. // if (!params.version ().empty () && params.version () != "*") - q = q && compare_version_eq (P::id.version, + q = q && compare_version_eq (qp::id.version, version (params.version ()), // May throw. true); } @@ -344,9 +353,9 @@ handle (request& rq, response& rs) toolchains r; for (auto& t: build_db_->query ( - (tn ? query::id.package.tenant == *tn : query (true)) + - "ORDER BY" + query::toolchain_name + - order_by_version_desc (query::id.toolchain_version, false))) + (tn ? query::build::id.package.tenant == *tn : query (true)) + + "ORDER BY" + query::build::toolchain_name + + order_by_version_desc (query::build::id.toolchain_version, false))) r.emplace_back (move (t.name), move (t.version)); return r; @@ -440,7 +449,8 @@ handle (request& rq, response& rs) transaction t (build_db_->begin ()); count = build_db_->query_value ( - build_query (*build_conf_names_, params, tn)); + build_query ( + *build_conf_names_, params, tn, nullopt /* archived */)); // Print the filter form. // @@ -455,7 +465,8 @@ handle (request& rq, response& rs) // s << DIV; for (auto& pb: build_db_->query ( - build_query (*build_conf_names_, params, tn) + + build_query ( + *build_conf_names_, params, tn, nullopt /* archived */) + "ORDER BY" + query::build::timestamp + "DESC" + "OFFSET" + to_string (page * page_configs) + "LIMIT" + to_string (page_configs))) @@ -608,12 +619,12 @@ handle (request& rq, response& rs) size_t nmax ( config_toolchains.size () * build_db_->query_value ( - package_query (params, tn))); + package_query ( + params, tn, false /* archived */))); size_t ncur = build_db_->query_value ( - build_query (*build_conf_names_, - bld_params, - tn)); + build_query ( + *build_conf_names_, bld_params, tn, false /* archived */)); // From now we will be using specific package name and version for each // build database query. @@ -645,7 +656,8 @@ handle (request& rq, response& rs) // build_query (cstrings () /* configs */, bld_params, - nullopt /* tenant */)); + nullopt /* tenant */, + false /* archived */)); prep_bld_query bld_prep_query ( build_db_->prepare_query ( @@ -658,9 +670,9 @@ handle (request& rq, response& rs) // caching will not be easy as the cached values depend on the filter // form parameters. // - using query = query; - query q (package_query (params, - tn)); + query q ( + package_query ( + params, tn, false /* archived */)); for (const auto& p: build_db_->query (q)) { @@ -715,7 +727,8 @@ handle (request& rq, response& rs) using pkg_query = query; using prep_pkg_query = prepared_query; - pkg_query pq (package_query (params, tn)); + pkg_query pq ( + package_query (params, tn, false /* archived */)); // Specify the portion. Note that we will still be querying packages in // chunks, not to hold locks for too long. @@ -753,7 +766,8 @@ handle (request& rq, response& rs) // build_query (*build_conf_names_, bld_params, - nullopt /* tenant */)); + nullopt /* tenant */, + false /* archived */)); prep_bld_query bld_prep_query ( conn->prepare_query ("mod-builds-build-query", bq)); @@ -767,7 +781,8 @@ handle (request& rq, response& rs) using prep_ctr_query = prepared_query; ctr_query cq ( - package_id_eq (ctr_query::id, id)); + package_id_eq ( + ctr_query::build_package::id, id)); prep_ctr_query ctr_prep_query ( conn->prepare_query ( -- cgit v1.1