Skip to content

Conversation

@mrmundt
Copy link
Owner

@mrmundt mrmundt commented Oct 6, 2025

Fixes # .

Summary/Motivation:

Changes proposed in this PR:

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.



class _LicenseManager:
def acquire(self, timeout: Optional[float] = None) -> None:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put in a docstring (from the issue in which I took notes) explaining how users are expected to interact with licenses.

return

if not timeout:
cls._set_env(gurobipy.Env())
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Pyomo thread would need its own gurobi.Env - because a single thread needs a single Env.

Comment on lines +368 to +427
@classmethod
def _register_env_client(cls):
cls._num_gurobipy_env_clients += 1

@classmethod
def _release_env_client(cls):
if cls._num_gurobipy_env_clients > 0:
cls._num_gurobipy_env_clients -= 1
env = cls._get_env()
if cls._num_gurobipy_env_clients <= 0 and env is not None:
if cls._num_gurobipy_env_clients < 0:
logger.warning(
"Gurobi env client refcount went negative "
f"({cls._num_gurobipy_env_clients}). "
"This should not have happened and should be reported to "
"Pyomo development team."
)
try:
env.close()
except Exception as err:
logger.warning(f"Exception while closing Gurobi environment: {err!r}")
finally:
cls._set_env(None)

@classmethod
def _get_env(cls):
"""Return the current shared Env (or None)"""
if cls._gurobipy_env_key is None:
return None
return _GUROBI_ENV_REGISTRY.get(cls._gurobipy_env_key)

@classmethod
def _set_env(cls, env):
"""Install or clear the current process-local Env"""
if env is None:
if cls._gurobipy_env_key is not None:
_GUROBI_ENV_REGISTRY.pop(cls._gurobipy_env_key, None)
cls._gurobipy_env_key = None
else:
if cls._gurobipy_env_key is None:
cls._gurobipy_env_key = id(cls)
_GUROBI_ENV_REGISTRY[cls._gurobipy_env_key] = env

@classmethod
def _ensure_env(cls):
"""
Return a live gurobipy.Env. If current env exists but is closed,
recreate it.
"""
env = cls._get_env()
if env is None:
return None
if not hasattr(env, "_cenv"):
try:
env.close()
except Exception:
pass
env = gurobipy.Env()
cls._set_env(env)
return env
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should all move to the _GurobiLicenseManager

return _cm()


class GurobiSolverMixin:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double-check that the default behavior is that only one Env is created even if you make both a gurobi_direct and gurobi_persistent solver at the same time.

# requirement that the direct interface doesn't)
if not self._is_gp_available():
self.__class__._available_cache = Availability.NotFound
else:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the license manager instead. (Which I half-did - whoops, gotta fix this. Def a bug.)

vtype=vtype,

# Acquire a Gurobi env for the duration of solve (opt + postsolve):
with self.license():
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to do this in solve, but let's make sure that acquiring a license twice doesn't do anything (e.g., if a user runs solver.license.acquire(), it won't try to acquire another license).


class _LicenseManager:
def acquire(self, timeout: Optional[float] = None) -> None:
"""Acquire and lock a license. Default behavior is to simply return
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had a discussion about timeout here - there is a difference between "time allowing the server to try before we give up / cancel" vs. "we are retrying for X amount of time, only if we keep getting errors"

Copy link
Owner Author

@mrmundt mrmundt Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout (flaky network / talking to license server) and retry (programming convenience, keep trying until a license becomes available) or whatever. If one/both are irrelevant, great, ignore it.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • timeout - infinity
  • retry - left at solver default (None / not set in some way)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants