Skip to content

Fix a regression with snowflake connection being closed#13665

Merged
lukasmasuch merged 1 commit intodevelopfrom
fix/issue-with-closed-connection
Jan 22, 2026
Merged

Fix a regression with snowflake connection being closed#13665
lukasmasuch merged 1 commit intodevelopfrom
fix/issue-with-closed-connection

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Jan 21, 2026

Describe your changes

Fixes an issue with Snowflake connections not getting re-initialized after having been closed.

Claude issue analysis # Issue Report: Snowflake Connection "Connection is closed" Error

Summary

After recent PRs adding on_release to st.cache_resource and session-scoped connection support, users may encounter a snowflake.connector.errors.DatabaseError: 250002 (08003): Connection is closed error when using st.connection("snowflake") with cached data queries.

Related PRs

Error Details

Traceback (most recent call last):
  File ".../streamlit/runtime/scriptrunner/exec_code.py", line 129, in exec_func_with_error_handling
    result = func()
  ...
  File ".../snowflake/snowpark/_internal/server_connection.py", line 205, in _cursor
    self._thread_store.cursor = self._conn.cursor()
  File ".../snowflake/connector/connection.py", line 1270, in cursor
    Error.errorhandler_wrapper(...)
snowflake.connector.errors.DatabaseError: 250002 (08003): Connection is closed

Root Cause Analysis

Background

The PRs mentioned added important functionality:

  1. PR Add on_release to st.cache_resource. #13439 added the on_release callback to st.cache_resource, which is called when cache entries are evicted
  2. PR Add session-scoped connection support. #13506 modified connection_factory.py to use this on_release callback to call connection.close() when a connection is evicted from the cache

This is the relevant code in connection_factory.py:

def on_release_wrapped(connection: ConnectionClass) -> None:
    connection.close()

__create_connection = cache_resource(
    max_entries=max_entries,
    show_spinner="Running `st.connection(...)`.",
    ttl=ttl,
    scope=scope,
    on_release=on_release_wrapped,  # Calls close() when evicted
)(__create_connection)

The Bug

In BaseSnowflakeConnection.close(), after calling self._raw_instance.close(), the _raw_instance attribute was NOT reset to None:

def close(self) -> None:
    """Closes the underlying Snowflake connection."""
    if self._raw_instance is not None:
        self._raw_instance.close()
        # BUG: _raw_instance was NOT set to None!

This caused the following issue:

  1. When close() was called (e.g., via on_release when a cache entry is evicted), the underlying connection was closed

  2. However, _raw_instance still referenced the closed connection object

  3. The _instance property checks if self._raw_instance is None to decide whether to create a new connection:

    @property
    def _instance(self) -> RawConnectionT:
        if self._raw_instance is None:
            self._raw_instance = self._connect(**self._kwargs)
        return self._raw_instance
  4. Since _raw_instance wasn't None, subsequent access to _instance returned the CLOSED connection

  5. Any operations on the closed connection failed with "Connection is closed"

When This Bug Manifests

The on_release callback (which calls close()) is triggered when:

  • Cache entries expire due to TTL
  • Cache is full and oldest entries are evicted (max_entries)
  • st.cache_resource.clear() is called
  • For session-scoped caches: when a session disconnects

For global-scoped connections like st.connection("snowflake"), this typically only happens if:

  • st.cache_resource.clear() is called explicitly
  • TTL is set and expires
  • max_entries is set and exceeded

Additional Consideration: Snowpark Sessions

When users call conn.session(), they get a Snowpark Session that internally references self._instance. If the underlying connection is closed:

def session(self) -> Session:
    if running_in_sis():
        return get_active_session()
    return Session.builder.configs({"connection": self._instance}).create()

Any Snowpark Sessions created from the connection will also fail because they hold a reference to the now-closed underlying connection object.

Fix

The fix is simple: reset _raw_instance to None after closing the connection:

def close(self) -> None:
    """Closes the underlying Snowflake connection."""
    if self._raw_instance is not None:
        self._raw_instance.close()
        self._raw_instance = None  # Added this line

This ensures that after close() is called, the next access to _instance will create a new connection instead of returning the closed one.

Files Changed

  1. lib/streamlit/connections/snowflake_connection.py

    • Fixed close() method to reset _raw_instance = None after closing
  2. lib/tests/streamlit/connections/snowflake_connection_test.py

    • Added TestSnowflakeConnectionClose test class with:
      • test_close_resets_raw_instance: Verifies that close() closes the connection AND resets _raw_instance
      • test_close_is_noop_when_not_connected: Verifies that close() doesn't fail when _raw_instance is already None

Testing

PYTHONPATH=lib pytest lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose -v

Output:

lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose::test_close_resets_raw_instance PASSED
lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose::test_close_is_noop_when_not_connected PASSED

Recommendations for Users

Until this fix is released, users experiencing this issue can:

  1. Avoid storing Snowpark Sessions long-term: Instead of caching Snowpark Sessions, create them fresh when needed
  2. Check if using st.cache_resource.clear(): If calling this anywhere in the app, it will close all cached connections
  3. Consider connection TTL settings: If TTL is set on the connection, it may expire and close

Impact

  • Affected: Users of st.connection("snowflake") and st.connection("snowflake-callers-rights") who experience cache eviction scenarios
  • Severity: Medium - The bug causes operations to fail with a confusing error message, but the workaround (restarting the app or avoiding cache clears) is available
  • Scope: Only affects SnowflakeConnection and its subclasses; other connection types (SQLConnection, SnowparkConnection) inherit the no-op close() from BaseConnection and are not affected

GitHub Issue Link (if applicable)

Testing Plan

  • Added unit test.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 21, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 21, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13665/streamlit-1.53.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13665.streamlit.app (☁️ Deploy here if not accessible)

@lukasmasuch lukasmasuch added security-assessment-completed change:bugfix PR contains bug fix implementation impact:users PR changes affect end users labels Jan 21, 2026
@lukasmasuch lukasmasuch marked this pull request as ready for review January 21, 2026 21:55
Copilot AI review requested due to automatic review settings January 21, 2026 21:55
@lukasmasuch
Copy link
Copy Markdown
Collaborator Author

@cursor review

@sfc-gh-lmasuch sfc-gh-lmasuch added the ai-review If applied to PR or issue will run AI review workflow label Jan 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a regression where Snowflake connections fail with a "Connection is closed" error after the connection has been evicted from the cache. The bug was introduced when on_release callbacks were added to st.cache_resource, which calls close() on evicted connections.

Changes:

  • Fixed BaseSnowflakeConnection.close() to reset _raw_instance to None after closing the connection
  • Added unit tests to verify the fix and prevent regressions

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
lib/streamlit/connections/snowflake_connection.py Added self._raw_instance = None after closing the connection to ensure subsequent access creates a new connection instead of returning the closed one
lib/tests/streamlit/connections/snowflake_connection_test.py Added TestSnowflakeConnectionClose test class with comprehensive tests covering the close behavior and edge cases

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Jan 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR fixes a regression in BaseSnowflakeConnection.close() where the underlying connection was closed but _raw_instance was not reset to None. This caused the _instance property to return a closed connection object on subsequent access instead of creating a new one, resulting in "Connection is closed" errors.

The fix is minimal and targeted: adding self._raw_instance = None after self._raw_instance.close() in the close() method.

Code Quality

The code change is clean and follows existing patterns in the codebase:

  1. Consistency with reset(): The fix aligns with how BaseConnection.reset() works (line 155 of base_connection.py), which sets self._raw_instance = None. The close() method now properly closes the connection AND resets the instance reference.

  2. Minimal change: The fix is a single line addition at line 352 of snowflake_connection.py, which is the correct and minimal solution to the problem.

  3. Clear documentation: The PR description provides excellent context about the root cause, affected scenarios, and the fix.

Test Coverage

The tests are well-written and follow the Python unit test guidelines:

  1. test_close_resets_raw_instance (lines 234-263):

    • Has a proper docstring explaining the test purpose
    • Type-annotated with -> None
    • Verifies both the positive behavior (connection is closed, new connection is created) and the state change (_raw_instance is None after close)
    • Tests the full lifecycle: connect → close → reconnect
  2. test_close_is_noop_when_not_connected (lines 265-280):

    • Tests the edge case where close() is called when _raw_instance is already None
    • Ensures no exception is raised and state remains consistent
    • This is a good negative/edge-case test as recommended by the testing guidelines
  3. Test class placement: The new TestSnowflakeConnectionClose class is correctly placed and not marked with @pytest.mark.require_integration since these tests mock the Snowflake connection and don't require actual integration.

Minor observation: The tests could include a negative assertion to verify that a closed connection is NOT returned after close() is called (e.g., assert conn._instance is not mock_connection after close). However, the current assertion assert conn._instance is second_mock_connection implicitly covers this.

Backwards Compatibility

This change is fully backwards compatible:

  1. No API changes: The close() method signature remains unchanged.
  2. Bug fix only: This restores expected behavior - connections should be re-creatable after being closed.
  3. No breaking changes: Existing code that calls close() will now work correctly. Code that doesn't call close() is unaffected.

Security & Risk

  1. No security concerns: This is a bug fix for connection lifecycle management.
  2. Low regression risk: The change is minimal and well-tested.
  3. PR is labeled security-assessment-completed: Security review has already been performed.

One consideration: Snowpark Sessions created via conn.session() will still hold references to the old closed connection. This is existing behavior and not introduced by this PR - the PR description correctly documents this as a known limitation that users should be aware of.

Recommendations

No blocking issues found. The PR is ready for merge.

Optional improvements (non-blocking):

  1. Consider adding a brief inline comment explaining why _raw_instance is set to None:
def close(self) -> None:
    """Closes the underlying Snowflake connection."""
    if self._raw_instance is not None:
        self._raw_instance.close()
        # Reset so _instance property creates a new connection on next access
        self._raw_instance = None
  1. The test test_close_resets_raw_instance could add an explicit negative assertion like assert conn._instance is not mock_connection after the close, though this is implicitly covered by the existing assertions.

Verdict

APPROVED: This is a well-implemented, minimal bug fix that correctly resolves a regression in Snowflake connection handling. The fix follows existing patterns in the codebase, and the tests adequately cover both the primary fix and edge cases.


This is an automated AI review using opus-4.5-thinking. Please verify the feedback and use your judgment.

@github-actions
Copy link
Copy Markdown
Contributor

📈 Frontend coverage change detected

The frontend unit test (vitest) coverage has increased by 0.0500%

  • Current PR: 86.4500% (13339 lines, 1807 missed)
  • Latest develop: 86.4000% (13339 lines, 1814 missed)

🎉 Great job on improving test coverage!

📊 View detailed coverage comparison

@lukasmasuch lukasmasuch merged commit a7a2e8f into develop Jan 22, 2026
85 of 86 checks passed
@lukasmasuch lukasmasuch deleted the fix/issue-with-closed-connection branch January 22, 2026 00:16
github-actions bot pushed a commit that referenced this pull request Jan 22, 2026
## Describe your changes

Fixes an issue with Snowflake connections not getting re-initialized
after having been closed.

<details>
<summary>Claude issue analysis</summary> 
# Issue Report: Snowflake Connection "Connection is closed" Error

## Summary

After recent PRs adding `on_release` to `st.cache_resource` and
session-scoped connection support, users may encounter a
`snowflake.connector.errors.DatabaseError: 250002 (08003): Connection is
closed` error when using `st.connection("snowflake")` with cached data
queries.

## Related PRs

- **PR #13439**: Add `on_release` to `st.cache_resource`
- **PR #13482**: Add session scoping to caches
- **PR #13538**: Add `SnowflakeCallersRightsConnection`
- **PR #13506**: Add session-scoped connection support

## Error Details

```
Traceback (most recent call last):
  File ".../streamlit/runtime/scriptrunner/exec_code.py", line 129, in exec_func_with_error_handling
    result = func()
  ...
  File ".../snowflake/snowpark/_internal/server_connection.py", line 205, in _cursor
    self._thread_store.cursor = self._conn.cursor()
  File ".../snowflake/connector/connection.py", line 1270, in cursor
    Error.errorhandler_wrapper(...)
snowflake.connector.errors.DatabaseError: 250002 (08003): Connection is closed
```

## Root Cause Analysis

### Background

The PRs mentioned added important functionality:

1. **PR #13439** added the `on_release` callback to `st.cache_resource`,
which is called when cache entries are evicted
2. **PR #13506** modified `connection_factory.py` to use this
`on_release` callback to call `connection.close()` when a connection is
evicted from the cache

This is the relevant code in `connection_factory.py`:

```python
def on_release_wrapped(connection: ConnectionClass) -> None:
    connection.close()

__create_connection = cache_resource(
    max_entries=max_entries,
    show_spinner="Running `st.connection(...)`.",
    ttl=ttl,
    scope=scope,
    on_release=on_release_wrapped,  # Calls close() when evicted
)(__create_connection)
```

### The Bug

In `BaseSnowflakeConnection.close()`, after calling
`self._raw_instance.close()`, the `_raw_instance` attribute was **NOT**
reset to `None`:

```python
def close(self) -> None:
    """Closes the underlying Snowflake connection."""
    if self._raw_instance is not None:
        self._raw_instance.close()
        # BUG: _raw_instance was NOT set to None!
```

This caused the following issue:

1. When `close()` was called (e.g., via `on_release` when a cache entry
is evicted), the underlying connection was closed
2. However, `_raw_instance` still referenced the **closed** connection
object
3. The `_instance` property checks `if self._raw_instance is None` to
decide whether to create a new connection:

   ```python
   @Property
   def _instance(self) -> RawConnectionT:
       if self._raw_instance is None:
           self._raw_instance = self._connect(**self._kwargs)
       return self._raw_instance
   ```

4. Since `_raw_instance` wasn't `None`, subsequent access to `_instance`
returned the **CLOSED** connection
5. Any operations on the closed connection failed with "Connection is
closed"

### When This Bug Manifests

The `on_release` callback (which calls `close()`) is triggered when:

- Cache entries expire due to TTL
- Cache is full and oldest entries are evicted (`max_entries`)
- `st.cache_resource.clear()` is called
- For session-scoped caches: when a session disconnects

For global-scoped connections like `st.connection("snowflake")`, this
typically only happens if:
- `st.cache_resource.clear()` is called explicitly
- TTL is set and expires
- `max_entries` is set and exceeded

### Additional Consideration: Snowpark Sessions

When users call `conn.session()`, they get a Snowpark Session that
internally references `self._instance`. If the underlying connection is
closed:

```python
def session(self) -> Session:
    if running_in_sis():
        return get_active_session()
    return Session.builder.configs({"connection": self._instance}).create()
```

Any Snowpark Sessions created from the connection will also fail because
they hold a reference to the now-closed underlying connection object.

## Fix

The fix is simple: reset `_raw_instance` to `None` after closing the
connection:

```python
def close(self) -> None:
    """Closes the underlying Snowflake connection."""
    if self._raw_instance is not None:
        self._raw_instance.close()
        self._raw_instance = None  # Added this line
```

This ensures that after `close()` is called, the next access to
`_instance` will create a new connection instead of returning the closed
one.

## Files Changed

1. **`lib/streamlit/connections/snowflake_connection.py`**
- Fixed `close()` method to reset `_raw_instance = None` after closing

2. **`lib/tests/streamlit/connections/snowflake_connection_test.py`**
   - Added `TestSnowflakeConnectionClose` test class with:
- `test_close_resets_raw_instance`: Verifies that `close()` closes the
connection AND resets `_raw_instance`
- `test_close_is_noop_when_not_connected`: Verifies that `close()`
doesn't fail when `_raw_instance` is already `None`

## Testing

```bash
PYTHONPATH=lib pytest lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose -v
```

Output:
```
lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose::test_close_resets_raw_instance PASSED
lib/tests/streamlit/connections/snowflake_connection_test.py::TestSnowflakeConnectionClose::test_close_is_noop_when_not_connected PASSED
```

## Recommendations for Users

Until this fix is released, users experiencing this issue can:

1. **Avoid storing Snowpark Sessions long-term**: Instead of caching
Snowpark Sessions, create them fresh when needed
2. **Check if using `st.cache_resource.clear()`**: If calling this
anywhere in the app, it will close all cached connections
3. **Consider connection TTL settings**: If TTL is set on the
connection, it may expire and close

## Impact

- **Affected**: Users of `st.connection("snowflake")` and
`st.connection("snowflake-callers-rights")` who experience cache
eviction scenarios
- **Severity**: Medium - The bug causes operations to fail with a
confusing error message, but the workaround (restarting the app or
avoiding cache clears) is available
- **Scope**: Only affects `SnowflakeConnection` and its subclasses;
other connection types (`SQLConnection`, `SnowparkConnection`) inherit
the no-op `close()` from `BaseConnection` and are not affected
</details>

## GitHub Issue Link (if applicable)

## Testing Plan

- Added unit test.

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants