Skip to content
Open
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
1 change: 1 addition & 0 deletions news/+envvar-durations.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The duration settings read by the app and the state managers take a unit suffix: `SQLALCHEMY_POOL_TIMEOUT`, `REFLEX_SOCKET_INTERVAL` and `REFLEX_SOCKET_TIMEOUT` accept values such as `2m`, and `REFLEX_AUTO_RELOAD_COOLDOWN`, `REFLEX_OPLOCK_HOLD_TIME` and `REFLEX_STATE_MANAGER_DISK_DEBOUNCE` replace the `_MS`/`_SECONDS` names, which still work with a deprecation warning until 1.0. A bare number is read as seconds.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecation warnings no longer point at a pseudo-location such as `<string>` or `<frozen importlib._bootstrap>` when the deprecated call runs inside generated or frozen code; the location now names the first real user file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`REFLEX_AUTO_RELOAD_COOLDOWN`, `REFLEX_OPLOCK_HOLD_TIME` and `REFLEX_STATE_MANAGER_DISK_DEBOUNCE` replace `REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS`, `REFLEX_OPLOCK_HOLD_TIME_MS` and `REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS`, and take a duration such as `250ms` or `5m`. The old names still work and keep counting the unit in their name, with a deprecation warning; they are removed in 1.0.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`SQLALCHEMY_POOL_TIMEOUT`, `REFLEX_BACKEND_COLD_START_TIMEOUT`, `REFLEX_SOCKET_INTERVAL` and `REFLEX_SOCKET_TIMEOUT` are `timedelta` settings, so they accept a unit suffix such as `REFLEX_SOCKET_TIMEOUT=2m`. A bare number is still read as seconds, so existing values keep their meaning.
123 changes: 115 additions & 8 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ class EnvironmentVariables:
SQLALCHEMY_POOL_RECYCLE: EnvVar[int] = env_var(-1)

# The timeout for acquiring a connection from the pool.
SQLALCHEMY_POOL_TIMEOUT: EnvVar[int] = env_var(30)
SQLALCHEMY_POOL_TIMEOUT: EnvVar[timedelta] = env_var(timedelta(seconds=30))

# Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
Expand Down Expand Up @@ -763,8 +763,10 @@ class EnvironmentVariables:
# Enables different behavior for when the backend would do a cold start if it was inactive.
REFLEX_DOES_BACKEND_COLD_START: EnvVar[bool] = env_var(False)

# The timeout for the backend to do a cold start in seconds.
REFLEX_BACKEND_COLD_START_TIMEOUT: EnvVar[int] = env_var(10)
# The timeout for the backend to do a cold start.
REFLEX_BACKEND_COLD_START_TIMEOUT: EnvVar[timedelta] = env_var(
timedelta(seconds=10)
)

# Used by flexgen to enumerate the pages.
REFLEX_ADD_ALL_ROUTES_ENDPOINT: EnvVar[bool] = env_var(False)
Expand All @@ -777,11 +779,15 @@ class EnvironmentVariables:
constants.POLLING_MAX_HTTP_BUFFER_SIZE
)

# The interval to send a ping to the websocket server in seconds.
REFLEX_SOCKET_INTERVAL: EnvVar[int] = env_var(constants.Ping.INTERVAL)
# The interval to send a ping to the websocket server.
REFLEX_SOCKET_INTERVAL: EnvVar[timedelta] = env_var(
timedelta(seconds=constants.Ping.INTERVAL)
)

# The timeout to wait for a pong from the websocket server in seconds.
REFLEX_SOCKET_TIMEOUT: EnvVar[int] = env_var(constants.Ping.TIMEOUT)
# The timeout to wait for a pong from the websocket server.
REFLEX_SOCKET_TIMEOUT: EnvVar[timedelta] = env_var(
timedelta(seconds=constants.Ping.TIMEOUT)
)

# Whether to run Granian in a spawn process. This enables Reflex to pick up on environment variable changes between hot reloads.
REFLEX_STRICT_HOT_RELOAD: EnvVar[bool] = env_var(False)
Expand Down Expand Up @@ -826,9 +832,17 @@ class EnvironmentVariables:
REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True)

# How long to delay writing updated states to disk. (Higher values mean less writes, but more chance of lost data.)
REFLEX_STATE_MANAGER_DISK_DEBOUNCE: EnvVar[timedelta] = env_var(
timedelta(seconds=2)
)

# Deprecated in favour of REFLEX_STATE_MANAGER_DISK_DEBOUNCE.
REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS: EnvVar[float] = env_var(2.0)

# How long to wait between automatic reload on frontend error to avoid reload loops.
REFLEX_AUTO_RELOAD_COOLDOWN: EnvVar[timedelta] = env_var(timedelta(seconds=10))

# Deprecated in favour of REFLEX_AUTO_RELOAD_COOLDOWN.
REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS: EnvVar[int] = env_var(10_000)

# Whether to enable debug logging for the redis state manager.
Expand All @@ -837,7 +851,10 @@ class EnvironmentVariables:
# Whether to opportunistically hold the redis lock to allow fast in-memory access while uncontended.
REFLEX_OPLOCK_ENABLED: EnvVar[bool] = env_var(False)

# How long to opportunistically hold the redis lock in milliseconds (must be less than the token expiration).
# How long to opportunistically hold the redis lock (must be less than the token expiration).
REFLEX_OPLOCK_HOLD_TIME: EnvVar[timedelta] = env_var(timedelta(0))

# Deprecated in favour of REFLEX_OPLOCK_HOLD_TIME.
REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0)

# Extra plugins to append to the config's plugins list.
Expand All @@ -850,6 +867,96 @@ class EnvironmentVariables:

environment = EnvironmentVariables()

# Superseded settings already warned about. A setting has no call site, so the
# per-location dedupe in `console.deprecate` would repeat the warning from every
# code path that reads it.
_WARNED_SUPERSEDED: set[str] = set()


def _duration_setting(
setting: EnvVar[timedelta],
superseded: EnvVar[int] | EnvVar[float],
unit: str,
) -> timedelta:
"""Read a duration setting, honouring the unit-suffixed name it replaced.

*superseded* carried its unit in its name and its value as a bare number, so it
cannot become a duration in place: ``10000`` would read as seconds rather than
the milliseconds it means. It is read in *unit* instead, and only while it is
still set.

Args:
setting: The duration setting to read.
superseded: The setting it replaced, whose value is a count of *unit*.
unit: The suffix of the unit *superseded* counts in, as
:func:`interpret_timedelta_env` accepts it.

Returns:
The configured duration.
"""
if not superseded.is_set():
return setting.get()

if superseded.name not in _WARNED_SUPERSEDED:
_WARNED_SUPERSEDED.add(superseded.name)
from reflex_base.utils import console

# Spell out the exact replacement: renaming the variable without adding
# the unit would silently read its value as seconds. The number comes
# from the parsed value rather than the raw text, because the forms a
# bare int or float accepts are wider than the duration parser's: `.5`,
# `1_000` and `1e3` would all suggest a value that fails to parse.
replacement = f"{superseded.get()}{unit}"
console.deprecate(
feature_name=superseded.name,
reason=f"Set {setting.name}={replacement} instead.",
deprecation_version="0.9.12",
removal_version="1.0",
)
if setting.is_set():
return setting.get()
return superseded.get() * timedelta(**{_TIMEDELTA_UNITS[unit]: 1})


def auto_reload_cooldown() -> timedelta:
"""How long to wait between automatic reloads on a frontend error.

Returns:
The configured duration.
"""
return _duration_setting(
environment.REFLEX_AUTO_RELOAD_COOLDOWN,
environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS,
"ms",
)


def oplock_hold_time() -> timedelta:
"""How long to opportunistically hold the redis lock.

Returns:
The configured duration.
"""
return _duration_setting(
environment.REFLEX_OPLOCK_HOLD_TIME,
environment.REFLEX_OPLOCK_HOLD_TIME_MS,
"ms",
)


def state_manager_disk_debounce() -> timedelta:
"""How long to delay writing updated states to disk.

Returns:
The configured duration.
"""
return _duration_setting(
environment.REFLEX_STATE_MANAGER_DISK_DEBOUNCE,
environment.REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS,
"s",
)


try:
from dotenv import load_dotenv
except ImportError:
Expand Down
9 changes: 9 additions & 0 deletions packages/reflex-base/src/reflex_base/utils/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,15 @@ def _is_framework_filename(filename: str) -> bool:
Returns:
Whether the file lives under one of the excluded framework roots.
"""
# Generated code carries a pseudo-name rather than a path: `<string>` for
# `exec` and a dataclass's generated `__init__`, `<frozen ...>` for the
# import machinery. Neither is a user call site, and treating one as a path
# would resolve it against the cwd, so whether it counted as framework code
# would depend on where the app was started from. Other bracketed names are
# left alone on purpose: `<stdin>` and an `<ipython-input-N-...>` cell are
# exactly where an interactive user would look for their own call.
if filename == "<string>" or filename.startswith("<frozen "):
Comment thread
masenf marked this conversation as resolved.
return True
Comment thread
masenf marked this conversation as resolved.
frame_path = Path(filename).resolve()
return any(
frame_path.is_relative_to(root) for root in _exclude_paths_from_frame_info()
Expand Down
9 changes: 9 additions & 0 deletions packages/reflex-base/src/reflex_base/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,15 @@ def _is_framework_filename(filename: str) -> bool:
Returns:
Whether the file lives under one of the excluded framework roots.
"""
# Generated code carries a pseudo-name rather than a path: `<string>` for
# `exec` and a dataclass's generated `__init__`, `<frozen ...>` for the
# import machinery. Neither is a user call site, and treating one as a path
# would resolve it against the cwd, so whether it counted as framework code
# would depend on where the app was started from. Other bracketed names are
# left alone on purpose: `<stdin>` and an `<ipython-input-N-...>` cell are
# exactly where an interactive user would look for their own call.
if filename == "<string>" or filename.startswith("<frozen "):
return True
frame_path = Path(filename).resolve()
return any(
frame_path.is_relative_to(root) for root in _exclude_paths_from_frame_info()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The connection banner reads `REFLEX_BACKEND_COLD_START_TIMEOUT` as a duration, so it accepts a unit suffix such as `30s`. A bare number is still read as seconds.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from datetime import timedelta

from reflex_base import constants
from reflex_base.components.component import Component
from reflex_base.constants import Dirs, Hooks, Imports
Expand Down Expand Up @@ -124,7 +126,7 @@ def add_hooks(self) -> list[str | Var]:
if ({has_too_many_connection_errors!s}) {{
setWaitedForBackend(true);
}}
}}, {environment.REFLEX_BACKEND_COLD_START_TIMEOUT.get() * 1000});
}}, {environment.REFLEX_BACKEND_COLD_START_TIMEOUT.get() // timedelta(milliseconds=1)});
Comment thread
benedikt-bartscher marked this conversation as resolved.
"""
)
else:
Expand Down
10 changes: 7 additions & 3 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from reflex_base.components.component import Component, ComponentStyle
from reflex_base.config import get_config, reload_config
from reflex_base.context.base import BaseContext
from reflex_base.environment import environment
from reflex_base.environment import auto_reload_cooldown, environment
from reflex_base.event import (
_EVENT_FIELDS,
Event,
Expand Down Expand Up @@ -585,6 +585,10 @@ def _setup_state(self) -> None:
# Set up the state manager.
self._state_manager = StateManager.create()

# Read the auto-reload cooldown now so a deprecated name warns at startup
# rather than on the first frontend error that consults it.
auto_reload_cooldown()

# Set up the Socket.IO AsyncServer.
if not self.sio:
self.sio = AsyncServer(
Expand All @@ -600,8 +604,8 @@ def _setup_state(self) -> None:
),
cors_credentials=config.transport == "websocket",
max_http_buffer_size=environment.REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE.get(),
ping_interval=environment.REFLEX_SOCKET_INTERVAL.get(),
ping_timeout=environment.REFLEX_SOCKET_TIMEOUT.get(),
ping_interval=environment.REFLEX_SOCKET_INTERVAL.get().total_seconds(),
ping_timeout=environment.REFLEX_SOCKET_TIMEOUT.get().total_seconds(),
Comment thread
benedikt-bartscher marked this conversation as resolved.
json=SimpleNamespace(
dumps=staticmethod(_sio_dumps),
loads=staticmethod(_sio_loads),
Expand Down
4 changes: 2 additions & 2 deletions reflex/istate/manager/disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pathlib import Path
from typing import Any, Generic, cast

from reflex_base.environment import environment
from reflex_base.environment import state_manager_disk_debounce
from typing_extensions import Unpack, override

from reflex.istate.manager import (
Expand Down Expand Up @@ -68,7 +68,7 @@ class StateManagerDisk(StateManager):
)
_write_queue_task: asyncio.Task | None = None
_write_debounce_seconds: float = dataclasses.field(
default=environment.REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS.get()
default_factory=lambda: state_manager_disk_debounce().total_seconds()
)

def __post_init__(self):
Expand Down
22 changes: 18 additions & 4 deletions reflex/istate/manager/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import time
import uuid
from collections.abc import AsyncIterator
from datetime import timedelta
from typing import Any, TypedDict, cast

from redis import ResponseError
from redis.asyncio import Redis
from reflex_base.config import get_config
from reflex_base.environment import environment
from reflex_base.environment import environment, oplock_hold_time
from reflex_base.utils.exceptions import (
EnvironmentVarValueError,
InvalidLockWarningThresholdError,
LockExpiredError,
StateSchemaMismatchError,
Expand Down Expand Up @@ -87,10 +89,22 @@ def _default_oplock_hold_time_ms() -> int:

Returns:
The default opportunistic lock hold time.

Raises:
EnvironmentVarValueError: If the configured hold time is negative.
"""
return environment.REFLEX_OPLOCK_HOLD_TIME_MS.get() or (
_default_lock_expiration() // 2
)
hold_time = oplock_hold_time()
if hold_time < timedelta(0):
msg = (
"The opportunistic lock hold time must not be negative, got "
f"{hold_time.total_seconds()} seconds."
)
raise EnvironmentVarValueError(msg)
if not hold_time:
return _default_lock_expiration() // 2
# A configured hold time is worth at least one millisecond, so that a
# sub-millisecond duration is not mistaken for the unset default above.
return max(hold_time // timedelta(milliseconds=1), 1)


# The lock waiter task should subscribe to lock channel updates within this period.
Expand Down
2 changes: 1 addition & 1 deletion reflex/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def get_engine_args(url: str | None = None) -> dict[str, Any]:
"pool_size": environment.SQLALCHEMY_POOL_SIZE.get(),
"max_overflow": environment.SQLALCHEMY_MAX_OVERFLOW.get(),
"pool_recycle": environment.SQLALCHEMY_POOL_RECYCLE.get(),
"pool_timeout": environment.SQLALCHEMY_POOL_TIMEOUT.get(),
"pool_timeout": environment.SQLALCHEMY_POOL_TIMEOUT.get().total_seconds(),
}
conf = get_config()
url = url or conf.db_url
Expand Down
5 changes: 3 additions & 2 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import sys
import time
from collections.abc import Callable, Iterator, Mapping, Sequence
from datetime import timedelta
from hashlib import md5
from types import FunctionType
from typing import (
Expand All @@ -30,7 +31,7 @@

from reflex_base import constants
from reflex_base.constants.state import FIELD_MARKER
from reflex_base.environment import PerformanceMode, environment
from reflex_base.environment import PerformanceMode, auto_reload_cooldown, environment
from reflex_base.event import (
EVENT_ACTIONS_MARKER,
Event,
Expand Down Expand Up @@ -2563,7 +2564,7 @@ def handle_frontend_exception(
):
yield call_script(
f"const last_reload = parseInt(window.sessionStorage.getItem('{LAST_RELOADED_KEY}')) || 0;"
f"if (Date.now() - last_reload > {environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS.get()})"
f"if (Date.now() - last_reload > {auto_reload_cooldown() // timedelta(milliseconds=1)})"
"{"
f"window.sessionStorage.setItem('{LAST_RELOADED_KEY}', Date.now().toString());"
"window.location.reload();"
Expand Down
Loading
Loading