// file : mod/mod-build-task.cxx -*- C++ -*- // copyright : Copyright (c) 2014-2018 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file #include #include #include #include #include #include #include #include // compare_c_string #include #include // nullfd #include // path_match() #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace butl; using namespace bbot; using namespace brep::cli; using namespace odb::core; // While currently the user-defined copy constructor is not required (we don't // need to deep copy nullptr's), it is a good idea to keep the placeholder // ready for less trivial cases. // brep::build_task:: build_task (const build_task& r) : database_module (r), options_ (r.initialized_ ? r.options_ : nullptr) { } void brep::build_task:: init (scanner& s) { HANDLER_DIAG; options_ = make_shared ( s, unknown_mode::fail, unknown_mode::fail); if (options_->build_config_specified ()) { database_module::init (static_cast (*options_), static_cast (*options_), options_->build_db_retry ()); // Check that the database 'build' schema matches the current one. It's // enough to perform the check in just a single module implementation // (more details in the comment in package_search::init()). // const string ds ("build"); if (schema_catalog::current_version (*build_db_, ds) != build_db_->schema_version (ds)) fail << "database 'build' schema differs from the current one (module " << BREP_VERSION_ID << ")"; } if (options_->root ().empty ()) options_->root (dir_path ("/")); } bool brep::build_task:: handle (request& rq, response& rs) { HANDLER_DIAG; if (build_db_ == nullptr) throw invalid_request (501, "not implemented"); params::build_task params; try { // Note that we expect the task request manifest to be posted and so // consider parameters from the URL only. // name_value_scanner s (rq.parameters (0 /* limit */, true /* url_only */)); params = params::build_task (s, unknown_mode::fail, unknown_mode::fail); } catch (const cli::exception& e) { throw invalid_request (400, e.what ()); } task_request_manifest tqm; try { // We fully cache the request content to be able to retry the request // handling if odb::recoverable is thrown (see database-module.cxx for // details). // size_t limit (options_->build_task_request_max_size ()); manifest_parser p (rq.content (limit, limit), "task_request_manifest"); tqm = task_request_manifest (p); } catch (const manifest_parsing& e) { throw invalid_request (400, e.what ()); } // Obtain the agent's public key fingerprint if requested. If the fingerprint // is requested but is not present in the request or is unknown, then respond // with 401 HTTP code (unauthorized). // optional agent_fp; if (bot_agent_keys_ != nullptr) { if (!tqm.fingerprint || bot_agent_keys_->find (*tqm.fingerprint) == bot_agent_keys_->end ()) throw invalid_request (401, "unauthorized"); agent_fp = move (tqm.fingerprint); } task_response_manifest tsm; // Map build configurations to machines that are capable of building them. // The first matching machine is selected for each configuration. Also // create the configuration name list for use in database queries. // struct config_machine { const build_config* config; machine_header_manifest* machine; }; using config_machines = map; cstrings cfg_names; config_machines cfg_machines; for (const auto& c: *build_conf_) { for (auto& m: tqm.machines) { if (path_match (c.machine_pattern, m.name) && cfg_machines.insert ( make_pair (c.name.c_str (), config_machine ({&c, &m}))).second) cfg_names.push_back (c.name.c_str ()); } } // Go through packages until we find one that has no build configuration // present in the database, or is in the building state but expired // (collectively called unbuilt). If such a package configuration is found // then put it into the building state, set the current timestamp and respond // with the task for building this package configuration. // // While trying to find a non-built package configuration we will also // collect the list of the built package configurations which it's time to // rebuild. So if no unbuilt package is found, we will pickup one to // rebuild. The rebuild preference is given in the following order: the // greater force state, the greater overall status, the lower timestamp. // if (!cfg_machines.empty ()) { vector> rebuilds; // Create the task response manifest. The package must have the internal // repository loaded. // auto task = [this] (shared_ptr&& b, shared_ptr&& p, const config_machine& cm) -> task_response_manifest { uint64_t ts ( chrono::duration_cast ( b->timestamp.time_since_epoch ()).count ()); string session (b->tenant + '/' + b->package_name.string () + '/' + b->package_version.string () + '/' + b->configuration + '/' + b->toolchain_version.string () + '/' + to_string (ts)); string result_url (options_->host () + tenant_dir (options_->root (), b->tenant).string () + "?build-result"); lazy_shared_ptr r (p->internal_repository); strings fp; if (r->certificate_fingerprint) fp.emplace_back (move (*r->certificate_fingerprint)); task_manifest task (move (b->package_name), move (b->package_version), move (r->location), move (fp), cm.machine->name, cm.config->target, cm.config->vars, cm.config->warning_regexes); return task_response_manifest (move (session), move (b->agent_challenge), move (result_url), move (task)); }; // Calculate the build (building state) or rebuild (built state) expiration // time for package configurations // timestamp now (system_clock::now ()); auto expiration = [&now] (size_t timeout) -> timestamp { return now - chrono::seconds (timeout); }; auto expiration_ns = [&expiration] (size_t timeout) -> uint64_t { return chrono::duration_cast ( expiration (timeout).time_since_epoch ()).count (); }; uint64_t normal_result_expiration_ns ( expiration_ns (options_->build_result_timeout ())); uint64_t forced_result_expiration_ns ( expiration_ns (options_->build_forced_rebuild_timeout ())); timestamp normal_rebuild_expiration ( expiration (options_->build_normal_rebuild_timeout ())); timestamp forced_rebuild_expiration ( expiration (options_->build_forced_rebuild_timeout ())); // Return the challenge (nonce) if brep is configured to authenticate bbot // agents. Return nullopt otherwise. // // Nonce generator must guarantee a probabilistically insignificant chance // of repeating a previously generated value. The common approach is to use // counters or random number generators (alone or in combination), that // produce values of the sufficient length. 64-bit non-repeating and // 512-bit random numbers are considered to be more than sufficient for // most practical purposes. // // We will produce the challenge as the sha256sum of the 512-bit random // number and the 64-bit current timestamp combination. The latter is // not really a non-repeating counter and can't be used alone. However // adding it is a good and cheap uniqueness improvement. // auto challenge = [&agent_fp, &now, &fail, &trace, this] () { optional r; if (agent_fp) { try { auto print_args = [&trace, this] (const char* args[], size_t n) { l2 ([&]{trace << process_args {args, n};}); }; openssl os (print_args, nullfd, path ("-"), 2, process_env (options_->openssl (), options_->openssl_envvar ()), "rand", options_->openssl_option (), 64); vector nonce (os.in.read_binary ()); os.in.close (); if (!os.wait () || nonce.size () != 64) fail << "unable to generate nonce"; uint64_t t (chrono::duration_cast ( now.time_since_epoch ()).count ()); sha256 cs (nonce.data (), nonce.size ()); cs.append (&t, sizeof (t)); r = cs.string (); } catch (const system_error& e) { fail << "unable to generate nonce: " << e; } } return r; }; // Convert butl::standard_version type to brep::version. // brep::version toolchain_version (tqm.toolchain_version.string ()); // Prepare the buildable package prepared query. // // Note that the number of packages can be large and so, in order not to // hold locks for too long, we will restrict the number of packages being // queried in a single transaction. To achieve this we will iterate through // packages using the OFFSET/LIMIT pair and sort the query result. // // Note that this approach can result in missing some packages or // iterating multiple times over some of them. However there is nothing // harmful in that: updates are infrequent and missed packages will be // picked up on the next request. // // Also note that we disregard the request tenant and operate on the whole // set of the packages and builds. In future we may add support for // building packages for a specific tenant. // using pkg_query = query; using prep_pkg_query = prepared_query; pkg_query pq (true); // Filter by repositories canonical names (if requested). // const vector& rp (params.repository ()); if (!rp.empty ()) pq = pq && pkg_query::build_repository::id.canonical_name.in_range (rp.begin (), rp.end ()); // Specify the portion. // size_t offset (0); pq += "ORDER BY" + pkg_query::build_package::id.tenant + "," + pkg_query::build_package::id.name + order_by_version (pkg_query::build_package::id.version, false) + "OFFSET" + pkg_query::_ref (offset) + "LIMIT 50"; connection_ptr conn (build_db_->connection ()); prep_pkg_query pkg_prep_query ( conn->prepare_query ( "mod-build-task-package-query", pq)); // Prepare the build prepared query. // // Note that we can not query the database for configurations that a // package was not built with, as the database contains only those package // configurations that have already been acted upon (initially empty). // // This is why we query the database for package configurations that // should not be built (in the built state, or in the building state and // not expired). Having such a list we will select the first build // configuration that is not in the list (if available) for the response. // using bld_query = query; using prep_bld_query = prepared_query; package_id id; const auto& qv (bld_query::id.package.version); bld_query bq ( bld_query::id.package.tenant == bld_query::_ref (id.tenant) && bld_query::id.package.name == bld_query::_ref (id.name) && qv.epoch == bld_query::_ref (id.version.epoch) && qv.canonical_upstream == bld_query::_ref (id.version.canonical_upstream) && qv.canonical_release == bld_query::_ref (id.version.canonical_release) && qv.revision == bld_query::_ref (id.version.revision) && bld_query::id.configuration.in_range (cfg_names.begin (), cfg_names.end ()) && compare_version_eq (bld_query::id.toolchain_version, toolchain_version, true) && (bld_query::state == "built" || ((bld_query::force == "forcing" && bld_query::timestamp > forced_result_expiration_ns) || (bld_query::force != "forcing" && // Unforced or forced. bld_query::timestamp > normal_result_expiration_ns)))); prep_bld_query bld_prep_query ( conn->prepare_query ("mod-build-task-build-query", bq)); while (tsm.session.empty ()) { transaction t (conn->begin ()); // Query (and cache) buildable packages. // auto packages (pkg_prep_query.execute ()); // Bail out if there is nothing left. // if (packages.empty ()) { t.commit (); break; } offset += packages.size (); // Iterate over packages until we find one that needs building. // for (auto& bp: packages) { id = move (bp.id); // Iterate through the package configurations and erase those that // don't need building from the build configuration map. All those // configurations that remained can be built. We will take the first // one, if present. // // Also save the built package configurations for which it's time to be // rebuilt. // config_machines configs (cfg_machines); // Make a copy for this pkg. auto pkg_builds (bld_prep_query.execute ()); for (auto i (pkg_builds.begin ()); i != pkg_builds.end (); ++i) { auto j (configs.find (i->id.configuration.c_str ())); // Outdated configurations are already excluded with the database // query. // assert (j != configs.end ()); configs.erase (j); if (i->state == build_state::built) { assert (i->force != force_state::forcing); if (i->timestamp <= (i->force == force_state::forced ? forced_rebuild_expiration : normal_rebuild_expiration)) rebuilds.emplace_back (i.load ()); } } if (!configs.empty ()) { // Find the first build configuration that is not excluded by the // package. // shared_ptr p (build_db_->load (id)); auto i (configs.begin ()); auto e (configs.end ()); for (; i != e && exclude (*p, *i->second.config); ++i) ; if (i != e) { config_machine& cm (i->second); machine_header_manifest& mh (*cm.machine); build_id bid (move (id), cm.config->name, toolchain_version); shared_ptr b (build_db_->find (bid)); optional cl (challenge ()); // If build configuration doesn't exist then create the new one // and persist. Otherwise put it into the building state, refresh // the timestamp and update. // if (b == nullptr) { b = make_shared (move (bid.package.tenant), move (bid.package.name), move (bp.version), move (bid.configuration), move (tqm.toolchain_name), move (toolchain_version), move (agent_fp), move (cl), mh.name, move (mh.summary), cm.config->target); build_db_->persist (b); } else { // The package configuration is in the building state, and there // are no results. // // Note that in both cases we keep the status intact to be able // to compare it with the final one in the result request // handling in order to decide if to send the notification // email. The same is true for the forced flag (in the sense // that we don't set the force state to unforced). // // Load the section to assert the above statement. // build_db_->load (*b, b->results_section); assert (b->state == build_state::building && b->results.empty ()); b->state = build_state::building; // Switch the force state not to reissue the task after the // forced rebuild timeout. Note that the result handler will // still recognize that the rebuild was forced. // if (b->force == force_state::forcing) b->force = force_state::forced; b->toolchain_name = move (tqm.toolchain_name); b->agent_fingerprint = move (agent_fp); b->agent_challenge = move (cl); b->machine = mh.name; b->machine_summary = move (mh.summary); b->target = cm.config->target; b->timestamp = system_clock::now (); build_db_->update (b); } // Finally, prepare the task response manifest. // // We iterate over buildable packages. // assert (p->internal_repository != nullptr); p->internal_repository.load (); tsm = task (move (b), move (p), cm); } } // If the task response manifest is prepared, then bail out from the // package loop, commit the transaction and respond. // if (!tsm.session.empty ()) break; } t.commit (); } // If we don't have an unbuilt package, then let's see if we have a // package to rebuild. // if (tsm.session.empty () && !rebuilds.empty ()) { // Sort the package configuration rebuild list with the following sort // priority: // // 1: force state // 2: overall status // 3: timestamp (less is preferred) // auto cmp = [] (const shared_ptr& x, const shared_ptr& y) { if (x->force != y->force) return x->force > y->force; // Forced goes first. assert (x->status && y->status); // Both built. if (x->status != y->status) return x->status > y->status; // Larger status goes first. return x->timestamp < y->timestamp; // Older goes first. }; sort (rebuilds.begin (), rebuilds.end (), cmp); optional cl (challenge ()); // Pick the first package configuration from the ordered list. // // Note that the configurations and packages may not match the required // criteria anymore (as we have committed the database transactions that // were used to collect this data) so we recheck. If we find one that // matches then put it into the building state, refresh the timestamp and // update. Note that we don't amend the status and the force state to // have them available in the result request handling (see above). // for (auto& b: rebuilds) { try { transaction t (build_db_->begin ()); b = build_db_->find (b->id); if (b != nullptr && b->state == build_state::built && b->timestamp <= (b->force == force_state::forced ? forced_rebuild_expiration : normal_rebuild_expiration)) { auto i (cfg_machines.find (b->id.configuration.c_str ())); // Only actual package configurations are loaded (see above). // assert (i != cfg_machines.end ()); const config_machine& cm (i->second); // Rebuild the package if still present, is buildable and doesn't // exclude the configuration. // shared_ptr p ( build_db_->find (b->id.package)); if (p != nullptr && p->internal_repository != nullptr && !exclude (*p, *cm.config)) { assert (b->status); b->state = build_state::building; // Can't move from, as may need them on the next iteration. // b->agent_fingerprint = agent_fp; b->agent_challenge = cl; b->toolchain_name = tqm.toolchain_name; const machine_header_manifest& mh (*cm.machine); b->machine = mh.name; b->machine_summary = mh.summary; b->target = cm.config->target; // Mark the section as loaded, so results are updated. // b->results_section.load (); b->results.clear (); b->timestamp = system_clock::now (); build_db_->update (b); p->internal_repository.load (); tsm = task (move (b), move (p), cm); } } t.commit (); } catch (const odb::deadlock&) {} // Just try with the next rebuild. // If the task response manifest is prepared, then bail out from the // package configuration rebuilds loop and respond. // if (!tsm.session.empty ()) break; } } } // @@ Probably it would be a good idea to also send some cache control // headers to avoid caching by HTTP proxies. That would require extension // of the web::response interface. // manifest_serializer s (rs.content (200, "text/manifest;charset=utf-8"), "task_response_manifest"); tsm.serialize (s); return true; }