django-lambda-tasks 0.6.4__tar.gz → 0.6.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/steering/product.md +1 -1
  2. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/PKG-INFO +1 -1
  3. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/handler.py +13 -10
  4. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/pyproject.toml +1 -1
  5. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_memory_limit.py +61 -29
  6. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.github/workflows/ci.yml +0 -0
  7. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.github/workflows/release.yml +0 -0
  8. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.gitignore +0 -0
  9. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  10. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/async-local-execution/design.md +0 -0
  11. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/async-local-execution/requirements.md +0 -0
  12. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/async-local-execution/tasks.md +0 -0
  13. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  14. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  15. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  16. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  17. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  18. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  19. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  20. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  21. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  22. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  23. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  24. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  25. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  26. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  27. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  28. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  29. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/retry-delay/.config.kiro +0 -0
  30. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/retry-delay/design.md +0 -0
  31. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/retry-delay/requirements.md +0 -0
  32. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/retry-delay/tasks.md +0 -0
  33. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  34. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks/design.md +0 -0
  35. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  36. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  37. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  38. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  39. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  40. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  41. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/singleton-task/.config.kiro +0 -0
  42. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/singleton-task/design.md +0 -0
  43. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/singleton-task/requirements.md +0 -0
  44. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/singleton-task/tasks.md +0 -0
  45. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  46. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  47. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  48. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  49. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/task-retry/.config.kiro +0 -0
  50. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/task-retry/design.md +0 -0
  51. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/task-retry/requirements.md +0 -0
  52. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/specs/task-retry/tasks.md +0 -0
  53. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/steering/structure.md +0 -0
  54. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.kiro/steering/tech.md +0 -0
  55. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.pre-commit-config.yaml +0 -0
  56. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/.vscode/settings.json +0 -0
  57. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/README.md +0 -0
  58. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/README.md +0 -0
  59. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_app/__init__.py +0 -0
  60. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_app/apps.py +0 -0
  61. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_app/tasks.py +0 -0
  62. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_app/urls.py +0 -0
  63. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_app/views.py +0 -0
  64. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_project/__init__.py +0 -0
  65. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_project/settings.py +0 -0
  66. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_project/urls.py +0 -0
  67. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/example_project/wsgi.py +0 -0
  68. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/example/manage.py +0 -0
  69. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/__init__.py +0 -0
  70. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/admin.py +0 -0
  71. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/apps.py +0 -0
  72. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/decorators.py +0 -0
  73. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/environment_loader.py +0 -0
  74. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/local_executor.py +0 -0
  75. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/logging.py +0 -0
  76. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/migrations/0001_initial.py +0 -0
  77. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  78. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/migrations/__init__.py +0 -0
  79. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/models.py +0 -0
  80. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/secret_loader.py +0 -0
  81. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/settings.py +0 -0
  82. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/tasks.py +0 -0
  83. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/lambda_tasks/timeouts.py +0 -0
  84. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/conftest.py +0 -0
  85. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/settings.py +0 -0
  86. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_admin.py +0 -0
  87. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_batch.py +0 -0
  88. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_batch_worker.py +0 -0
  89. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_decorator.py +0 -0
  90. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_decorators.py +0 -0
  91. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_deferred_enqueue.py +0 -0
  92. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_environment_loader.py +0 -0
  93. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_handler.py +0 -0
  94. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_kwargs_only.py +0 -0
  95. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_local_executor.py +0 -0
  96. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_logging.py +0 -0
  97. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_models.py +0 -0
  98. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_secret_loader.py +0 -0
  99. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_serializer.py +0 -0
  100. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_settings.py +0 -0
  101. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_tasks.py +0 -0
  102. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_timeout_validation.py +0 -0
  103. {django_lambda_tasks-0.6.4 → django_lambda_tasks-0.6.6}/tests/test_timeouts.py +0 -0
@@ -151,7 +151,7 @@ class SQSLambdaTaskMessage(BaseModel):
151
151
  - A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
152
152
  - Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
153
153
  - A temporary `StreamHandler` is attached to the `lambda_tasks` logger for the duration of the loaders so their log output is visible before Django's `LOGGING` dictConfig has run; it is removed immediately after so that Django's configuration is the sole authority on logging from that point on
154
- - `resource.setrlimit(RLIMIT_DATA)` is set from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` so that excessive allocation raises `MemoryError` instead of triggering the OOM killer
154
+ - `resource.setrlimit(RLIMIT_AS)` is set from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` (Lambda) or the ECS task metadata endpoint (Fargate/Batch) so that excessive allocation raises `MemoryError` instead of triggering the OOM killer
155
155
 
156
156
  `main()` in `handler.py` — AWS Batch container entry point (`python -m lambda_tasks.handler`):
157
157
  - Reads `LAMBDA_TASKS_MESSAGE` env var (the serialized task JSON, set by `submit_batch_job` via container overrides)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.6.4
3
+ Version: 0.6.6
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -12,12 +12,13 @@ timeouts. The sequence is guarded by a module-level sentinel so subsequent
12
12
  warm invocations skip it.
13
13
  """
14
14
 
15
+ import json
15
16
  import logging
16
17
  import os
17
18
  import resource
18
19
  import sys
20
+ import urllib.request
19
21
  import uuid
20
- from pathlib import Path
21
22
 
22
23
  import django
23
24
  from django.apps import apps
@@ -32,29 +33,31 @@ _cold_start_done: bool = False
32
33
 
33
34
  _MEMORY_RESERVED_MB = 128
34
35
  _MEMORY_MINIMUM_LIMIT_MB = 64
35
- _CGROUP_MEMORY_MAX_PATH = Path("/sys/fs/cgroup/memory.max")
36
36
 
37
37
 
38
38
  def _get_memory_mb() -> int | None:
39
- """Get container memory limit from Lambda env var or cgroup v2."""
39
+ """Get container memory limit from Lambda env var or ECS metadata endpoint."""
40
40
  memory_mb_str = os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
41
41
  if memory_mb_str is not None:
42
42
  return int(memory_mb_str)
43
43
 
44
- try:
45
- value = _CGROUP_MEMORY_MAX_PATH.read_text().strip()
46
- if value == "max":
44
+ metadata_uri = os.environ.get("ECS_CONTAINER_METADATA_URI_V4")
45
+ if metadata_uri is not None:
46
+ try:
47
+ with urllib.request.urlopen(f"{metadata_uri}/task", timeout=2) as response:
48
+ data = json.loads(response.read())
49
+ return int(data["Limits"]["Memory"])
50
+ except Exception:
47
51
  return None
48
- return int(value) // (1024 * 1024)
49
- except (FileNotFoundError, ValueError):
50
- return None
52
+
53
+ return None
51
54
 
52
55
 
53
56
  def _set_memory_limit() -> None:
54
57
  """Set RLIMIT_AS from the container memory limit.
55
58
 
56
59
  On Lambda, reads AWS_LAMBDA_FUNCTION_MEMORY_SIZE. On Fargate/Batch,
57
- reads the cgroup v2 memory limit. This causes Python to raise
60
+ queries the ECS task metadata endpoint. This causes Python to raise
58
61
  MemoryError on excessive allocation instead of being killed by the
59
62
  OOM killer.
60
63
 
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.6.4"
10
+ version = "0.6.6"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -6,30 +6,18 @@ memory allocation raises MemoryError instead of triggering the Lambda OOM
6
6
  killer.
7
7
  """
8
8
 
9
+ import json
9
10
  import resource
10
11
 
11
12
  import lambda_tasks.handler as handler_module
12
13
 
13
14
 
14
- class _FakePath:
15
- def __init__(self, *, content: str | None) -> None:
16
- self._content = content
17
-
18
- def read_text(self) -> str:
19
- if self._content is None:
20
- raise FileNotFoundError
21
- return self._content
22
-
23
-
24
- def fake_path(content: str | None) -> _FakePath:
25
- return _FakePath(content=content)
26
-
27
-
28
15
  class TestMemoryLimitSetDuringColdStart:
29
16
  def test_rlimit_as_set_from_env_var(self, monkeypatch):
30
17
  """When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is set, RLIMIT_AS is configured with 128 MB reserved."""
31
18
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
32
19
  monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "512")
20
+ monkeypatch.delenv("ECS_CONTAINER_METADATA_URI_V4", raising=False)
33
21
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
34
22
  monkeypatch.setattr(
35
23
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
@@ -48,15 +36,15 @@ class TestMemoryLimitSetDuringColdStart:
48
36
  expected = (512 - 128) * 1024 * 1024
49
37
  assert calls == [(resource.RLIMIT_AS, (expected, expected))]
50
38
 
51
- def test_rlimit_as_not_set_when_env_var_missing_and_no_cgroup(self, monkeypatch):
52
- """When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is not set and cgroup is unavailable, RLIMIT_AS is unchanged."""
39
+ def test_rlimit_as_not_set_when_no_memory_source(self, monkeypatch):
40
+ """When neither Lambda env var nor ECS metadata is available, RLIMIT_AS is unchanged."""
53
41
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
54
42
  monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
43
+ monkeypatch.delenv("ECS_CONTAINER_METADATA_URI_V4", raising=False)
55
44
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
56
45
  monkeypatch.setattr(
57
46
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
58
47
  )
59
- monkeypatch.setattr(handler_module, "_CGROUP_MEMORY_MAX_PATH", fake_path(None))
60
48
 
61
49
  calls: list[tuple] = []
62
50
 
@@ -70,20 +58,33 @@ class TestMemoryLimitSetDuringColdStart:
70
58
 
71
59
  assert calls == []
72
60
 
73
- def test_rlimit_as_set_from_cgroup(self, monkeypatch):
74
- """When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is not set, falls back to cgroup v2."""
61
+ def test_rlimit_as_set_from_ecs_metadata(self, monkeypatch):
62
+ """When ECS_CONTAINER_METADATA_URI_V4 is set, queries the task endpoint for memory."""
75
63
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
76
64
  monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
65
+ monkeypatch.setenv("ECS_CONTAINER_METADATA_URI_V4", "http://169.254.170.2/v4")
77
66
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
78
67
  monkeypatch.setattr(
79
68
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
80
69
  )
81
- # 2 GB in bytes
82
- monkeypatch.setattr(
83
- handler_module,
84
- "_CGROUP_MEMORY_MAX_PATH",
85
- fake_path(str(2048 * 1024 * 1024)),
86
- )
70
+
71
+ task_metadata = json.dumps({"Limits": {"CPU": 2, "Memory": 8192}}).encode()
72
+
73
+ class FakeResponse:
74
+ def read(self):
75
+ return task_metadata
76
+
77
+ def __enter__(self):
78
+ return self
79
+
80
+ def __exit__(self, *args):
81
+ pass
82
+
83
+ def fake_urlopen(url, *, timeout=None):
84
+ assert url == "http://169.254.170.2/v4/task"
85
+ return FakeResponse()
86
+
87
+ monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
87
88
 
88
89
  calls: list[tuple] = []
89
90
 
@@ -95,18 +96,23 @@ class TestMemoryLimitSetDuringColdStart:
95
96
  handler_module._cold_start_done = False
96
97
  handler_module.handler(event={"Records": []}, context=None)
97
98
 
98
- expected = (2048 - 128) * 1024 * 1024
99
+ expected = (8192 - 128) * 1024 * 1024
99
100
  assert calls == [(resource.RLIMIT_AS, (expected, expected))]
100
101
 
101
- def test_rlimit_as_not_set_when_cgroup_is_max(self, monkeypatch):
102
- """When cgroup memory.max is 'max' (unlimited), RLIMIT_AS is unchanged."""
102
+ def test_rlimit_as_not_set_when_ecs_metadata_fails(self, monkeypatch):
103
+ """When ECS metadata endpoint fails, RLIMIT_AS is unchanged."""
103
104
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
104
105
  monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
106
+ monkeypatch.setenv("ECS_CONTAINER_METADATA_URI_V4", "http://169.254.170.2/v4")
105
107
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
106
108
  monkeypatch.setattr(
107
109
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
108
110
  )
109
- monkeypatch.setattr(handler_module, "_CGROUP_MEMORY_MAX_PATH", fake_path("max"))
111
+
112
+ def fake_urlopen(url, *, timeout=None):
113
+ raise OSError("connection refused")
114
+
115
+ monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
110
116
 
111
117
  calls: list[tuple] = []
112
118
 
@@ -120,10 +126,34 @@ class TestMemoryLimitSetDuringColdStart:
120
126
 
121
127
  assert calls == []
122
128
 
129
+ def test_lambda_env_var_takes_precedence_over_ecs_metadata(self, monkeypatch):
130
+ """AWS_LAMBDA_FUNCTION_MEMORY_SIZE is preferred over ECS metadata."""
131
+ monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
132
+ monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "512")
133
+ monkeypatch.setenv("ECS_CONTAINER_METADATA_URI_V4", "http://169.254.170.2/v4")
134
+ monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
135
+ monkeypatch.setattr(
136
+ "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
137
+ )
138
+
139
+ calls: list[tuple] = []
140
+
141
+ def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
142
+ calls.append((which, limits))
143
+
144
+ monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
145
+
146
+ handler_module._cold_start_done = False
147
+ handler_module.handler(event={"Records": []}, context=None)
148
+
149
+ expected = (512 - 128) * 1024 * 1024
150
+ assert calls == [(resource.RLIMIT_AS, (expected, expected))]
151
+
123
152
  def test_memory_limit_set_before_loaders(self, monkeypatch):
124
153
  """The memory limit is set before resolve_environment runs."""
125
154
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
126
155
  monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "256")
156
+ monkeypatch.delenv("ECS_CONTAINER_METADATA_URI_V4", raising=False)
127
157
 
128
158
  call_order: list[str] = []
129
159
 
@@ -159,6 +189,7 @@ class TestMemoryLimitSetDuringColdStart:
159
189
 
160
190
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
161
191
  monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "1024")
192
+ monkeypatch.delenv("ECS_CONTAINER_METADATA_URI_V4", raising=False)
162
193
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
163
194
  monkeypatch.setattr(
164
195
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
@@ -176,6 +207,7 @@ class TestMemoryLimitSetDuringColdStart:
176
207
  """When AWS_LAMBDA_FUNCTION_MEMORY_SIZE <= reserved, RLIMIT_AS is set to the 64 MB minimum."""
177
208
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
178
209
  monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128")
210
+ monkeypatch.delenv("ECS_CONTAINER_METADATA_URI_V4", raising=False)
179
211
  monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
180
212
  monkeypatch.setattr(
181
213
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None