Skip to content

Commit ef49090

Browse files
committed
feat: Add codejail darklaunch toggle.
This adds a toggle for running codejail in both remote and local configurations for testing purposes. edx/edx-arch-experiments#895
1 parent 3401d09 commit ef49090

File tree

3 files changed

+124
-3
lines changed

3 files changed

+124
-3
lines changed

xmodule/capa/safe_exec/remote_exec.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,35 @@
2929
"ENABLE_CODEJAIL_REST_SERVICE", default=False, module_name=__name__
3030
)
3131

32+
# .. toggle_name: ENABLE_CODEJAIL_DARKLAUNCH
33+
# .. toggle_implementation: SettingToggle
34+
# .. toggle_default: False
35+
# .. toggle_description: Turn on to send requests to both the codejail service and the installed codejail library for
36+
# testing and evaluation purposes. The results from the installed codejail library will be the ones used.
37+
# .. toggle_warning: This toggle will only behave as expected when ENABLE_CODEJAIL_REST_SERVICE is not enabled and when
38+
# CODE_JAIL_REST_SERVICE_REMOTE_EXEC, CODE_JAIL_REST_SERVICE_HOST, CODE_JAIL_REST_SERVICE_READ_TIMEOUT,
39+
# and CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT are configured.
40+
# .. toggle_use_cases: temporary
41+
# .. toggle_creation_date: 2025-04-03
42+
# .. toggle_target_removal_date: 2025-05-01
43+
ENABLE_CODEJAIL_DARKLAUNCH = SettingToggle(
44+
"ENABLE_CODEJAIL_DARKLAUNCH", default=False, module_name=__name__
45+
)
46+
3247

3348
def is_codejail_rest_service_enabled():
3449
return ENABLE_CODEJAIL_REST_SERVICE.is_enabled()
3550

3651

52+
def is_codejail_in_darklaunch():
53+
"""
54+
Returns whether codejail dark launch is enabled.
55+
56+
Codejail dark launch can only be enabled if ENABLE_CODEJAIL_REST_SERVICE is not enabled.
57+
"""
58+
return not is_codejail_rest_service_enabled() and ENABLE_CODEJAIL_DARKLAUNCH.is_enabled()
59+
60+
3761
def get_remote_exec(*args, **kwargs):
3862
"""Get remote exec function based on setting and executes it."""
3963
remote_exec_function_name = settings.CODE_JAIL_REST_SERVICE_REMOTE_EXEC

xmodule/capa/safe_exec/safe_exec.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""Capa's specialized use of codejail.safe_exec."""
2+
import copy
23
import hashlib
4+
import logging
35

46
from codejail.safe_exec import SafeExecException, json_safe
57
from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec
68
from codejail.safe_exec import safe_exec as codejail_safe_exec
7-
from edx_django_utils.monitoring import function_trace
9+
from edx_django_utils.monitoring import function_trace, record_exception
810

911
from . import lazymod
10-
from .remote_exec import is_codejail_rest_service_enabled, get_remote_exec
12+
from .remote_exec import is_codejail_rest_service_enabled, is_codejail_in_darklaunch, get_remote_exec
13+
14+
log = logging.getLogger(__name__)
15+
1116

1217
# Establish the Python environment for Capa.
1318
# Capa assumes float-friendly division always.
@@ -155,6 +160,7 @@ def safe_exec(
155160
emsg, exception = get_remote_exec(data)
156161

157162
else:
163+
158164
# Decide which code executor to use.
159165
if unsafely:
160166
exec_fn = codejail_not_safe_exec
@@ -178,6 +184,32 @@ def safe_exec(
178184
else:
179185
emsg = None
180186

187+
# Run the code in both the remote codejail service as well as the local codejail
188+
# when in darklaunch mode.
189+
if is_codejail_in_darklaunch():
190+
try:
191+
# Create a copy so the originals are not modified as part of this call.
192+
darklaunch_globals = copy.deepcopy(globals_dict)
193+
data = {
194+
"code": code_prolog + LAZY_IMPORTS + code,
195+
"globals_dict": darklaunch_globals,
196+
"python_path": python_path,
197+
"limit_overrides_context": limit_overrides_context,
198+
"slug": slug,
199+
"unsafely": unsafely,
200+
"extra_files": extra_files,
201+
}
202+
remote_emsg, _remote_exception = get_remote_exec(data)
203+
log.info(
204+
f"Remote execution in darklaunch mode produces: {darklaunch_globals} or exception: {remote_emsg}"
205+
)
206+
log.info(f"Local execution in darklaunch mode produces: {globals_dict} or exception: {emsg}")
207+
except Exception as e: # pragma: no cover # pylint: disable=broad-except
208+
# Swallows all exceptions and logs it in monitoring so that dark launch doesn't cause issues during
209+
# deploy.
210+
log.exception("Error occurred while trying to remote exec in dark launch mode.")
211+
record_exception()
212+
181213
# Put the result back in the cache. This is complicated by the fact that
182214
# the globals dict might not be entirely serializable.
183215
if cache:

xmodule/capa/safe_exec/tests/test_safe_exec.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os.path
77
import textwrap
88
import unittest
9+
from unittest.mock import patch
910

1011
import pytest
1112
import random2 as random
@@ -20,7 +21,7 @@
2021

2122
from openedx.core.djangolib.testing.utils import skip_unless_lms
2223
from xmodule.capa.safe_exec import safe_exec, update_hash
23-
from xmodule.capa.safe_exec.remote_exec import is_codejail_rest_service_enabled
24+
from xmodule.capa.safe_exec.remote_exec import is_codejail_in_darklaunch, is_codejail_rest_service_enabled
2425

2526

2627
class TestSafeExec(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
@@ -125,6 +126,70 @@ def test_can_do_something_forbidden_if_run_unsafely(self):
125126
assert "SystemExit" in str(cm)
126127

127128

129+
class TestCodeJailDarkLaunch(unittest.TestCase):
130+
"""
131+
Test that the behavior of the dark launched code behaves as expected.
132+
"""
133+
@patch('xmodule.capa.safe_exec.safe_exec.get_remote_exec')
134+
@patch('xmodule.capa.safe_exec.safe_exec.codejail_safe_exec')
135+
def test_default_code_execution(self, local_exec, remote_exec):
136+
137+
# Test default only runs local exec.
138+
g = {}
139+
safe_exec('a=1', g)
140+
assert local_exec.called
141+
assert not remote_exec.called
142+
143+
@override_settings(ENABLE_CODEJAIL_REST_SERVICE=True)
144+
@patch('xmodule.capa.safe_exec.safe_exec.get_remote_exec')
145+
@patch('xmodule.capa.safe_exec.safe_exec.codejail_safe_exec')
146+
def test_code_execution_only_codejail_service(self, local_exec, remote_exec):
147+
# Set return values to empty values to indicate no error.
148+
remote_exec.return_value = (None, None)
149+
# Test with only the service enabled.
150+
g = {}
151+
safe_exec('a=1', g)
152+
assert not local_exec.called
153+
assert remote_exec.called
154+
155+
@override_settings(ENABLE_CODEJAIL_DARKLAUNCH=True)
156+
@patch('xmodule.capa.safe_exec.safe_exec.get_remote_exec')
157+
@patch('xmodule.capa.safe_exec.safe_exec.codejail_safe_exec')
158+
def test_code_execution_darklaunch(self, local_exec, remote_exec):
159+
# Set return values to empty values to indicate no error.
160+
remote_exec.return_value = (None, None)
161+
g = {}
162+
163+
# Verify that incorrect config runs only remote and not both.
164+
with override_settings(ENABLE_CODEJAIL_REST_SERVICE=True):
165+
safe_exec('a=1', g)
166+
assert not local_exec.called
167+
assert remote_exec.called
168+
169+
local_exec.reset_mock()
170+
remote_exec.reset_mock()
171+
172+
# Set up side effects to mimic the real behavior of modifying the globals_dict.
173+
def local_side_effect(*args, **kwargs):
174+
test_globals = args[1]
175+
test_globals['test'] = 'local_test'
176+
177+
def remote_side_effect(*args, **kwargs):
178+
test_globals = args[0]['globals_dict']
179+
test_globals['test'] = 'remote_test'
180+
181+
local_exec.side_effect = local_side_effect
182+
remote_exec.side_effect = remote_side_effect
183+
184+
assert is_codejail_in_darklaunch()
185+
safe_exec('a=1', g)
186+
187+
assert local_exec.called
188+
assert remote_exec.called
189+
# Verify that the local/default behavior currently wins out.
190+
assert g['test'] == 'local_test'
191+
192+
128193
class TestLimitConfiguration(unittest.TestCase):
129194
"""
130195
Test that resource limits can be configured and overriden via Django settings.

0 commit comments

Comments
 (0)