Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ repair-wheel-command = ""

[tool.cibuildwheel.windows]
archs = ["AMD64"]

[tool.check-manifest]
ignore = ["src/snowflake/connector/minicore/*"]
6 changes: 6 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ where = src
exclude = snowflake.connector.cpp*
include = snowflake.*

[options.package_data]
snowflake.connector.minicore =
*.so
*.dll
*.dylib

[options.entry_points]
console_scripts =
snowflake-dump-ocsp-response = snowflake.connector.tool.dump_ocsp_response:main
Expand Down
10 changes: 10 additions & 0 deletions src/snowflake/connector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from functools import wraps

from ._utils import _core_loader

apilevel = "2.0"
threadsafety = 2
paramstyle = "pyformat"
Expand Down Expand Up @@ -45,6 +47,14 @@
from .log_configuration import EasyLoggingConfigPython
from .version import VERSION

# Load the core library - failures are captured in core_loader and don't prevent module loading
try:
_core_loader.load()
except Exception:
# Silently continue if core loading fails - the error is already captured in core_loader
# This ensures the connector module loads even if the minicore library is unavailable
pass

logging.getLogger(__name__).addHandler(NullHandler())
setup_external_libraries()

Expand Down
76 changes: 76 additions & 0 deletions src/snowflake/connector/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from __future__ import annotations

import ctypes
import importlib
import os
import string
import sys
import threading
from enum import Enum
from inspect import stack
from random import choice
Expand Down Expand Up @@ -96,3 +101,74 @@ def get_application_path() -> str:
return outermost_frame.filename
except Exception:
return "unknown"


class _CoreLoader:
def __init__(self):
self._version: bytes | None = None
self._error: Exception | None = None

@staticmethod
def _get_core_path():
# Define the file name for each platform
if sys.platform.startswith("win"):
lib_name = "libsf_mini_core.dll"
elif sys.platform.startswith("darwin"):
lib_name = "libsf_mini_core.dylib"
else:
lib_name = "libsf_mini_core.so"

files = importlib.resources.files("snowflake.connector.minicore")
return files.joinpath(lib_name)

@staticmethod
def _register_functions(core: ctypes.CDLL):
core.sf_core_full_version.argtypes = []
core.sf_core_full_version.restype = ctypes.c_char_p

@staticmethod
def _load_minicore(path: str) -> ctypes.CDLL:
# This context manager is the safe way to get a
# file path from importlib.resources. It handles cases
# where the file is inside a zip and needs to be extracted
# to a temporary location.
with importlib.resources.as_file(path) as lib_path:
core = ctypes.CDLL(str(lib_path))
return core

def _is_core_disabled(self) -> bool:
value = str(os.getenv("SNOWFLAKE_DISABLE_MINICORE", None)).lower()
return value in ["1", "true"]

def _load(self) -> None:
try:
path = self._get_core_path()
core = self._load_minicore(path)
self._register_functions(core)
self._version = core.sf_core_full_version()
self._error = None
except Exception as err:
self._error = err

def load(self):
"""Spawn a separate thread to load the minicore library (non-blocking)."""
if self._is_core_disabled():
self._error = "mini-core-disabled"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we wrap it into an error / change class definition to Exception | str | None?

return
self._error = "still-loading"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we wrap it into an error / change class definition to Exception | str | None?

thread = threading.Thread(target=self._load, daemon=True)
thread.start()

def get_load_error(self) -> str:
return str(self._error)

def get_core_version(self) -> str | None:
if self._version:
try:
return self._version.decode("utf-8")
except Exception:
pass
return None


_core_loader = _CoreLoader()
18 changes: 18 additions & 0 deletions src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from ._utils import (
_DEFAULT_VALUE_SERVER_DOP_CAP_FOR_FILE_TRANSFER,
_VARIABLE_NAME_SERVER_DOP_CAP_FOR_FILE_TRANSFER,
_core_loader,
)
from .auth import (
FIRST_PARTY_AUTHENTICATORS,
Expand Down Expand Up @@ -677,6 +678,7 @@ def __init__(

# get the imported modules from sys.modules
self._log_telemetry_imported_packages()
self._log_minicore_import()
# check SNOW-1218851 for long term improvement plan to refactor ocsp code
atexit.register(self._close_at_exit)

Expand Down Expand Up @@ -2488,6 +2490,22 @@ def async_query_check_helper(

return not found_unfinished_query

def _log_minicore_import(self):
ts = get_time_millis()
self._log_telemetry(
TelemetryData.from_telemetry_data_dict(
from_dict={
TelemetryField.KEY_TYPE.value: TelemetryField.CORE_IMPORT.value,
TelemetryField.KEY_VALUE: {
"CORE_LOAD_ERROR": _core_loader.get_load_error(),
"CORE_VERSION": _core_loader.get_core_version(),
},
},
timestamp=ts,
connection=self,
)
)

def _log_telemetry_imported_packages(self) -> None:
if self._log_imported_packages_in_telemetry:
# filter out duplicates caused by submodules
Expand Down
Empty file.
54 changes: 54 additions & 0 deletions src/snowflake/connector/minicore/sf_mini_core.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @file sf_mini_core.h
* @brief C API for sf_mini_core library
*
* This header file provides the C API for the sf_mini_core Rust library.
* This file is auto-generated by cbindgen. Do not edit manually!
*/

#ifndef SF_MINI_CORE_H
#define SF_MINI_CORE_H

/* Warning, this file is autogenerated by cbindgen. Don't modify this manually.
*/

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

/**
* Returns the full version string for sf_core.
*
* This function returns a pointer to a static null-terminated string
* containing the version of sf_core.
*
* @return A pointer to a static string containing the version.
* The caller must NOT free this pointer.
* The returned string is valid for the lifetime of the program.
*
* @note Thread-safe: Yes
* @note This function never returns NULL
*
* Example usage:
* @code
* const char* version = sf_core_full_version();
* printf("Version: %s\n", version);
* @endcode
*
* # Safety
*
* The returned pointer points to a static string that is valid for the lifetime
* of the program. The caller must not free the returned pointer.
*/
const char *sf_core_full_version(void);

#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus

#endif /* SF_MINI_CORE_H */
2 changes: 2 additions & 0 deletions src/snowflake/connector/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class TelemetryField(Enum):
PANDAS_WRITE = "client_write_pandas"
# imported packages along with client
IMPORTED_PACKAGES = "client_imported_packages"
# Core import
CORE_IMPORT = "mini_core_import"
# multi-statement usage
MULTI_STATEMENT = "client_multi_statement_query"
# Keys for telemetry data sent through either in-band or out-of-band telemetry
Expand Down
30 changes: 30 additions & 0 deletions test/integ/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,36 @@ def check_packages(message: str, expected_packages: list[str]) -> bool:
assert len(telemetry_test.records) == 0


@pytest.mark.skipolddriver
def test_minicore_import_telemetry(conn_cnx, capture_sf_telemetry):
"""Test that minicore import telemetry (CORE_LOAD_ERROR and CORE_VERSION) is logged."""
with (
conn_cnx() as conn,
capture_sf_telemetry.patch_connection(conn, False) as telemetry_test,
):
conn._log_minicore_import()
assert len(telemetry_test.records) > 0
# Check that the telemetry record contains the proper structure
found_minicore_telemetry = False
for t in telemetry_test.records:
if (
t.message.get(TelemetryField.KEY_TYPE.value)
== TelemetryField.CORE_IMPORT.value
and TelemetryField.KEY_VALUE.value in t.message
):
found_minicore_telemetry = True
# Verify that the value contains CORE_LOAD_ERROR and CORE_VERSION
value = t.message[TelemetryField.KEY_VALUE.value]
assert (
"CORE_LOAD_ERROR" in value
), "CORE_LOAD_ERROR not in telemetry value"
assert "CORE_VERSION" in value, "CORE_VERSION not in telemetry value"
break
assert (
found_minicore_telemetry
), "Minicore telemetry not found in telemetry records"


@pytest.mark.skipolddriver
def test_disable_query_context_cache(conn_cnx) -> None:
with conn_cnx(disable_query_context_cache=True) as conn:
Expand Down
Loading
Loading