Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dingo/exec/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ def evaluate_single_data(self, dingo_id: str, eval_fields: dict, eval_type: str,
model_cls = Model.rule_name_map.get(e_c_i.name)
model = model_cls() # 实例化类为对象,避免多线程配置覆盖
Model.set_config_rule(model, e_c_i.config)
# Backward compatibility: most rules still use @classmethod eval,
# which reads class-level dynamic_config instead of instance-level.
eval_self = getattr(model.eval, "__self__", None)
if eval_self is model_cls:
Model.set_config_rule(model_cls, e_c_i.config)
Comment on lines +185 to +187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Modifying the class-level configuration (model_cls) for backward compatibility is not thread-safe when using ThreadPoolExecutor (which is active when LOCAL_DEPLOYMENT_MODE is "true").

Multiple threads evaluating different configurations for the same classmethod-based rule will concurrently overwrite the class-level dynamic_config, leading to race conditions and incorrect evaluation results. Since the goal of instantiating the model (model = model_cls()) was specifically to avoid multi-thread config overwrite, this backward compatibility fallback defeats that safety mechanism for any rules that still use @classmethod.

Recommendation:
Migrate all remaining rules to instance-level eval methods (removing @classmethod) as soon as possible to ensure full thread safety.

elif eval_type == 'llm':
model_cls = Model.llm_name_map.get(e_c_i.name)
model = model_cls()
Expand Down
7 changes: 6 additions & 1 deletion dingo/exec/spark.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,13 @@ def evaluate_item(self, eval_fields: dict, eval_type: str, map_data: dict, eval_

for e_c_i in eval_list:
if eval_type == 'rule':
model = Model.rule_name_map.get(e_c_i.name)
model_cls = Model.rule_name_map.get(e_c_i.name)
model = model_cls()
Model.set_config_rule(model, e_c_i.config)
# Backward compatibility for classmethod-based rules.
eval_self = getattr(model.eval, "__self__", None)
if eval_self is model_cls:
Model.set_config_rule(model_cls, e_c_i.config)
Comment on lines +243 to +245

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the local executor, modifying the class-level configuration (model_cls) for backward compatibility is not thread-safe if Spark executors are configured to run with multiple threads (e.g., when spark.executor.cores > 1 with a multi-threaded task execution model).

Classmethod-based rules should be migrated to instance methods to ensure complete thread safety across all execution environments.

elif eval_type == 'llm':
model = Model.llm_name_map.get(e_c_i.name)
Model.set_config_llm(model, e_c_i.config)
Expand Down
7 changes: 3 additions & 4 deletions dingo/model/rule/scibase/rule_quanliang.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,11 +750,10 @@ class RuleQuanliangFieldValidation(BaseRule):
_required_fields = []
dynamic_config = EvaluatorRuleArgs(key_list=list(FIELD_VALIDATORS.keys()))

@classmethod
def eval(cls, input_data: Data) -> EvalDetail:
res = EvalDetail(metric=cls.__name__)
def eval(self, input_data: Data) -> EvalDetail:
res = EvalDetail(metric=self.__class__.__name__)
normalized = normalize_record(input_data.to_dict())
selected_fields = cls.dynamic_config.key_list or []
selected_fields = self.dynamic_config.key_list or []
bad_fields: List[str] = []
reasons: List[str] = []
for field in selected_fields:
Expand Down
37 changes: 37 additions & 0 deletions test/scripts/exec/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@


class TestLocal:
def test_rule_runtime_config_key_list_applies_in_local_executor(self):
input_data = {
"evaluator": [],
}
input_args = InputArgs(**input_data)
executor = LocalExecutor(input_args)

eval_list = [
InputArgs(
evaluator=[
{
"fields": {},
"evals": [
{
"name": "RuleQuanliangFieldValidation",
"config": {
"key_list": ["doi", "references", "related_works", "citations"]
},
}
],
}
]
).evaluator[0].evals[0]
]

result = executor.evaluate_single_data(
dingo_id="1",
eval_fields={},
eval_type="rule",
map_data={},
eval_list=eval_list,
)

details = result.eval_details["default"][0]
assert details.metric == "RuleQuanliangFieldValidation"
assert details.label == ["doi", "references", "related_works", "citations"]

def test_write_single_data_limit_per_file(self, tmp_path):
input_data = {
"executor": {
Expand Down
6 changes: 4 additions & 2 deletions test/scripts/model/rule/test_rule_scibase.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ def test_rule_quanliang_cases_from_jsonl(self):
if not line:
continue
row = json.loads(line)
RuleQuanliangFieldValidation.dynamic_config.key_list = row["key_list"]
result = RuleQuanliangFieldValidation.eval(Data(**row["input"]))
model = RuleQuanliangFieldValidation()
model.dynamic_config = model.dynamic_config.model_copy(deep=True)
model.dynamic_config.key_list = row["key_list"]
result = model.eval(Data(**row["input"]))
Comment on lines +23 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since we are now modifying the instance-level dynamic_config of model instead of the class-level RuleQuanliangFieldValidation.dynamic_config, the class-level configuration is never mutated.

This makes the try...finally block (which saves and restores original_key_list at lines 15 and 37-38) redundant. You can simplify this test file by removing the try...finally block entirely.


assert result.metric == "RuleQuanliangFieldValidation"
assert result.status is row["expected_status"], row["case"]
Expand Down
Loading