Skip to content

Commit 0817f9d

Browse files
authored
Merge pull request #908 from karrioapi/patch-2025.5rc35
Patch 2025.5rc35
2 parents 1752da4 + 2783f95 commit 0817f9d

File tree

182 files changed

+7378
-1494
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+7378
-1494
lines changed

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,50 @@
1+
# Karrio 2025.5rc45
2+
3+
## Changes
4+
5+
### Feat
6+
7+
- feat: introduce shipping rules, app management, and improve types setup scripting
8+
- feat: introduce bulk print and shipment CSV export
9+
- feat: add landmark_maxipak_scan_pddp for LGINTBPMO landmark service
10+
- feat: add support for manual organization creation in the dashboard
11+
- feat: add sensitive default to DHL Parcel DE and missing required exportType
12+
- feat: introduce package_configuration field to shipping methods
13+
- feat: improve dhl parcel error parsing
14+
- feat: add full Sentry features
15+
16+
### Fix
17+
18+
- fix: regression for final N+1 manager models solution
19+
- fix: slow archiving queries
20+
- fix: database lock issue with SQLite-based worker
21+
- fix: N+1 issue with tenants resolution
22+
- fix: invalid GraphQL query filter applied to Logs on GraphQL
23+
- fix: JSONDecode issue with Sapient auth error
24+
- fix: N+1 issue with `constance` config
25+
- fix: shipment cancellation webhook notification unparsable format data
26+
- fix: N+1 issue for all carrier_config affected models
27+
- fix: N+1 issue with the tracker's background update
28+
- fix: Sentry caught a validator issue
29+
- fix: solve the orders duplication and race conditions issues
30+
- fix: N+1 issues across Models and GraphQL queries
31+
- fix: tests and installation for Teleship to be released
32+
- fix: OAuth token expiry calculation and add comprehensive error handling
33+
- fix: exclude customs node completely for DHL Parcel national label requests
34+
- fix: download header content causing failure in production
35+
- fix: AttributeError: 'ShipmentSerializer' object has no attribute 'Meta', overshadowing root cause error
36+
- fix: regression in custom rate resolution when dimensions are absent
37+
- fix: remove Redis health check when using granular parameters
38+
- fix: Redis configuration for health check and task queue
39+
- fix: regression redis connection setup for worker and API
40+
- fix: unstable state management for connections and method forms
41+
42+
### Chores
43+
44+
- chore: clean up dhl parcel and landmark tests to match coding design
45+
- chore: add data isolation tests for contextual carrier configs
46+
- chore: customize karrio admin session cookies to prevent clash with ORY sessions
47+
148
# Karrio 2025.5rc34
249

350
## Changes

apps/api/karrio/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2025.5rc34
1+
2025.5rc35

apps/api/karrio/server/settings/apm.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,19 @@
4949
dsn=SENTRY_DSN,
5050
integrations=integrations,
5151
# Set traces_sample_rate to 1.0 to capture 100%
52-
# of transactions for performance monitoring.
53-
# We recommend adjusting this value in production,
52+
# of transactions for tracing.
5453
traces_sample_rate=1.0,
54+
# Set profile_session_sample_rate to 1.0 to profile 100%
55+
# of profile sessions.
56+
profile_session_sample_rate=1.0,
57+
# Set profile_lifecycle to "trace" to automatically
58+
# run the profiler on when there is an active transaction
59+
profile_lifecycle="trace",
5560
# If you wish to associate users to errors (assuming you are using
5661
# django.contrib.auth) you may enable sending PII data.
5762
send_default_pii=True,
63+
# Enable sending logs to Sentry
64+
enable_logs=True,
5865
)
5966

6067

apps/api/karrio/server/settings/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
# HTTPS configuration
6868
if USE_HTTPS is True:
6969
global SECURE_SSL_REDIRECT
70+
global SECURE_REDIRECT_EXEMPT
7071
global SECURE_PROXY_SSL_HEADER
7172
global SESSION_COOKIE_SECURE
7273
global SECURE_HSTS_SECONDS
@@ -75,6 +76,8 @@
7576
global SECURE_HSTS_PRELOAD
7677

7778
SECURE_SSL_REDIRECT = True
79+
# Exempt health check endpoint from HTTPS redirect for Kubernetes probes
80+
SECURE_REDIRECT_EXEMPT = [r'^status/$']
7881
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
7982
SESSION_COOKIE_SECURE = True
8083
SECURE_HSTS_SECONDS = 1
@@ -385,6 +388,16 @@
385388
AUTH_USER_MODEL = "user.User"
386389

387390

391+
# Session configuration
392+
# Use a unique session cookie name to prevent conflicts with other applications
393+
# (e.g., ORY Kratos sessions in the shipping-app frontend)
394+
SESSION_COOKIE_NAME = config("SESSION_COOKIE_NAME", default="karrio_sessionid")
395+
SESSION_COOKIE_PATH = config("SESSION_COOKIE_PATH", default="/")
396+
SESSION_COOKIE_HTTPONLY = True
397+
SESSION_COOKIE_SAMESITE = config("SESSION_COOKIE_SAMESITE", default="Lax")
398+
# SESSION_COOKIE_DOMAIN is intentionally not set to allow per-host cookies
399+
400+
388401
# Internationalization
389402
# https://docs.djangoproject.com/en/3.0/topics/i18n/
390403

apps/api/karrio/server/settings/cache.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
# Detect if running as a worker process (via run_huey command)
1515
IS_WORKER_PROCESS = any("run_huey" in arg for arg in sys.argv)
1616

17-
# Skip default Redis cache configuration if in worker mode
18-
# Workers only need HUEY Redis, not the default Django cache
19-
SKIP_DEFAULT_CACHE = DETACHED_WORKER or IS_WORKER_PROCESS
17+
# Skip default Redis cache configuration only for worker processes
18+
# API servers should use Redis cache even when DETACHED_WORKER is True
19+
SKIP_DEFAULT_CACHE = IS_WORKER_PROCESS
2020

2121
# Redis configuration - REDIS_URL takes precedence and supersedes granular env vars
2222
REDIS_URL = config("REDIS_URL", default=None)
@@ -60,8 +60,16 @@
6060

6161
# Configure Django cache if Redis is available and not in worker mode
6262
if REDIS_HOST is not None and not SKIP_DEFAULT_CACHE:
63-
HEALTH_CHECK_APPS += ["health_check.contrib.redis"]
64-
INSTALLED_APPS += ["health_check.contrib.redis"]
63+
# Configure connection pool with max_connections to prevent exhaustion
64+
# Default: 50 connections per process (2 Gunicorn workers = 100 total)
65+
# Azure Redis Basic: 256 max connections total
66+
REDIS_CACHE_MAX_CONNECTIONS = config(
67+
"REDIS_CACHE_MAX_CONNECTIONS", default=50, cast=int
68+
)
69+
70+
pool_kwargs = {"max_connections": REDIS_CACHE_MAX_CONNECTIONS}
71+
if REDIS_SSL:
72+
pool_kwargs["ssl_cert_reqs"] = None
6573

6674
CACHES = {
6775
"default": {
@@ -78,6 +86,14 @@
7886
"KEY_PREFIX": REDIS_PREFIX,
7987
}
8088
}
89+
90+
# Django cache health check uses the cache backend directly
91+
# Only add Redis health check if REDIS_URL environment variable is set
92+
# When using granular params, the cache check is sufficient
93+
if config("REDIS_URL", default=None) is not None:
94+
HEALTH_CHECK_APPS += ["health_check.contrib.redis"]
95+
INSTALLED_APPS += ["health_check.contrib.redis"]
96+
8197
print(f"Redis cache connection initialized")
8298
elif SKIP_DEFAULT_CACHE:
8399
print(

apps/api/karrio/server/settings/workers.py

Lines changed: 110 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -22,117 +22,118 @@
2222
)
2323

2424
# Detect if running as a worker process (via run_huey command)
25-
# Workers always need Huey configured regardless of DETACHED_WORKER setting
2625
IS_WORKER_PROCESS = any("run_huey" in arg for arg in sys.argv)
2726

28-
# Skip Huey configuration only if:
29-
# 1. Running in detached worker mode (DETACHED_WORKER=True)
30-
# 2. AND not running as a worker process (IS_WORKER_PROCESS=False)
31-
# This ensures workers always get Huey configured, but API servers don't when detached
32-
if DETACHED_WORKER and not IS_WORKER_PROCESS:
33-
# API server in detached mode - skip Huey configuration
34-
HUEY = None
35-
else:
36-
# Either: worker process, or API server with embedded workers
37-
# Redis configuration - REDIS_URL takes precedence and supersedes granular env vars
38-
REDIS_URL = decouple.config("REDIS_URL", default=None)
39-
40-
# Parse REDIS_URL or construct from individual parameters
41-
if REDIS_URL is not None:
42-
from urllib.parse import urlparse
43-
44-
parsed = urlparse(REDIS_URL)
45-
46-
# Extract values from REDIS_URL (these supersede granular env vars)
47-
REDIS_HOST = parsed.hostname
48-
REDIS_PORT = parsed.port or 6379
49-
REDIS_USERNAME = parsed.username or "default"
50-
REDIS_PASSWORD = parsed.password
51-
52-
# Determine SSL from URL scheme (rediss:// means SSL is enabled)
53-
REDIS_SCHEME = (
54-
parsed.scheme if parsed.scheme in ("redis", "rediss") else "redis"
55-
)
56-
REDIS_SSL = REDIS_SCHEME == "rediss"
57-
58-
else:
59-
# Fall back to individual parameters
60-
REDIS_HOST = decouple.config("REDIS_HOST", default=None)
61-
REDIS_PORT = decouple.config("REDIS_PORT", default=None)
62-
REDIS_PASSWORD = decouple.config("REDIS_PASSWORD", default=None)
63-
REDIS_USERNAME = decouple.config("REDIS_USERNAME", default="default")
64-
REDIS_SSL = decouple.config("REDIS_SSL", default=False, cast=bool)
65-
66-
# Configure HUEY based on available Redis configuration
67-
if REDIS_HOST is not None:
68-
# Calculate max connections based on environment
69-
# Each worker replica needs: (workers_per_replica + 1 scheduler) connections
70-
# Formula: (worker_replicas * (threads_per_worker + 1)) + api_connections + buffer
71-
# Example: 100 connections = (5 replicas * (8 workers + 1 scheduler)) + 40 API + 15 buffer
72-
REDIS_MAX_CONNECTIONS = decouple.config(
73-
"REDIS_MAX_CONNECTIONS", default=100, cast=int
74-
)
75-
76-
# Connection pool configuration with timeouts
77-
# Use BlockingConnectionPool to wait for available connections instead of failing immediately
78-
pool_kwargs = {
79-
"host": REDIS_HOST,
80-
"port": REDIS_PORT,
81-
"max_connections": REDIS_MAX_CONNECTIONS,
82-
"timeout": 20, # Wait up to 20 seconds for an available connection
83-
# Timeout settings to prevent hung connections
84-
"socket_timeout": 10, # Command execution timeout (seconds)
85-
"socket_connect_timeout": 10, # Connection establishment timeout (seconds)
86-
# Keep connections alive to prevent closure by firewalls/load balancers
87-
"socket_keepalive": True,
88-
# Retry on transient failures
89-
"retry_on_timeout": True,
90-
}
27+
# Always configure Huey for both API servers and workers
28+
# API servers need to enqueue tasks even when DETACHED_WORKER=True
29+
# Only the worker pods will consume tasks
30+
# Redis configuration - REDIS_URL takes precedence and supersedes granular env vars
31+
REDIS_URL = decouple.config("REDIS_URL", default=None)
32+
33+
# Parse REDIS_URL or construct from individual parameters
34+
if REDIS_URL is not None:
35+
from urllib.parse import urlparse
36+
37+
parsed = urlparse(REDIS_URL)
9138

92-
# Add TCP keepalive options if available (Linux/Unix only)
93-
try:
94-
pool_kwargs["socket_keepalive_options"] = {
95-
socket.TCP_KEEPIDLE: 60, # Start keepalive after 60s idle
96-
socket.TCP_KEEPINTVL: 10, # Keepalive interval
97-
socket.TCP_KEEPCNT: 3, # Keepalive probes before timeout
98-
}
99-
except AttributeError:
100-
# TCP keepalive constants not available on this platform
101-
pass
102-
103-
# Add authentication if provided
104-
if REDIS_PASSWORD:
105-
pool_kwargs["password"] = REDIS_PASSWORD
106-
if REDIS_USERNAME:
107-
pool_kwargs["username"] = REDIS_USERNAME
108-
109-
# Add SSL/TLS configuration if enabled
110-
if REDIS_SSL:
111-
# Use SSLConnection class for SSL/TLS connections
112-
pool_kwargs["connection_class"] = redis.SSLConnection
113-
pool_kwargs["ssl_cert_reqs"] = None # For Azure Redis compatibility
114-
115-
# Use BlockingConnectionPool to wait for connections instead of raising errors immediately
116-
pool = redis.BlockingConnectionPool(**pool_kwargs)
117-
118-
HUEY = huey.RedisHuey(
119-
"default",
120-
connection_pool=pool,
121-
**({"immediate": WORKER_IMMEDIATE_MODE} if WORKER_IMMEDIATE_MODE else {}),
122-
)
123-
124-
else:
125-
# No Redis configured, use SQLite
126-
WORKER_DB_DIR = decouple.config("WORKER_DB_DIR", default=settings.WORK_DIR)
127-
WORKER_DB_FILE_NAME = os.path.join(WORKER_DB_DIR, "tasks.sqlite3")
128-
129-
settings.DATABASES["workers"] = {
130-
"NAME": WORKER_DB_FILE_NAME,
131-
"ENGINE": "django.db.backends.sqlite3",
39+
# Extract values from REDIS_URL (these supersede granular env vars)
40+
REDIS_HOST = parsed.hostname
41+
REDIS_PORT = parsed.port or 6379
42+
REDIS_USERNAME = parsed.username or "default"
43+
REDIS_PASSWORD = parsed.password
44+
45+
# Determine SSL from URL scheme (rediss:// means SSL is enabled)
46+
REDIS_SCHEME = (
47+
parsed.scheme if parsed.scheme in ("redis", "rediss") else "redis"
48+
)
49+
REDIS_SSL = REDIS_SCHEME == "rediss"
50+
51+
else:
52+
# Fall back to individual parameters
53+
REDIS_HOST = decouple.config("REDIS_HOST", default=None)
54+
REDIS_PORT = decouple.config("REDIS_PORT", default=None)
55+
REDIS_PASSWORD = decouple.config("REDIS_PASSWORD", default=None)
56+
REDIS_USERNAME = decouple.config("REDIS_USERNAME", default="default")
57+
REDIS_SSL = decouple.config("REDIS_SSL", default=False, cast=bool)
58+
59+
# Configure HUEY based on available Redis configuration
60+
if REDIS_HOST is not None:
61+
# Calculate max connections based on environment
62+
# Each worker replica needs: (workers_per_replica + 1 scheduler) connections
63+
# Formula: (worker_replicas * (threads_per_worker + 1)) + api_connections + buffer
64+
# Example: 100 connections = (5 replicas * (8 workers + 1 scheduler)) + 40 API + 15 buffer
65+
REDIS_MAX_CONNECTIONS = decouple.config(
66+
"REDIS_MAX_CONNECTIONS", default=100, cast=int
67+
)
68+
69+
# Connection pool configuration with timeouts
70+
# Use BlockingConnectionPool to wait for available connections instead of failing immediately
71+
pool_kwargs = {
72+
"host": REDIS_HOST,
73+
"port": REDIS_PORT,
74+
"max_connections": REDIS_MAX_CONNECTIONS,
75+
"timeout": 20, # Wait up to 20 seconds for an available connection
76+
# Timeout settings to prevent hung connections
77+
"socket_timeout": 10, # Command execution timeout (seconds)
78+
"socket_connect_timeout": 10, # Connection establishment timeout (seconds)
79+
# Keep connections alive to prevent closure by firewalls/load balancers
80+
"socket_keepalive": True,
81+
# Retry on transient failures
82+
"retry_on_timeout": True,
83+
}
84+
85+
# Add TCP keepalive options if available (Linux/Unix only)
86+
try:
87+
pool_kwargs["socket_keepalive_options"] = {
88+
socket.TCP_KEEPIDLE: 60, # Start keepalive after 60s idle
89+
socket.TCP_KEEPINTVL: 10, # Keepalive interval
90+
socket.TCP_KEEPCNT: 3, # Keepalive probes before timeout
13291
}
92+
except AttributeError:
93+
# TCP keepalive constants not available on this platform
94+
pass
95+
96+
# Add authentication if provided
97+
if REDIS_PASSWORD:
98+
pool_kwargs["password"] = REDIS_PASSWORD
99+
if REDIS_USERNAME:
100+
pool_kwargs["username"] = REDIS_USERNAME
101+
102+
# Add SSL/TLS configuration if enabled
103+
if REDIS_SSL:
104+
# Use SSLConnection class for SSL/TLS connections
105+
pool_kwargs["connection_class"] = redis.SSLConnection
106+
pool_kwargs["ssl_cert_reqs"] = None # For Azure Redis compatibility
107+
108+
# Use BlockingConnectionPool to wait for connections instead of raising errors immediately
109+
pool = redis.BlockingConnectionPool(**pool_kwargs)
110+
111+
HUEY = huey.RedisHuey(
112+
"default",
113+
connection_pool=pool,
114+
**({"immediate": WORKER_IMMEDIATE_MODE} if WORKER_IMMEDIATE_MODE else {}),
115+
)
133116

134-
HUEY = huey.SqliteHuey(
135-
name="default",
136-
filename=WORKER_DB_FILE_NAME,
137-
**({"immediate": WORKER_IMMEDIATE_MODE} if WORKER_IMMEDIATE_MODE else {}),
138-
)
117+
else:
118+
# No Redis configured, use SQLite
119+
WORKER_DB_DIR = decouple.config("WORKER_DB_DIR", default=settings.WORK_DIR)
120+
WORKER_DB_FILE_NAME = os.path.join(WORKER_DB_DIR, "tasks.sqlite3")
121+
122+
settings.DATABASES["workers"] = {
123+
"NAME": WORKER_DB_FILE_NAME,
124+
"ENGINE": "django.db.backends.sqlite3",
125+
}
126+
127+
# SQLite-specific Huey configuration
128+
# WAL mode (Write-Ahead Logging) enables better concurrency for multiple workers
129+
# Increased timeout helps handle lock contention under concurrent access
130+
HUEY = huey.SqliteHuey(
131+
name="default",
132+
filename=WORKER_DB_FILE_NAME,
133+
# Storage-specific kwargs for better concurrent access handling
134+
journal_mode="wal", # Enable Write-Ahead Logging for better concurrency
135+
timeout=30, # Increase timeout to 30s to handle lock contention with multiple workers
136+
cache_mb=16, # Increase cache size for better performance (default: 8MB)
137+
fsync=False, # Disable forced fsync for better performance (default: False)
138+
**({"immediate": WORKER_IMMEDIATE_MODE} if WORKER_IMMEDIATE_MODE else {}),
139+
)

apps/api/karrio/server/static/karrio/carriers/teleship_icon.svg

Lines changed: 3 additions & 0 deletions
Loading

apps/dashboard/public/carriers/teleship_icon.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "@karrio/core/layouts/create-org-layout";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { dynamicMetadata } from "@karrio/core/components/metadata";
2+
export { default } from "@karrio/core/modules/Registration/create_org";
3+
export const generateMetadata = dynamicMetadata("Create Organization");

0 commit comments

Comments
 (0)