diff --git a/.env.example b/.env.example index ec9813293c..53e8a2ee9f 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,7 @@ DATABASE_NAME=baserow # BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT= # BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS= # BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS= +# BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS= # BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS= # BASEROW_EXTRA_ALLOWED_HOSTS= # ADDITIONAL_APPS= diff --git a/backend/src/baserow/config/settings/base.py b/backend/src/baserow/config/settings/base.py index c64116d69d..77fe6f8f1b 100644 --- a/backend/src/baserow/config/settings/base.py +++ b/backend/src/baserow/config/settings/base.py @@ -809,6 +809,12 @@ def __setitem__(self, key, value): AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS = int( os.getenv("BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS", 5) ) +AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS = int( + os.getenv( + "BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS", + AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS, + ) +) AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS = int( os.getenv("BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS", 5) ) diff --git a/backend/src/baserow/contrib/automation/history/handler.py b/backend/src/baserow/contrib/automation/history/handler.py index 2b14f62f32..cb3e06a67f 100644 --- a/backend/src/baserow/contrib/automation/history/handler.py +++ b/backend/src/baserow/contrib/automation/history/handler.py @@ -28,10 +28,15 @@ def create_workflow_history( workflow: AutomationWorkflow, started_on: datetime, is_test_run: bool, + status: HistoryStatusChoices = HistoryStatusChoices.STARTED, + completed_on: Optional[datetime] = None, + message: str = "", ) -> AutomationWorkflowHistory: return AutomationWorkflowHistory.objects.create( workflow=workflow, started_on=started_on, is_test_run=is_test_run, - status=HistoryStatusChoices.STARTED, + status=status, + completed_on=completed_on, + message=message, ) diff --git a/backend/src/baserow/contrib/automation/workflows/handler.py b/backend/src/baserow/contrib/automation/workflows/handler.py index 222c88ee5c..f8434ee9c6 100644 --- a/backend/src/baserow/contrib/automation/workflows/handler.py +++ b/backend/src/baserow/contrib/automation/workflows/handler.py @@ -61,6 +61,7 @@ ) WORKFLOW_RATE_LIMIT_CACHE_PREFIX = "automation_workflow_{}" +WORKFLOW_HISTORY_RATE_LIMIT_CACHE_PREFIX = "automation_workflow_history_{}" AUTOMATION_WORKFLOW_CACHE_LOCK_SECONDS = 5 tracer = trace.get_tracer(__name__) @@ -686,11 +687,47 @@ def before_run(self, workflow: AutomationWorkflow) -> None: self.reset_workflow_temporary_states(workflow) self._check_too_many_errors(workflow) - self._check_is_rate_limited(workflow.id) def _get_rate_limit_cache_key(self, workflow_id: int) -> str: return WORKFLOW_RATE_LIMIT_CACHE_PREFIX.format(workflow_id) + def _get_workflow_history_rate_limit_cache_key(self, workflow_id: int) -> str: + return WORKFLOW_HISTORY_RATE_LIMIT_CACHE_PREFIX.format(workflow_id) + + def _should_create_rate_limited_workflow_history(self, workflow_id: int) -> bool: + """ + Checks if the workflow history should be created when rate limited. + + Returns True if the history should be created, False otherwise. + """ + + cache_key = self._get_workflow_history_rate_limit_cache_key(workflow_id) + + should_create_history = False + + def _should_create_history(history_exists): + """ + Sets should_create_history to True if this is the first time + we're checking the workflow history in this window. + + Returns True because we always want to set the cache key + when update() is called. + """ + + if not history_exists: + nonlocal should_create_history + should_create_history = True + return True + + global_cache.update( + cache_key, + callback=_should_create_history, + default_value=lambda: False, + timeout=settings.AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS, + ) + + return should_create_history + def _check_is_rate_limited(self, workflow_id: int) -> None: """Uses a global cache key to track recent runs for the given workflow.""" @@ -835,6 +872,22 @@ def async_start_workflow( :param event_payload: The payload from the action. """ + try: + self._check_is_rate_limited(workflow.id) + except AutomationWorkflowRateLimited as e: + if self._should_create_rate_limited_workflow_history(workflow.id): + original_workflow = self.get_original_workflow(workflow) + now = timezone.now() + AutomationHistoryHandler().create_workflow_history( + original_workflow, + is_test_run=original_workflow == workflow, + started_on=now, + completed_on=now, + message=str(e), + status=HistoryStatusChoices.ERROR, + ) + return + start_workflow_celery_task.delay( workflow.id, event_payload, diff --git a/backend/src/baserow/contrib/database/api/fields/serializers.py b/backend/src/baserow/contrib/database/api/fields/serializers.py index 41d0c8aa28..7d1f2faa49 100644 --- a/backend/src/baserow/contrib/database/api/fields/serializers.py +++ b/backend/src/baserow/contrib/database/api/fields/serializers.py @@ -447,11 +447,40 @@ class UniqueRowValuesSerializer(serializers.Serializer): values = serializers.ListSerializer(child=serializers.CharField()) +@extend_schema_field(OpenApiTypes.INT) +class CollaboratorField(serializers.Field): + """ + A serializer field that accepts an int or a dict with an "id" field. + """ + + def to_internal_value(self, data): + if isinstance(data, int) or (isinstance(data, str) and data.isdigit()): + return int(data) + + if isinstance(data, dict): + try: + return int(data["id"]) + except (KeyError, TypeError, ValueError): + pass + + raise serializers.ValidationError( + "Expected an integer or an object with an 'id' field", + code="invalid", + ) + + def to_representation(self, value): + return value + + class CollaboratorSerializer(serializers.Serializer): id = serializers.IntegerField() name = serializers.CharField(source="first_name", read_only=True) +class CollaboratorRequestSerializer(serializers.ListSerializer): + child = CollaboratorField() + + class AvailableCollaboratorsSerializer(serializers.ListField): def __init__(self, **kwargs): kwargs["child"] = CollaboratorSerializer() diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index 5a66051a71..8a2aa70f52 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -6744,18 +6744,13 @@ def can_represent_collaborators(self, field): return True def get_serializer_field(self, instance, **kwargs): - required = kwargs.pop("required", False) - field_serializer = CollaboratorSerializer( - **{ - "required": required, - "allow_null": False, - **kwargs, - } - ) - return serializers.ListSerializer( - child=field_serializer, required=required, **kwargs + from baserow.contrib.database.api.fields.serializers import ( + CollaboratorRequestSerializer, ) + kwargs.setdefault("required", False) + return CollaboratorRequestSerializer(**kwargs) + def get_search_expression( self, field: MultipleCollaboratorsField, queryset: QuerySet ) -> Expression: diff --git a/backend/tests/baserow/contrib/automation/history/utils.py b/backend/tests/baserow/contrib/automation/history/utils.py new file mode 100644 index 0000000000..66b38aeedb --- /dev/null +++ b/backend/tests/baserow/contrib/automation/history/utils.py @@ -0,0 +1,13 @@ +from baserow.contrib.automation.history.models import AutomationWorkflowHistory + + +def assert_history(workflow, expected_count, expected_status, expected_msg): + """Helper to test AutomationWorkflowHistory objects.""" + + histories = AutomationWorkflowHistory.objects.filter(workflow=workflow) + assert len(histories) == expected_count + if expected_count > 0: + history = histories[0] + assert history.workflow == workflow + assert history.status == expected_status + assert history.message == expected_msg diff --git a/backend/tests/baserow/contrib/automation/workflows/test_workflow_handler.py b/backend/tests/baserow/contrib/automation/workflows/test_workflow_handler.py index 3bb0fe50aa..1cc1b944df 100644 --- a/backend/tests/baserow/contrib/automation/workflows/test_workflow_handler.py +++ b/backend/tests/baserow/contrib/automation/workflows/test_workflow_handler.py @@ -26,6 +26,7 @@ ) from baserow.contrib.automation.workflows.handler import AutomationWorkflowHandler from baserow.core.trash.handler import TrashHandler +from tests.baserow.contrib.automation.history.utils import assert_history WORKFLOWS_MODULE = "baserow.contrib.automation.workflows" HANDLER_MODULE = f"{WORKFLOWS_MODULE}.handler" @@ -569,6 +570,71 @@ def test_check_is_rate_limited_raises_if_above_limit(): ) +@pytest.mark.django_db +@override_settings( + AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS=5, + AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS=2, +) +@patch(f"{WORKFLOWS_MODULE}.handler.start_workflow_celery_task") +def test_workflow_rate_limiter_is_checked_before_starting_celery_task( + mock_celery_task, data_fixture +): + user = data_fixture.create_user() + + original_workflow = data_fixture.create_automation_workflow(user=user) + published_workflow = data_fixture.create_automation_workflow( + state=WorkflowState.LIVE, user=user + ) + published_workflow.automation.published_from = original_workflow + published_workflow.automation.save() + + handler = AutomationWorkflowHandler() + rate_limited_error = "The workflow was rate limited due to too many recent runs." + + with freeze_time("2026-01-26 13:00:00"): + # First 2 calls should queue workflow runs + handler.async_start_workflow(published_workflow) + handler.async_start_workflow(published_workflow) + assert mock_celery_task.delay.call_count == 2 + assert_history(original_workflow, 0, "error", rate_limited_error) + + # 3rd call should be rate limited + handler.async_start_workflow(published_workflow) + assert mock_celery_task.delay.call_count == 2 + assert_history(original_workflow, 1, "error", rate_limited_error) + + +@pytest.mark.django_db +@override_settings( + AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS=5, +) +@patch(f"{WORKFLOWS_MODULE}.handler.start_workflow_celery_task") +def test_should_create_rate_limited_workflow_history(mock_celery_task, data_fixture): + workflow_id = 99999 + handler = AutomationWorkflowHandler() + + with freeze_time("2026-01-26 13:00:00"): + # True because this is the first time we're attempting to create a history + assert handler._should_create_rate_limited_workflow_history(workflow_id) is True + + # False because a history already exists within the expiry window + assert ( + handler._should_create_rate_limited_workflow_history(workflow_id) is False + ) + assert ( + handler._should_create_rate_limited_workflow_history(workflow_id) is False + ) + + with freeze_time("2026-01-26 13:00:06"): + # True because the cache window of 5 seconds has expired + assert handler._should_create_rate_limited_workflow_history(workflow_id) is True + + # False because a new history was created via the previous call to the method + assert ( + handler._should_create_rate_limited_workflow_history(workflow_id) is False + ) + + @pytest.mark.django_db def test_disable_workflow_disables_original_workflow(data_fixture): original_workflow = data_fixture.create_automation_workflow() diff --git a/backend/tests/baserow/contrib/automation/workflows/test_workflow_tasks.py b/backend/tests/baserow/contrib/automation/workflows/test_workflow_tasks.py index 1d60adf614..359c9bacb8 100644 --- a/backend/tests/baserow/contrib/automation/workflows/test_workflow_tasks.py +++ b/backend/tests/baserow/contrib/automation/workflows/test_workflow_tasks.py @@ -1,13 +1,10 @@ from unittest.mock import patch -from django.test import override_settings - import pytest from baserow.contrib.automation.history.models import AutomationWorkflowHistory from baserow.contrib.automation.workflows.constants import WorkflowState from baserow.contrib.automation.workflows.exceptions import ( - AutomationWorkflowRateLimited, AutomationWorkflowTooManyErrors, ) from baserow.contrib.automation.workflows.handler import AutomationWorkflowHandler @@ -110,63 +107,6 @@ def test_run_workflow_unexpected_error_creates_workflow_history( mock_logger.exception.assert_called_once_with(error_msg) -def assert_history(workflow, expected_count, expected_status, expected_msg): - histories = AutomationWorkflowHistory.objects.filter(workflow=workflow) - assert len(histories) == expected_count - history = histories[0] - assert history.workflow == workflow - assert history.status == expected_status - assert history.message == expected_msg - - -@pytest.mark.django_db -@override_settings( - AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS=3, - AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS=10, -) -@patch( - "baserow.contrib.automation.workflows.handler.AutomationWorkflowHandler._check_is_rate_limited" -) -@patch("baserow.contrib.automation.nodes.handler.AutomationNodeHandler.dispatch_node") -def test_run_workflow_disables_workflow_if_too_many_errors( - mock_dispatch_node, mock_is_rate_limited, data_fixture -): - mock_is_rate_limited.side_effect = AutomationWorkflowRateLimited( - "mock rate limited error" - ) - - original_workflow = data_fixture.create_automation_workflow() - published_workflow = data_fixture.create_automation_workflow( - state=WorkflowState.LIVE - ) - published_workflow.automation.published_from = original_workflow - published_workflow.automation.save() - - # The first 3 runs should just be an error - for i in range(3): - start_workflow_celery_task(published_workflow.id, False, None) - mock_dispatch_node.assert_not_called() - assert_history(original_workflow, i + 1, "error", "mock rate limited error") - original_workflow.refresh_from_db() - published_workflow.refresh_from_db() - assert original_workflow.state == WorkflowState.DRAFT - assert published_workflow.state == WorkflowState.LIVE - - # The fourth run should disable the workflow due to too many errors - start_workflow_celery_task(published_workflow.id, False, None) - mock_dispatch_node.assert_not_called() - assert_history( - original_workflow, - 4, - "disabled", - f"The workflow {original_workflow.id} was disabled due to too many consecutive errors.", - ) - original_workflow.refresh_from_db() - published_workflow.refresh_from_db() - assert original_workflow.state == WorkflowState.DISABLED - assert published_workflow.state == WorkflowState.DISABLED - - @pytest.mark.django_db @patch( "baserow.contrib.automation.workflows.handler.AutomationWorkflowHandler._check_too_many_errors" diff --git a/backend/tests/baserow/contrib/database/api/fields/test_collaborator_serializers.py b/backend/tests/baserow/contrib/database/api/fields/test_collaborator_serializers.py new file mode 100644 index 0000000000..c80019c839 --- /dev/null +++ b/backend/tests/baserow/contrib/database/api/fields/test_collaborator_serializers.py @@ -0,0 +1,54 @@ +import pytest +from rest_framework import serializers + +from baserow.contrib.database.api.fields.serializers import ( + CollaboratorField, + CollaboratorRequestSerializer, +) + + +def test_collaborator_request_serializer_with_list_of_ints(): + result = CollaboratorRequestSerializer().to_internal_value([1, 2, 3]) + assert result == [1, 2, 3] + + +def test_collaborator_request_serializer_with_list_of_dicts(): + result = CollaboratorRequestSerializer().to_internal_value([{"id": 1}, {"id": 2}]) + assert result == [1, 2] + + +def test_collaborator_request_serializer_with_ints_and_dicts(): + serializer = CollaboratorRequestSerializer() + result = serializer.to_internal_value([1, {"id": 2}, "3"]) + assert result == [1, 2, 3] + + +def test_collaborator_field_int(): + assert CollaboratorField().to_internal_value(100) == 100 + + +def test_collaborator_field_numeric_string(): + assert CollaboratorField().to_internal_value("200") == 200 + + +def test_collaborator_field_dict_with_int_id(): + assert CollaboratorField().to_internal_value({"id": 300}) == 300 + + +def test_collaborator_field_dict_with_string_id(): + assert CollaboratorField().to_internal_value({"id": "404"}) == 404 + + +def test_collaborator_field_dict_without_id(): + with pytest.raises(serializers.ValidationError): + CollaboratorField().to_internal_value({"foo": "bar"}) + + +def test_collaborator_field_invalid_type(): + with pytest.raises(serializers.ValidationError): + CollaboratorField().to_internal_value(["foo"]) + + +def test_collaborator_field_non_numeric_string(): + with pytest.raises(serializers.ValidationError): + CollaboratorField().to_internal_value("foo bar") diff --git a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py index 8499103458..6b1b83eb3d 100644 --- a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py @@ -50,6 +50,29 @@ def test_multiple_collaborators_field_type_create(data_fixture): assert collaborator_field_list[0].id == user.id +@pytest.mark.django_db +@pytest.mark.field_multiple_collaborators +def test_multiple_collaborators_field_type_create_with_int(data_fixture): + user = data_fixture.create_user() + database = data_fixture.create_database_application(user=user, name="Placeholder") + table = data_fixture.create_database_table(name="Example", database=database) + + row_handler = RowHandler() + + collaborator_field = data_fixture.create_multiple_collaborators_field( + user=user, table=table, name="Collaborator 1" + ) + field_id = collaborator_field.db_column + + assert MultipleCollaboratorsField.objects.all().first().id == collaborator_field.id + + row = row_handler.create_row(user=user, table=table, values={field_id: [user.id]}) + assert row.id + collaborator_field_list = getattr(row, field_id).all() + assert len(collaborator_field_list) == 1 + assert collaborator_field_list[0].id == user.id + + @pytest.mark.django_db @pytest.mark.field_multiple_collaborators def test_multiple_collaborators_field_type_update(data_fixture): diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py index 2035999cc7..4d2fc33aab 100644 --- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py +++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py @@ -833,3 +833,43 @@ def test_extract_properties_returns_expected_list(path, expected): result = service_type.extract_properties(path) assert result == expected + + +@pytest.mark.django_db +def test_local_baserow_upsert_row_service_dispatch_data_with_collaborators( + data_fixture, +): + user = data_fixture.create_user() + page = data_fixture.create_builder_page(user=user) + integration = data_fixture.create_local_baserow_integration( + application=page.builder, user=user + ) + database = data_fixture.create_database_application( + workspace=page.builder.workspace + ) + table = TableHandler().create_table_and_fields( + user=user, + database=database, + name=data_fixture.fake.name(), + fields=[ + ("Collaborators", "multiple_collaborators", {}), + ], + ) + collaborator_field = table.field_set.get(name="Collaborators") + + service = data_fixture.create_local_baserow_upsert_row_service( + integration=integration, + table=table, + ) + service_type = service.get_type() + # Simulate a user ID as a string + service.field_mappings.create(field=collaborator_field, value=f'"{user.id}"') + + dispatch_context = FakeDispatchContext() + dispatch_values = service_type.resolve_service_formulas(service, dispatch_context) + dispatch_data = service_type.dispatch_data( + service, dispatch_values, dispatch_context + ) + + collaborators = getattr(dispatch_data["data"], collaborator_field.db_column).all() + assert list(collaborators.values_list("id", flat=True)) == [user.id] diff --git a/changelog/entries/unreleased/bug/3954_fixed_a_bug_preventing_collaborator_field_from_being_upserte.json b/changelog/entries/unreleased/bug/3954_fixed_a_bug_preventing_collaborator_field_from_being_upserte.json new file mode 100644 index 0000000000..3a4006d36a --- /dev/null +++ b/changelog/entries/unreleased/bug/3954_fixed_a_bug_preventing_collaborator_field_from_being_upserte.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed a bug preventing Collaborator field from being upserted by user ID.", + "issue_origin": "github", + "issue_number": 3954, + "domain": "database", + "bullet_points": [], + "created_at": "2026-01-28" +} diff --git a/changelog/entries/unreleased/bug/3954_fixed_a_bug_where_the_collaborators_field_couldnt_be_updated.json b/changelog/entries/unreleased/bug/3954_fixed_a_bug_where_the_collaborators_field_couldnt_be_updated.json new file mode 100644 index 0000000000..47c519d5d4 --- /dev/null +++ b/changelog/entries/unreleased/bug/3954_fixed_a_bug_where_the_collaborators_field_couldnt_be_updated.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed a bug where the collaborators field couldn't be updated via upsert actions.", + "issue_origin": "github", + "issue_number": 3954, + "domain": "integration", + "bullet_points": [], + "created_at": "2026-01-19" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/refactor/4582_improve_rate_limiter_by_checking_it_before_queuing_celery_ta.json b/changelog/entries/unreleased/refactor/4582_improve_rate_limiter_by_checking_it_before_queuing_celery_ta.json new file mode 100644 index 0000000000..838f2e7581 --- /dev/null +++ b/changelog/entries/unreleased/refactor/4582_improve_rate_limiter_by_checking_it_before_queuing_celery_ta.json @@ -0,0 +1,9 @@ +{ + "type": "refactor", + "message": "Improved workflow rate limiter performance by checking limits before queuing tasks.", + "issue_origin": "github", + "issue_number": 4582, + "domain": "automation", + "bullet_points": [], + "created_at": "2026-01-26" +} \ No newline at end of file diff --git a/docker-compose.no-caddy.yml b/docker-compose.no-caddy.yml index 21bf7407e4..fc80a44130 100644 --- a/docker-compose.no-caddy.yml +++ b/docker-compose.no-caddy.yml @@ -85,6 +85,7 @@ x-backend-variables: BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS: + BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS: BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS: BASEROW_EXTRA_ALLOWED_HOSTS: @@ -240,6 +241,7 @@ services: BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS: + BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS: BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS: depends_on: - backend diff --git a/docker-compose.yml b/docker-compose.yml index 0765515304..1e31d0056b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -98,6 +98,7 @@ x-backend-variables: BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS: + BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS: BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS: BASEROW_EXTRA_ALLOWED_HOSTS: @@ -318,6 +319,7 @@ services: BASEROW_AUTOMATION_HISTORY_PAGE_SIZE_LIMIT: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_MAX_RUNS: BASEROW_AUTOMATION_WORKFLOW_RATE_LIMIT_CACHE_EXPIRY_SECONDS: + BASEROW_AUTOMATION_WORKFLOW_HISTORY_RATE_LIMIT_CACHE_EXPIRY_SECONDS: BASEROW_AUTOMATION_WORKFLOW_MAX_CONSECUTIVE_ERRORS: BASEROW_INTEGRATIONS_PERIODIC_MINUTE_MIN: BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL: