Skip to content

Commit

Permalink
Never destroy local Gurobi licenses
Browse files Browse the repository at this point in the history
Iff the contents of the file identified in GRB_LICENSE_FILE contain
the magic keywork HOSTID, then any license that is obtained will never
be destroyed.

Resolves RobotLocomotion#19657.
  • Loading branch information
RussTedrake committed Jun 30, 2023
1 parent 774f887 commit 9874fc2
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 9 deletions.
1 change: 1 addition & 0 deletions solvers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,7 @@ drake_cc_googletest(
deps = [
":gurobi_solver",
":mathematical_program",
"//common:temp_directory",
"//common/test_utilities:expect_no_throw",
"//common/test_utilities:expect_throws_message",
],
Expand Down
44 changes: 43 additions & 1 deletion solvers/gurobi_solver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
#include <algorithm>
#include <charconv>
#include <cmath>
#include <fstream>
#include <iostream>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -1039,6 +1042,7 @@ class GurobiSolver::License {
"Could not locate Gurobi license key file because GRB_LICENSE_FILE "
"environment variable was not set.");
}

const int num_tries = 3;
int grb_load_env_error = 1;
for (int i = 0; grb_load_env_error && i < num_tries; ++i) {
Expand All @@ -1053,21 +1057,59 @@ class GurobiSolver::License {
"\".");
}
DRAKE_DEMAND(env_ != nullptr);

// We use the existence of the string HOSTID in the license file as
// confirmation that the license is associated with the local host.
const char* grb_license_filename = std::getenv("GRB_LICENSE_FILE");
std::ifstream grb_license_file(grb_license_filename);
if (!grb_license_file) {
throw std::runtime_error(
"Could not read Gurobi license file specified in the "
"GRB_LICENSE_FILE environment variable");
}
const std::string grb_license_file_contents(
(std::istreambuf_iterator<char>(grb_license_file)),
std::istreambuf_iterator<char>());
is_local_license_ =
grb_license_file_contents.find("HOSTID") != std::string::npos;
}

~License() {
GRBfreeenv(env_);
env_ = nullptr;
}

bool is_local_license() const { return is_local_license_; }

GRBenv* GurobiEnv() { return env_; }

private:
bool is_local_license_{false};
GRBenv* env_ = nullptr;
};

/* Gurobi recommends acquiring the license only once per program to avoid
overhead from acquiring the license (and console spew for academic license
users; see #19657). However, if users are using a shared network license from a
limited pool, then we risk them checking out the license and not giving it back
(for instance, if they are working in a jupyter notebook). As a compromise, we
hold on to the license beyond the lifetime of the GurobiSolver iff we can
confirm that the license is associated with the local host. */
std::shared_ptr<GurobiSolver::License> local_host_gurobi_license{};

std::shared_ptr<GurobiSolver::License> GurobiSolver::AcquireLicense() {
return GetScopedSingleton<GurobiSolver::License>();
if (local_host_gurobi_license) {
return local_host_gurobi_license;
}
auto license = GetScopedSingleton<GurobiSolver::License>();
if (license->is_local_license()) {
local_host_gurobi_license = license;
}
return license;
}

bool GurobiSolver::has_acquired_local_license() const {
return license_ && license_->is_local_license();
}

// TODO([email protected]): break this large DoSolve function to smaller
Expand Down
25 changes: 17 additions & 8 deletions solvers/gurobi_solver.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,16 @@ class GurobiSolver final : public SolverBase {

/**
* This acquires a Gurobi license environment shared among all GurobiSolver
* instances; the environment will stay valid as long as at least one
* shared_ptr returned by this function is alive.
* Call this ONLY if you must use different MathematicalProgram
* instances at different instances in time, and repeatedly acquiring the
* license is costly (e.g., requires contacting a license server).
* @return A shared pointer to a license environment that will stay valid
* as long as any shared_ptr returned by this function is alive. If Gurobi
* not available in your build, this will return a null (empty) shared_ptr.
* instances. The environment will stay valid as long as at least one
* shared_ptr returned by this function is alive or if the license can be
* confirmed to be a local license then it will never be destroyed; see
* has_acquired_local_license(). Call this ONLY if you must use different
* MathematicalProgram instances at different instances in time, and
* repeatedly acquiring the license is costly (e.g., requires contacting a
* license server).
* @return A shared pointer to a license environment that will stay valid as
* long as any shared_ptr returned by this function is alive. If Gurobi not
* available in your build, this will return a null (empty) shared_ptr.
* @throws std::exception if Gurobi is available but a license cannot be
* obtained.
*/
Expand All @@ -187,6 +189,13 @@ class GurobiSolver final : public SolverBase {
static std::string UnsatisfiedProgramAttributes(const MathematicalProgram&);
//@}

// Returns true if this solver has acquired a license during a Solve(), and
// this license was confirmed to be tied to the local host. If the license can
// be confirmed to be local, then it will never be destroyed. If the license
// cannot be confirmed to be local, then the license will stay valid as long
// as at least one shared_ptr returned by AcquireLicense() is alive.
bool has_acquired_local_license() const;

// A using-declaration adds these methods into our class's Doxygen.
using SolverBase::Solve;

Expand Down
30 changes: 30 additions & 0 deletions solvers/test/gurobi_solver_grb_license_file_test.cc
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include <cstdlib>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>

#include <gtest/gtest.h>

#include "drake/common/temp_directory.h"
#include "drake/common/test_utilities/expect_no_throw.h"
#include "drake/common/test_utilities/expect_throws_message.h"
#include "drake/solvers/gurobi_solver.h"
Expand Down Expand Up @@ -39,6 +41,16 @@ class GrbLicenseFileTest : public ::testing::Test {
prog_.NewContinuousVariables<1>();
}

void WriteTempLicenseFile(std::string contents) {
std::string test_license = temp_directory() + "/test.lic";
std::ofstream file(test_license);
ASSERT_TRUE(file);
file << contents;
const int setenv_result =
::setenv("GRB_LICENSE_FILE", test_license.c_str(), 1);
ASSERT_EQ(setenv_result, 0);
}

void TearDown() override {
if (orig_grb_license_file_) {
const int setenv_result =
Expand Down Expand Up @@ -68,6 +80,24 @@ TEST_F(GrbLicenseFileTest, GrbLicenseFileUnset) {
".*GurobiSolver has not been properly configured.*");
}


TEST_F(GrbLicenseFileTest, LocalLicenseFile) {
EXPECT_EQ(solver_.enabled(), true);
WriteTempLicenseFile(
"license file contents that contains the string HOSTID and perhaps some "
"other info.");
solver_.Solve(prog_);
EXPECT_TRUE(solver_.has_acquired_local_license());
}

TEST_F(GrbLicenseFileTest, ServerLicenseFile) {
EXPECT_EQ(solver_.enabled(), true);
WriteTempLicenseFile(
"license file contents without the magic keyword.");
solver_.Solve(prog_);
EXPECT_FALSE(solver_.has_acquired_local_license());
}

} // namespace
} // namespace solvers
} // namespace drake

0 comments on commit 9874fc2

Please sign in to comment.