-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enforce Editor Lock for Write, Append, and Upload Operations in DataNode #2122
Changes from 6 commits
7fc9c4c
80a3627
fa18f0c
5677a46
0e9825a
87152de
08d70c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,6 +20,7 @@ | |||||
from taipy.common.logger._taipy_logger import _TaipyLogger | ||||||
|
||||||
from .._entity._reload import _self_reload | ||||||
from ..exceptions import DataNodeIsBeingEdited | ||||||
from ..reason import InvalidUploadFile, NoFileToDownload, NotAFile, ReasonCollection, UploadFileCanNotBeRead | ||||||
from .data_node import DataNode | ||||||
from .data_node_id import Edit | ||||||
|
@@ -100,7 +101,15 @@ | |||||
|
||||||
return "" | ||||||
|
||||||
def _upload(self, path: str, upload_checker: Optional[Callable[[str, Any], bool]] = None) -> ReasonCollection: | ||||||
def _upload( | ||||||
self, | ||||||
data: Optional[Any] = None, | ||||||
path: str = "", | ||||||
upload_checker: Optional[Callable[[str, Any], bool]] = None, | ||||||
job_id: Optional["JobId"] = None, | ||||||
editor_id: Optional[str] = None, | ||||||
**kwargs: Dict[str, Any] | ||||||
) -> None: | ||||||
"""Upload a file data to the data node. | ||||||
|
||||||
Arguments: | ||||||
|
@@ -112,6 +121,9 @@ | |||||
Returns: | ||||||
True if the upload was successful, otherwise False. | ||||||
""" | ||||||
if self.edit_in_progress and self.editor_id != editor_id: | ||||||
raise DataNodeIsBeingEdited(self.id, self.editor_id) | ||||||
|
||||||
Comment on lines
+124
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of raising an exception, you should build a specific I would also move this code after the creation of the reason_collection variable: |
||||||
from ._data_manager_factory import _DataManagerFactory | ||||||
|
||||||
reason_collection = ReasonCollection() | ||||||
|
@@ -124,7 +136,7 @@ | |||||
self.__logger.error(f"Error while uploading {upload_path.name} to data node {self.id}:") # type: ignore[attr-defined] | ||||||
self.__logger.error(f"Error: {err}") | ||||||
reason_collection._add_reason(self.id, UploadFileCanNotBeRead(upload_path.name, self.id)) # type: ignore[attr-defined] | ||||||
return reason_collection | ||||||
|
||||||
if upload_checker is not None: | ||||||
try: | ||||||
|
@@ -138,15 +150,15 @@ | |||||
|
||||||
if not can_upload: | ||||||
reason_collection._add_reason(self.id, InvalidUploadFile(upload_path.name, self.id)) # type: ignore[attr-defined] | ||||||
return reason_collection | ||||||
|
||||||
shutil.copy(upload_path, self.path) | ||||||
|
||||||
self.track_edit(timestamp=datetime.now()) # type: ignore[attr-defined] | ||||||
self._write(data) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The _upload method should not write data directly like that. It is designed to replace the data of a File data node with a new file without passing through the write mechanism. Please revert this change. |
||||||
self.track_edit(job_id=job_id, editor_id=editor_id, **kwargs) # type: ignore[attr-defined] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. THere is no possible job_id. This cannot be called by the orchestrator. |
||||||
self.unlock_edit() # type: ignore[attr-defined] | ||||||
_DataManagerFactory._build_manager()._set(self) # type: ignore[arg-type] | ||||||
|
||||||
return reason_collection | ||||||
|
||||||
def _read_from_path(self, path: Optional[str] = None, **read_kwargs) -> Any: | ||||||
raise NotImplementedError | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -415,7 +415,7 @@ def read(self) -> Any: | |||||||
) | ||||||||
return None | ||||||||
|
||||||||
def append(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]): | ||||||||
def append(self, data, job_id: Optional[JobId] = None, editor_id: Optional[str] = None, **kwargs: Dict[str, Any]): | ||||||||
"""Append some data to this data node. | ||||||||
|
||||||||
Arguments: | ||||||||
|
@@ -426,12 +426,14 @@ def append(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]) | |||||||
""" | ||||||||
from ._data_manager_factory import _DataManagerFactory | ||||||||
|
||||||||
if self.edit_in_progress and self.editor_id != editor_id: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate on why you did not use the same if condition as the |
||||||||
raise DataNodeIsBeingEdited(self.id, self.editor_id) | ||||||||
|
||||||||
self._append(data) | ||||||||
self.track_edit(job_id=job_id, **kwargs) | ||||||||
self.unlock_edit() | ||||||||
jrobinAV marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
self.track_edit(job_id=job_id, editor_id=editor_id, **kwargs) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
_DataManagerFactory._build_manager()._set(self) | ||||||||
|
||||||||
def write(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]): | ||||||||
def write(self, data, job_id: Optional[JobId] = None, editor_id: Optional[str] = None, **kwargs: Dict[str, Any]): | ||||||||
"""Write some data to this data node. | ||||||||
|
||||||||
Arguments: | ||||||||
|
@@ -442,9 +444,16 @@ def write(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]): | |||||||
""" | ||||||||
from ._data_manager_factory import _DataManagerFactory | ||||||||
|
||||||||
if self.edit_in_progress and editor_id and self.editor_id != editor_id: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand correctly, this covers the case where the data node is already locked by a different editor than the one attempting to write it. We also need to check that the |
||||||||
raise DataNodeIsBeingEdited(self.id, self.editor_id) | ||||||||
|
||||||||
if not editor_id and self.edit_in_progress: | ||||||||
print("Orchestrator writing without editor_id") | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This covers the case where the Orchestrator attempts to write a data node locked by an editor. We can eventually add an info log using the |
||||||||
|
||||||||
self._write(data) | ||||||||
self.track_edit(job_id=job_id, **kwargs) | ||||||||
self.unlock_edit() | ||||||||
Comment on lines
-452
to
-453
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PLease revert these two lines. |
||||||||
self.last_edit_date = datetime.now() | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||
self.edit_in_progress = False # Ensure it's not locked after writing | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The unlock edit that you removed should be reverted. It already sets the |
||||||||
self.track_edit(job_id=job_id, editor_id=editor_id, **kwargs) | ||||||||
_DataManagerFactory._build_manager()._set(self) | ||||||||
|
||||||||
def track_edit(self, **options): | ||||||||
|
@@ -471,6 +480,10 @@ def lock_edit(self, editor_id: Optional[str] = None): | |||||||
Arguments: | ||||||||
editor_id (Optional[str]): The editor's identifier. | ||||||||
""" | ||||||||
|
||||||||
if self._editor_expiration_date and datetime.now() > self._editor_expiration_date: | ||||||||
self.unlock_edit() | ||||||||
|
||||||||
Comment on lines
+505
to
+508
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see what case we want to cover here. For me, the next condition already covers this case. Can you elaborate? Otherwise, we should revert it. |
||||||||
if editor_id: | ||||||||
if ( | ||||||||
self.edit_in_progress | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,6 @@ | |
from typing import Any, Dict, List, Optional, Set | ||
|
||
from taipy.common.config.common.scope import Scope | ||
|
||
from .._version._version_manager_factory import _VersionManagerFactory | ||
from .data_node import DataNode | ||
from .data_node_id import DataNodeId, Edit | ||
|
@@ -98,3 +97,14 @@ def _read(self): | |
|
||
def _write(self, data): | ||
in_memory_storage[self.id] = data | ||
|
||
def _append(self, data): | ||
"""Append data to the existing data in the in-memory storage.""" | ||
if self.id not in in_memory_storage: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. self.id must not be None. you can remove it from the condition. |
||
in_memory_storage[self.id] = [] | ||
|
||
if not isinstance(in_memory_storage[self.id], list): | ||
in_memory_storage[self.id] = [in_memory_storage[self.id]] | ||
Comment on lines
+106
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
|
||
in_memory_storage[self.id].append(data) # Append new data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the previous data of the data node has no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I added a if instance condition to resolve that. Please review it and let me know if any changes required. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -745,3 +745,33 @@ def test_change_data_node_name(self): | |
# This new syntax will be the only one allowed: https://github.com/Avaiga/taipy-core/issues/806 | ||
dn.properties["name"] = "baz" | ||
assert dn.name == "baz" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we are missing a few test cases here:
|
||
def test_locked_data_node_write_should_fail_with_wrong_editor(self): | ||
dn = InMemoryDataNode("dn", Scope.SCENARIO) | ||
dn.lock_edit("editor_1") | ||
|
||
# Should raise exception for wrong editor | ||
with pytest.raises(DataNodeIsBeingEdited): | ||
dn.write("data", editor_id="editor_2") | ||
|
||
# Should succeed with correct editor | ||
dn.write("data", editor_id="editor_1") | ||
assert dn.read() == "data" | ||
|
||
def test_locked_data_node_append_should_fail_with_wrong_editor(self): | ||
dn = InMemoryDataNode("dn", Scope.SCENARIO) | ||
dn.lock_edit("editor_1") | ||
|
||
with pytest.raises(DataNodeIsBeingEdited): | ||
dn.append("data", editor_id="editor_2") | ||
|
||
dn.append("data", editor_id="editor_1") | ||
assert dn.read() == ["data"] | ||
|
||
def test_orchestrator_write_without_editor_id(self): | ||
dn = InMemoryDataNode("dn", Scope.SCENARIO) | ||
dn.lock_edit("editor_1") | ||
|
||
# Orchestrator write without editor_id should succeed | ||
dn.write("orchestrator_data") | ||
assert dn.read() == "orchestrator_data" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get this change.
You only need one new argument: an optional
editor_id
. In particular, we don't want data or job_id. We must also return a reason collection!Why not add a kwargs parameter to pass it to the track edit method, indeed. But this is out of the scope of the issue.