Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,21 @@ jobs:
cd e2e-tests
./wait-for-services.sh

- name: Show container logs on failure
if: failure()
run: |
echo "=== Backend container logs ==="
docker logs backend 2>&1 | tail -500 || true
echo ""
echo "=== Celery container logs ==="
docker logs celery 2>&1 | tail -200 || true
echo ""
echo "=== Web-frontend container logs ==="
docker logs web-frontend 2>&1 | tail -200 || true
echo ""
echo "=== Container status ==="
docker ps -a

- name: Run E2E tests (shard ${{ matrix.shard }})
env:
PUBLIC_BACKEND_URL: http://localhost:8000
Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ dev = [
"openapi-spec-validator==0.7.2",
"pytest-html==4.1.1",
"coverage==7.13.1",
"pytest-split==0.10.0",
"pytest-split==0.11.0",
"pytest-unordered==0.7.0",
"debugpy==1.8.20",
"backports.cached-property==1.0.2",
Expand Down
45 changes: 45 additions & 0 deletions backend/src/baserow/celery_singleton_backend.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.conf import settings
from django.core.cache import cache

from celery_singleton.backends import RedisBackend
from django_redis import get_redis_connection

Expand All @@ -9,3 +12,45 @@ def __init__(self, *args, **kwargs):
"""

self.redis = get_redis_connection("default")


class SingletonAutoRescheduleFlag:
"""
Flag is used to indicate that a task of this type is pending reschedule.

When the task ends, if this flag is set, it will re-schedule itself to
ensure that task is eventually run.
"""

def __init__(self, key: str):
self.key = key

def is_set(self) -> bool:
"""
Checks if the flag is set.

:return: True if the lock is set, False otherwise.
"""

return cache.get(key=self.key) or False

def set(self) -> bool:
"""
Sets the flag for the task, indicating it needs to be rescheduled.

:return: True if the flag was set, False if it was already set.
"""

return cache.set(
key=self.key,
value=True,
timeout=settings.AUTO_INDEX_LOCK_EXPIRY * 2,
)

def clear(self) -> bool:
"""
Clears the flag for the task.
:return: True if the flag was cleared, False otherwise.
"""

return cache.delete(key=self.key)
6 changes: 2 additions & 4 deletions backend/src/baserow/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
if "DATABASE_URL" in os.environ:
DATABASES = {
"default": dj_database_url.parse(os.getenv("DATABASE_URL"), conn_max_age=600)
}
DATABASES = {"default": dj_database_url.parse(os.getenv("DATABASE_URL"))}
else:
DATABASES = {
"default": {
Expand All @@ -246,7 +244,7 @@
if key.startswith("DATABASE_READ_REPLICA_") and key.endswith("_URL"):
suffix = key[len("DATABASE_READ_REPLICA_") : -len("_URL")]
db_key = f"read_{suffix}"
DATABASES[db_key] = dj_database_url.parse(value, conn_max_age=600)
DATABASES[db_key] = dj_database_url.parse(value)
DATABASE_READ_REPLICAS.append(db_key)
elif key.startswith("DATABASE_READ_") and key.endswith("_NAME"):
suffix = key[len("DATABASE_READ_") : -len("_NAME")]
Expand Down
58 changes: 7 additions & 51 deletions backend/src/baserow/contrib/database/search/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from typing import List, Optional

from django.conf import settings
from django.core.cache import cache
from django.db.models import Q

from celery_singleton import DuplicateTaskError, Singleton
from django_cte import With
from loguru import logger

from baserow.celery_singleton_backend import SingletonAutoRescheduleFlag
from baserow.config.celery import app
from baserow.contrib.database.search.models import PendingSearchValueUpdate
from baserow.contrib.database.table.exceptions import TableDoesNotExist
Expand All @@ -17,53 +17,8 @@
PERIODIC_CHECK_TIME_LIMIT = 60 * PERIODIC_CHECK_MINUTES # 15 minutes.


class PendingSearchUpdateFlag:
"""
Flag is used to indicate that a search data update task is pending for a
specific table and it has not been possible to schedule it yet due to a concurrent
task already running for the same table.

When the task ends, if this flag is set, it will re-schedule itself to ensure that
the search data is eventually updated.
"""

def __init__(self, table_id: int):
self.table_id = table_id

@property
def key(self):
"""
Returns the cache key to use for the table lock.
"""

return f"database_search_data_lock_{self.table_id}"

def get(self):
"""
Gets the lock for the search data update task.

:return: True if the lock is set, False otherwise.
"""

return cache.get(key=self.key)

def set(self):
"""
Sets the lock for the search data update task.
"""

return cache.set(
key=self.key,
value=True,
timeout=settings.AUTO_INDEX_LOCK_EXPIRY * 2,
)

def clear(self):
"""
Clears the lock for the search data update task.
"""

return cache.delete(key=self.key)
def _get_singleton_autoreschedule_flag(table_id: int) -> SingletonAutoRescheduleFlag:
return SingletonAutoRescheduleFlag(f"database_search_data_lock_{table_id}")


@app.task(queue="export")
Expand Down Expand Up @@ -114,7 +69,8 @@ def schedule_update_search_data(
# There are new updates pending to be processed, make sure the flag is set
# so the task will be re-scheduled at the end of the current run.
if new_pending_updates:
PendingSearchUpdateFlag(table_id).set()
flag = _get_singleton_autoreschedule_flag(table_id)
flag.set()


@app.task(
Expand Down Expand Up @@ -162,13 +118,13 @@ def update_search_data(table_id: int):
SearchHandler.initialize_missing_search_data(table)

# Make sure newer updates will re-schedule this task at the end if needed.
flag = PendingSearchUpdateFlag(table_id)
flag = _get_singleton_autoreschedule_flag(table_id)
flag.clear()

SearchHandler.process_search_data_updates(table)

# If new updates were queued during processing, schedule another update
if flag.get():
if flag.is_set():
logger.debug(
f"New updates detected, rescheduling the task for table {table_id}."
)
Expand Down
2 changes: 1 addition & 1 deletion backend/src/baserow/core/jobs/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def run_async_job(self, job_id: int):
job.set_state_cancelled()
job.save()
except BaseException as e:
# We also want to catch SystemExit exception here and all other possible
# BaseException allows catching SystemExit exceptions and all other possible
# exceptions to set the job state in a failed state.
error = f"Something went wrong during the {job_type.type} job execution."

Expand Down
2 changes: 1 addition & 1 deletion backend/tests/baserow/core/test_basic_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def test_workspace_member_permission_manager(data_fixture, django_assert_num_que
perm_manager.check_permissions(
user, ListApplicationsWorkspaceOperationType.type, workspace_2, workspace_2
)
except Exception: # noqa:W0718
except Exception: # noqa
...

with django_assert_num_queries(0):
Expand Down
8 changes: 4 additions & 4 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "refactor",
"message": "Debounce AI field value generation, that has been triggered from auto update.",
"issue_origin": "github",
"issue_number": 4317,
"domain": "database",
"bullet_points": [],
"created_at": "2025-12-03"
}
1 change: 1 addition & 0 deletions docs/installation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ The installation methods referred to in the variable descriptions are:
| BASEROW\_OLLAMA\_HOST | Provide an OLLAMA host to allow using OLLAMA for generative AI features like the AI field. | |
| BASEROW\_OLLAMA\_MODELS | Provide a comma separated list of Ollama models (https://ollama.com/library) that you would like to enable in the instance (e.g. `llama2`). Note that this only works if an Ollama host is set. If this variable is not provided, the user won't be able to choose a model. | |
| BASEROW\_AI\_FIELD\_MAX\_CONCURRENT\_GENERATIONS | If AI field values are recalculated in a large number (i.e. recalculating whole table, empty rows, or a selection of rows), this controls the number of concurrent requests issued to AI model to generate values. | 5 |
| BASEROW\_AI\_FIELD\_AUTO\_UPDATE\_DEBOUNCE\_TIME | Debounce time in seconds for AI field updates scheduled from auto-update feature. If AI field has auto-update feature enabled, and many changes occur on fields that are referenced by that AI field, this will delay AI field generation by a number of seconds to accumulate many short updates into one bigger. | 3 |

### Backend Misc Configuration
| Name | Description | Defaults |
Expand Down
Loading
Loading