Skip to content

fix(gurobi): dispose the solver model before the env on close#826

Merged
FabianHofmann merged 5 commits into
masterfrom
fix/gurobi-license-dispose
Jul 15, 2026
Merged

fix(gurobi): dispose the solver model before the env on close#826
FabianHofmann merged 5 commits into
masterfrom
fix/gurobi-license-dispose

Conversation

@FBumann

@FBumann FBumann commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

I treid to verify that the gurobi license issue #459 is fully solved. As far as i can see its not perfect yet. I also took the opportunity to make the .close() more discoverable!

Note

The following content was generated by AI (Claude Code).

Follow-up to #459. The env has been context-managed since the license-leak audit, but Solver.close() only dereferenced the gurobipy.Model — it disposed the env first and relied on refcount GC to free the model. Since Gurobi keeps the underlying environment (and with it the license) alive until every model created from it is freed, any user-held model.solver_model reference silently kept the license acquired after close() / model.solver = None.

What changed

  • Gurobi now registers the gurobipy.Model on the env ExitStack (as Xpress and Mosek already do). close() unwinds LIFO, disposing the model before the env — the ordering the Gurobi docs require — and explicit dispose() releases the license even while user code still holds a reference.
  • to_gurobipy() detaches the built model from its throwaway solver so the caller keeps ownership (its teardown would otherwise now dispose the returned model).
  • Model.solve() docstring documents when the license is released: model.solver.close(), model.solver = None, the next solve() call, or model GC.

Compatibility with older gurobipy

The fix relies on gurobipy.Model supporting the context-manager protocol. Env and Model have been context managers since Gurobi 9.0, and the oldest gurobipy with wheels for Python 3.11 (linopy's floor) is 10.0 — so every installable combination is covered. Verified identical behavior (context protocol + Model has already been freed on disposed access) on gurobipy 12.0.1 and 13.0.2.

Proof it was a footgun

The new test test_gurobi_close_disposes_held_solver_model holds a solver_model reference across close() and asserts the model is disposed. On master it fails — the held model is still fully queryable, i.e. the license is still acquired.

Empirical demonstration (gurobipy 13.0.2)
import contextlib
import gurobipy as gp

stack = contextlib.ExitStack()
env = stack.enter_context(gp.Env())
m = gp.Model(env=env)
m.addVar(); m.update()

held = m          # user-held reference, e.g. model.solver_model
stack.close()     # disposes the env — what Solver.close() did on master

print(held.NumVars)   # -> 1: model (and license) still alive
held.dispose()        # only now is everything released

New test against master (pre-fix):

FAILED test/test_solvers.py::test_gurobi_close_disposes_held_solver_model[direct]
1 failed, 5 passed, 100 deselected in 1.31s

With the fix: 19 passed (-k "close or gurobi"), and the full gurobi/persistent sweep passes (523 passed, 11 skipped).

🤖 Generated with Claude Code

Register the gurobipy model on the env ExitStack (as Xpress and Mosek
already do), so Solver.close() disposes it before the env — the ordering
Gurobi requires for the license to be released. Previously the model was
only dereferenced, so any user-held model.solver_model reference silently
kept the license acquired after close().

to_gurobipy() now detaches the built model from the throwaway solver so
the caller keeps ownership. Document the release points in Model.solve().

Closes #459 follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 173 untouched benchmarks
⏩ 173 skipped benchmarks1


Comparing fix/gurobi-license-dispose (9796537) with master (4ca8d5a)

Open in CodSpeed

Footnotes

  1. 173 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

FBumann and others added 4 commits July 14, 2026 15:53
Also note in Model.solve() that the attached solver enables persistent
re-solves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mypy rejected Gurobi._detach_solver_model on the Solver-typed return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The autosummary method tables on the subclass pages show only the first
docstring line, so it must carry the license-release fact itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@FBumann FBumann marked this pull request as ready for review July 14, 2026 14:27
@FabianHofmann FabianHofmann self-requested a review July 14, 2026 17:11

@FabianHofmann FabianHofmann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thanks for covering this remaining bit

@FabianHofmann FabianHofmann merged commit 065b7ff into master Jul 15, 2026
24 checks passed
@FabianHofmann FabianHofmann deleted the fix/gurobi-license-dispose branch July 15, 2026 18:10
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