1717from sqlalchemy .orm import Session
1818from traitlets .config import Config
1919from traitlets .config .configurable import LoggingConfigurable
20- from traitlets .traitlets import Callable , TraitError , Type , Unicode , validate
20+ from traitlets .traitlets import Int , TraitError , Type , Unicode , validate
2121
2222from grader_service .autograding .git_manager import GitSubmissionManager
2323from grader_service .autograding .utils import collect_logs , executable_validator , rmtree
2424from grader_service .convert .converters .autograde import Autograde
2525from grader_service .convert .gradebook .models import GradeBookModel
2626from grader_service .orm .assignment import Assignment
27- from grader_service .orm .lecture import Lecture
2827from grader_service .orm .submission import AutoStatus , ManualStatus , Submission
2928from grader_service .orm .submission_logs import SubmissionLogs
3029from grader_service .orm .submission_properties import SubmissionProperties
3130
3231
33- def default_timeout_func (lecture : Lecture ) -> int :
34- return 360
35-
36-
3732class LocalAutogradeExecutor (LoggingConfigurable ):
3833 """
3934 Runs an autograde job on the local machine
@@ -46,10 +41,19 @@ class LocalAutogradeExecutor(LoggingConfigurable):
4641 relative_output_path = Unicode ("convert_out" , allow_none = True ).tag (config = True )
4742 git_manager_class = Type (GitSubmissionManager , allow_none = False ).tag (config = True )
4843
49- timeout_func = Callable (
50- default_timeout_func ,
44+ cell_timeout = Int (
5145 allow_none = False ,
52- help = "Function that takes a lecture as an argument and returns the cell timeout in seconds." ,
46+ help = "Returns the cell timeout in seconds, either user-defined, from configuration or default values." ,
47+ ).tag (config = True )
48+
49+ default_cell_timeout = Int (300 , help = "Default cell timeout in seconds, defaults to 300" ).tag (
50+ config = True
51+ )
52+
53+ min_cell_timeout = Int (10 , help = "Min cell timeout in seconds, defaults to 10." ).tag (config = True )
54+
55+ max_cell_timeout = Int (
56+ 86400 , help = "Max cell timeout in seconds, defaults to 86400 (24 hours)"
5357 ).tag (config = True )
5458
5559 def __init__ (
@@ -86,6 +90,8 @@ def __init__(
8690 # Git manager performs the git operations when creating a new repo for the grading results
8791 self .git_manager = self .git_manager_class (grader_service_dir , self .submission )
8892
93+ self .cell_timeout = self ._determine_cell_timeout ()
94+
8995 def start (self ):
9096 """
9197 Starts the autograding job.
@@ -219,7 +225,7 @@ def _put_grades_in_assignment_properties(self) -> str:
219225 def _get_autograde_config (self ) -> Config :
220226 """Returns the autograde config, with the timeout set for ExecutePreprocessor."""
221227 c = Config ()
222- c .ExecutePreprocessor .timeout = self .timeout_func ( self . assignment . lecture )
228+ c .ExecutePreprocessor .timeout = self .cell_timeout
223229 return c
224230
225231 def _get_whitelist_patterns (self ) -> set [str ]:
@@ -320,6 +326,41 @@ def _cleanup(self) -> None:
320326 if self .close_session :
321327 self .session .close ()
322328
329+ def _determine_cell_timeout (self ):
330+ cell_timeout = self .default_cell_timeout
331+ # check if the cell timeout was set by user
332+ if self .assignment .settings .cell_timeout is not None :
333+ custom_cell_timeout = self .assignment .settings .cell_timeout
334+ self .log .info (
335+ f"Found custom cell timeout in assignment settings: { custom_cell_timeout } seconds."
336+ )
337+ cell_timeout = min (
338+ self .max_cell_timeout , max (custom_cell_timeout , self .min_cell_timeout )
339+ )
340+ self .log .info (f"Setting custom cell timeout to { cell_timeout } ." )
341+
342+ return cell_timeout
343+
344+ @validate ("min_cell_timeout" , "default_cell_timeout" , "max_cell_timeout" )
345+ def _validate_cell_timeouts (self , proposal ):
346+ trait_name = proposal ["trait" ].name
347+ value = proposal ["value" ]
348+
349+ # Get current or proposed values
350+ min_t = value if trait_name == "min_cell_timeout" else self .min_cell_timeout
351+ default_t = value if trait_name == "default_cell_timeout" else self .default_cell_timeout
352+ max_t = value if trait_name == "max_cell_timeout" else self .max_cell_timeout
353+
354+ # Validate the constraint
355+ if not (0 < min_t < default_t < max_t ):
356+ raise TraitError (
357+ f"Invalid { trait_name } value ({ value } ). "
358+ f"Timeout values must satisfy: 0 < min_cell_timeout < default_cell_timeout < max_cell_timeout. "
359+ f"Got min={ min_t } , default={ default_t } , max={ max_t } ."
360+ )
361+
362+ return value
363+
323364 @validate ("relative_input_path" , "relative_output_path" )
324365 def _validate_service_dir (self , proposal ):
325366 path : str = proposal ["value" ]
@@ -358,7 +399,7 @@ def _run(self):
358399 self .output_path ,
359400 "-p" ,
360401 "*.ipynb" ,
361- f"--ExecutePreprocessor.timeout={ self .timeout_func ( self . assignment . lecture ) } " ,
402+ f"--ExecutePreprocessor.timeout={ self .cell_timeout } " ,
362403 ]
363404 self .log .info (f"Running { command } " )
364405 process = subprocess .run (
0 commit comments