django-lambda-tasks 0.6.3__tar.gz → 0.6.5__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.3 → django_lambda_tasks-0.6.5}/PKG-INFO +1 -1
  2. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/handler.py +11 -3
  3. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/local_executor.py +18 -6
  4. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/pyproject.toml +1 -1
  5. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_local_executor.py +19 -0
  6. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_memory_limit.py +52 -4
  7. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.github/workflows/ci.yml +0 -0
  8. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.github/workflows/release.yml +0 -0
  9. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.gitignore +0 -0
  10. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  11. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/async-local-execution/design.md +0 -0
  12. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/async-local-execution/requirements.md +0 -0
  13. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/async-local-execution/tasks.md +0 -0
  14. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  15. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  16. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  17. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  18. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  19. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  20. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  21. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  22. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  23. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  24. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  25. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  26. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  27. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  28. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  29. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  30. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/retry-delay/.config.kiro +0 -0
  31. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/retry-delay/design.md +0 -0
  32. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/retry-delay/requirements.md +0 -0
  33. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/retry-delay/tasks.md +0 -0
  34. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  35. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks/design.md +0 -0
  36. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  37. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  38. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  39. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  40. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  41. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  42. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/singleton-task/.config.kiro +0 -0
  43. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/singleton-task/design.md +0 -0
  44. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/singleton-task/requirements.md +0 -0
  45. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/singleton-task/tasks.md +0 -0
  46. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  47. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  48. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  49. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  50. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/task-retry/.config.kiro +0 -0
  51. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/task-retry/design.md +0 -0
  52. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/task-retry/requirements.md +0 -0
  53. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/specs/task-retry/tasks.md +0 -0
  54. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/steering/product.md +0 -0
  55. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/steering/structure.md +0 -0
  56. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.kiro/steering/tech.md +0 -0
  57. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.pre-commit-config.yaml +0 -0
  58. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/.vscode/settings.json +0 -0
  59. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/README.md +0 -0
  60. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/README.md +0 -0
  61. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_app/__init__.py +0 -0
  62. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_app/apps.py +0 -0
  63. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_app/tasks.py +0 -0
  64. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_app/urls.py +0 -0
  65. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_app/views.py +0 -0
  66. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_project/__init__.py +0 -0
  67. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_project/settings.py +0 -0
  68. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_project/urls.py +0 -0
  69. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/example_project/wsgi.py +0 -0
  70. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/example/manage.py +0 -0
  71. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/__init__.py +0 -0
  72. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/admin.py +0 -0
  73. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/apps.py +0 -0
  74. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/decorators.py +0 -0
  75. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/environment_loader.py +0 -0
  76. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/logging.py +0 -0
  77. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/migrations/0001_initial.py +0 -0
  78. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  79. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/migrations/__init__.py +0 -0
  80. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/models.py +0 -0
  81. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/secret_loader.py +0 -0
  82. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/settings.py +0 -0
  83. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/tasks.py +0 -0
  84. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/lambda_tasks/timeouts.py +0 -0
  85. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/conftest.py +0 -0
  86. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/settings.py +0 -0
  87. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_admin.py +0 -0
  88. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_batch.py +0 -0
  89. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_batch_worker.py +0 -0
  90. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_decorator.py +0 -0
  91. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_decorators.py +0 -0
  92. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_deferred_enqueue.py +0 -0
  93. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_environment_loader.py +0 -0
  94. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_handler.py +0 -0
  95. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_kwargs_only.py +0 -0
  96. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_logging.py +0 -0
  97. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_models.py +0 -0
  98. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_secret_loader.py +0 -0
  99. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_serializer.py +0 -0
  100. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_settings.py +0 -0
  101. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_tasks.py +0 -0
  102. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_timeout_validation.py +0 -0
  103. {django_lambda_tasks-0.6.3 → django_lambda_tasks-0.6.5}/tests/test_timeouts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -32,19 +32,27 @@ _cold_start_done: bool = False
32
32
 
33
33
  _MEMORY_RESERVED_MB = 128
34
34
  _MEMORY_MINIMUM_LIMIT_MB = 64
35
- _CGROUP_MEMORY_MAX_PATH = Path("/sys/fs/cgroup/memory.max")
35
+ _CGROUP_V2_MEMORY_MAX_PATH = Path("/sys/fs/cgroup/memory.max")
36
+ _CGROUP_V1_MEMORY_LIMIT_PATH = Path("/sys/fs/cgroup/memory/memory.limit_in_bytes")
36
37
 
37
38
 
38
39
  def _get_memory_mb() -> int | None:
39
- """Get container memory limit from Lambda env var or cgroup v2."""
40
+ """Get container memory limit from Lambda env var or cgroup."""
40
41
  memory_mb_str = os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
41
42
  if memory_mb_str is not None:
42
43
  return int(memory_mb_str)
43
44
 
44
45
  try:
45
- value = _CGROUP_MEMORY_MAX_PATH.read_text().strip()
46
+ value = _CGROUP_V2_MEMORY_MAX_PATH.read_text().strip()
46
47
  if value == "max":
47
48
  return None
49
+ else:
50
+ return int(value) // (1024 * 1024)
51
+ except (FileNotFoundError, ValueError):
52
+ pass
53
+
54
+ try:
55
+ value = _CGROUP_V1_MEMORY_LIMIT_PATH.read_text().strip()
48
56
  return int(value) // (1024 * 1024)
49
57
  except (FileNotFoundError, ValueError):
50
58
  return None
@@ -2,6 +2,7 @@
2
2
 
3
3
  import atexit
4
4
  import logging
5
+ import multiprocessing
5
6
  import signal
6
7
  import threading
7
8
  import uuid
@@ -23,9 +24,10 @@ def _pool_initializer() -> None:
23
24
  atexit as a fallback). This prevents workers from being killed
24
25
  mid-operation and leaking semaphores.
25
26
 
26
- Closes all inherited database connections before setting up Django.
27
- On Linux (fork-based spawning), child processes inherit copies of the
28
- parent's DB connections which are in an inconsistent state.
27
+ Closes all database connections before setting up Django. With the
28
+ forkserver context, workers don't inherit the parent's connections,
29
+ but AppConfig.ready() hooks may open connections during django.setup()
30
+ that should be closed before task execution begins.
29
31
 
30
32
  Then sets up Django for task execution.
31
33
  """
@@ -34,10 +36,10 @@ def _pool_initializer() -> None:
34
36
  import django
35
37
  from django.db import connections
36
38
 
37
- connections.close_all()
38
-
39
39
  django.setup()
40
40
 
41
+ connections.close_all()
42
+
41
43
 
42
44
  def _shutdown_pool() -> None:
43
45
  """Shut down the pool, releasing its POSIX semaphores promptly.
@@ -118,12 +120,22 @@ def _install_shutdown_handlers() -> None:
118
120
 
119
121
 
120
122
  def get_pool() -> ProcessPoolExecutor:
121
- """Return the shared ProcessPoolExecutor, creating it on first call."""
123
+ """Return the shared ProcessPoolExecutor, creating it on first call.
124
+
125
+ Uses the "forkserver" multiprocessing context so that worker processes
126
+ are forked from a clean server process rather than from the Django parent.
127
+ This prevents workers from inheriting the parent's open database
128
+ connections (and other file descriptors), which would otherwise cause
129
+ "server closed the connection unexpectedly" errors in containerized
130
+ environments where close() in the child tears down the shared socket.
131
+ """
122
132
  global _pool
123
133
  if _pool is None:
124
134
  conf = LambdaTasksSettings()
135
+ mp_context = multiprocessing.get_context("forkserver")
125
136
  _pool = ProcessPoolExecutor(
126
137
  max_workers=conf.LOCAL_WORKERS,
138
+ mp_context=mp_context,
127
139
  initializer=_pool_initializer,
128
140
  )
129
141
  atexit.register(_shutdown_pool)
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.6.3"
10
+ version = "0.6.5"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -189,6 +189,25 @@ class TestGetPool:
189
189
 
190
190
  assert lambda_tasks.local_executor._pool is pool
191
191
 
192
+ @pytest.mark.usefixtures("_reset_pool")
193
+ def test_get_pool_uses_forkserver_context(self, settings):
194
+ """get_pool() uses the forkserver multiprocessing context.
195
+
196
+ This prevents workers from inheriting the parent's open database
197
+ connections, which would cause connection errors in containerized
198
+ environments.
199
+ """
200
+ import lambda_tasks.local_executor
201
+
202
+ settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
203
+ settings.LAMBDA_TASKS_EAGER = False
204
+
205
+ pool = lambda_tasks.local_executor.get_pool()
206
+
207
+ # ProcessPoolExecutor stores the context; forkserver processes
208
+ # use the ForkServerProcess type
209
+ assert pool._mp_context.get_start_method() == "forkserver"
210
+
192
211
 
193
212
  # ---------------------------------------------------------------------------
194
213
  # Property-based tests: get_pool() (Requirement 2)
@@ -56,7 +56,12 @@ class TestMemoryLimitSetDuringColdStart:
56
56
  monkeypatch.setattr(
57
57
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
58
58
  )
59
- monkeypatch.setattr(handler_module, "_CGROUP_MEMORY_MAX_PATH", fake_path(None))
59
+ monkeypatch.setattr(
60
+ handler_module, "_CGROUP_V2_MEMORY_MAX_PATH", fake_path(None)
61
+ )
62
+ monkeypatch.setattr(
63
+ handler_module, "_CGROUP_V1_MEMORY_LIMIT_PATH", fake_path(None)
64
+ )
60
65
 
61
66
  calls: list[tuple] = []
62
67
 
@@ -70,7 +75,7 @@ class TestMemoryLimitSetDuringColdStart:
70
75
 
71
76
  assert calls == []
72
77
 
73
- def test_rlimit_as_set_from_cgroup(self, monkeypatch):
78
+ def test_rlimit_as_set_from_cgroup_v2(self, monkeypatch):
74
79
  """When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is not set, falls back to cgroup v2."""
75
80
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
76
81
  monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
@@ -81,9 +86,14 @@ class TestMemoryLimitSetDuringColdStart:
81
86
  # 2 GB in bytes
82
87
  monkeypatch.setattr(
83
88
  handler_module,
84
- "_CGROUP_MEMORY_MAX_PATH",
89
+ "_CGROUP_V2_MEMORY_MAX_PATH",
85
90
  fake_path(str(2048 * 1024 * 1024)),
86
91
  )
92
+ monkeypatch.setattr(
93
+ handler_module,
94
+ "_CGROUP_V1_MEMORY_LIMIT_PATH",
95
+ fake_path(None),
96
+ )
87
97
 
88
98
  calls: list[tuple] = []
89
99
 
@@ -106,7 +116,12 @@ class TestMemoryLimitSetDuringColdStart:
106
116
  monkeypatch.setattr(
107
117
  "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
108
118
  )
109
- monkeypatch.setattr(handler_module, "_CGROUP_MEMORY_MAX_PATH", fake_path("max"))
119
+ monkeypatch.setattr(
120
+ handler_module, "_CGROUP_V2_MEMORY_MAX_PATH", fake_path("max")
121
+ )
122
+ monkeypatch.setattr(
123
+ handler_module, "_CGROUP_V1_MEMORY_LIMIT_PATH", fake_path(None)
124
+ )
110
125
 
111
126
  calls: list[tuple] = []
112
127
 
@@ -120,6 +135,39 @@ class TestMemoryLimitSetDuringColdStart:
120
135
 
121
136
  assert calls == []
122
137
 
138
+ def test_rlimit_as_set_from_cgroup_v1(self, monkeypatch):
139
+ """When cgroup v2 is unavailable, falls back to cgroup v1 (Fargate)."""
140
+ monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
141
+ monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
142
+ monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
143
+ monkeypatch.setattr(
144
+ "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
145
+ )
146
+ monkeypatch.setattr(
147
+ handler_module,
148
+ "_CGROUP_V2_MEMORY_MAX_PATH",
149
+ fake_path(None),
150
+ )
151
+ # 4 GB in bytes
152
+ monkeypatch.setattr(
153
+ handler_module,
154
+ "_CGROUP_V1_MEMORY_LIMIT_PATH",
155
+ fake_path(str(4096 * 1024 * 1024)),
156
+ )
157
+
158
+ calls: list[tuple] = []
159
+
160
+ def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
161
+ calls.append((which, limits))
162
+
163
+ monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
164
+
165
+ handler_module._cold_start_done = False
166
+ handler_module.handler(event={"Records": []}, context=None)
167
+
168
+ expected = (4096 - 128) * 1024 * 1024
169
+ assert calls == [(resource.RLIMIT_AS, (expected, expected))]
170
+
123
171
  def test_memory_limit_set_before_loaders(self, monkeypatch):
124
172
  """The memory limit is set before resolve_environment runs."""
125
173
  monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)