-
Notifications
You must be signed in to change notification settings - Fork 8
ARXIVNG-2210 ARXIVNG-2211 filemanager refactor #43
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
Conversation
…estore checkpoints.
…ing. [ARXIVNG-2032] David
… checkpoint tests.
| return self.path | ||
|
|
||
| @property | ||
| def name_sans_ext(self) -> str: |
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.
Is the same as (os.path.) basename? To be consistent should delete_file() become directory_sans_file()? The method name definitely caught my attention. I'm sure this is just fine.
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.
Slightly different from basename:
>>> os.path.basename('foo/bar.txt')
'bar.txt'
>>> os.path.splitext('foo/bar.txt')[0]
'foo/bar'To be consistent should delete_file() become directory_sans_file()?
Not sure what you mean -- this is just generating a string based on the path. delete_file() has side-effects (namely, deleting a file)
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 was really just poking fun at the method name. I apologize.
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.
haha no worries :-)
|
|
||
| - :class:`.ReadinessWorkspace`, which adds semantics around the "readiness" | ||
| of the workspace for use in a submission. | ||
| - :class:`.SingleFileWorkspace`, which adds the concept of a "single file |
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.
We've discussed eventually allowing ancillary files with 'single-file' submissions. Does this implementation limit in any way the ability to provide a single-file submission with ancillary data files?
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.
Does this implementation limit in any way the ability to provide a single-file submission with ancillary data files?
No, because .file_count excludes ancillary files, and .is_single_file_submission is just looking at .file_count.
| raise RuntimeError('Storage adapter is not set') | ||
| self.storage.makedirs(self, self.source_path) | ||
| self.storage.makedirs(self, self.ancillary_path) | ||
| self.storage.makedirs(self, self.removed_path) |
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.
Do we need to create system directory to be consistent?
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.
Well, source_path is based on base_path, which is where the system files live. So it should exist by extension of these being created. I guess if you wanted to store system files somewhere else; hadn't considered that scenario. Do you think we should support this?
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.
Ahh. I saw new system directory and thought you were now sticking the system/checkpoint files there. My bad. I need to take another look at the code.
| self.files.set(u_file.path, u_file) | ||
|
|
||
| @modifies_workspace() | ||
| def delete_workspace(self) -> bool: |
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.
This class contains methods that 'alter files' according to class docstring. The scope of this method seems a bit beyond just altering files. Maybe just inaccurate description or maybe it's in the wrong place.
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.
Agree it was misleading; fixed in 3d5ca4b
| return None | ||
|
|
||
|
|
||
| # QUESTION: is this still relevant, given that FM service is decoupled from |
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.
At the current time if you provide the compilation service with a submission containing the missing font file AutoTeX basically aborts. So you could eliminate this check and let the compilation service generate an error. One could argue that many of the checks in the FM service are related to compilation and belong in the compilation service.
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class CheckForMissingReferences(BaseChecker): |
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.
This no longer represents current logic in the Legacy system and needs to be updated.
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| # TODO: implement this. |
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.
Looks like you forgot to implement this functionality.
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.
These weren't implemented before the refactor, IIRC
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.
Right. That was a little wishful thinking on my part.
| @@ -0,0 +1,56 @@ | |||
| """Check for TeX-generated files.""" | |||
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'm wondering if this needs to be reworded along the lines of 'eliminate files generated from TeX compilation' to distinguish it more from TeX Produced. TeXGenerated makes me think of TeXProduced.
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.
fixed in 7545f3d
| workspace.rename(child, new_path) | ||
| workspace.remove(entries[0], "Removed top level directory") | ||
| # | ||
| # # Set permissions |
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.
Permissions have always been an issue. I expect you are dealing with these elsewhere.
| workspace.create(dest, touch=False, is_ancillary=is_ancillary) | ||
|
|
||
|
|
||
| # TODO: Add support for compressed files. |
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.
Will we accept Unix compressed files?
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.
Dunno. This TODO was there before the refactor IIRC
| # | ||
| # | ||
| # # Upload a submission package and perform normal operations on upload | ||
| # def test_upload_files_normal(self) -> None: |
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.
These normal/typical upload tests were definitely working in the pre-refactored FM service. This test basically scripted a series of typical requests that might be expected during a normal submission process. The idea was to combine different types of requests together in order to catch any inconsistencies in the final workspace state.
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.
Is this it?
| # Upload a submission package and perform normal operations on upload |
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 think these just migrated elsewhere. I seem to recall running into them elsewhere.
| logger.setLevel(int(os.environ.get('LOGLEVEL', '20'))) | ||
|
|
||
|
|
||
| class TestMissingReferences(TestCase): |
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.
These tests need revision as the bib/bbl logic has changed. (Ignore my comments on bib/bbl handling if you've already updated the internal logic per recent changes)
| @@ -0,0 +1,344 @@ | |||
| """Tests the application via its API, using the QuarantineStorageAdapter.""" | |||
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.
This test appears to specifically test the new quarantine storage adaptor. Would it make sense to run all existing tests against each of the storage adaptor instances? Could you set the storage adaptor, run all of the existing tests, set a different storage adaptor, and run all of the tests again? My sense is if we are changing the underlying storage adaptors we should run an exhaustive series of tests that are not specifically tailored to the storage adaptors. Then again we probably need a few tests tailored to the storage adaptors. Just my two cents here.
DavidLFielding
left a comment
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 managed to at least glance at most of the files in the PR. Hopefully, my comments are useful in some way. I may check a few more things if I have time but plan to move on to other tasks tomorrow.
At this point, I have not run the service beyond exercising the tests, which all passed.
|
I notice some formatting issues in my checkpoint PR so on a whim I decided to run some checks on File Manager. pylint filemanager
|
|
While I'm at it I ran 'pydocstyle --convention=numpy --add-ignore=D401' There are nearly 60 "Missing docstring" errors. Many of these are located in the newer file_mutations.py/storage.py code. Don't scream yet. mypy generates a large number of errors. Most of these are trivial and easily correctable: easy ones...lots of these... filemanager/domain/tests/test_workspace.py:16: error: Function is missing a return type annotation while some of these other errors might warrant a closer look: filemanager/domain/uploads/stored.py:223: error: "IStorageAdapter" has no attribute "get_size_bytes" along with several of the type mismatch flavor... filemanager/domain/uploads/checkpoint.py:307: error: Argument 1 to "_update_from_checkpoint" of "CheckpointWorkspace" has incompatible type "TranslatableWorkspace"; expected "CheckpointWorkspace" You may want to take a quick look to see if any of these need to be addressed. Tomorrow I will fire up the submission UI suite and test the FM as I resume work on alpha tests. |
Good catches. Some of these are really things that need to be fixed, and some are not. For example, the
|
I'd recommend using $ ./tests/type-check.sh filemanager
filemanager/domain/uploads/stored.py:225: error: "IStorageAdapter" has no attribute "get_size_bytes"
filemanager/domain/uploads/stored.py:249: error: Returning Any from function declared to return "bytes"
filemanager/domain/uploads/translatable.py:77: error: "Type[datetime]" has no attribute "fromisoformat"
filemanager/domain/uploads/translatable.py:109: error: "TranslatableWorkspace" has no attribute "lastupload_readiness"
filemanager/domain/uploads/translatable.py:110: error: "TranslatableWorkspace" has no attribute "status"
filemanager/domain/uploads/translatable.py:111: error: "TranslatableWorkspace" has no attribute "lock_state"
filemanager/domain/uploads/translatable.py:112: error: "TranslatableWorkspace" has no attribute "source_type"
filemanager/domain/uploads/translatable.py:141: error: "Type[TranslatableWorkspace]" has no attribute "Status"
filemanager/domain/uploads/translatable.py:142: error: "Type[TranslatableWorkspace]" has no attribute "LockState"
filemanager/domain/uploads/translatable.py:143: error: "Type[TranslatableWorkspace]" has no attribute "SourceType"
filemanager/domain/uploads/translatable.py:152: error: "Type[TranslatableWorkspace]" has no attribute "Readiness"
filemanager/domain/uploads/checkpoint.py:307: error: Argument 1 to "_update_from_checkpoint" of "CheckpointWorkspace" has incompatible type "TranslatableWorkspace"; expected "CheckpointWorkspace"
filemanager/process/strategy.py:34: error: Function is missing a return type annotation
filemanager/process/strategy.py:34: note: Use "-> None" if function does not return a value
filemanager/process/strategy.py:52: error: Need type annotation for 'tasks'
filemanager/process/strategy.py:56: error: Function is missing a return type annotation
filemanager/process/strategy.py:61: error: Function is missing a return type annotation
filemanager/process/strategy.py:61: note: Use "-> None" if function does not return a value
filemanager/process/strategy.py:74: error: List comprehension has incompatible type List[UploadedFile]; expected List[Tuple[Any, ...]]
filemanager/process/strategy.py:75: error: Call to untyped function "await_completion" in typed context
filemanager/process/strategy.py:84: error: "IChecker" has no attribute "__iter__" (not iterable)
filemanager/process/strategy.py:95: error: No return value expected
filemanager/process/check/file_type.py:399: error: Argument 1 to "_type_of_latex2e" has incompatible type "UploadedFile"; expected "BytesIO"
filemanager/process/check/file_type.py:435: error: Incompatible types in assignment (expression has type "bytes", variable has type "str")
filemanager/process/check/file_type.py:464: error: Argument 1 to "_type_of_latex2e" has incompatible type "IO[Any]"; expected "BytesIO"
filemanager/process/check/cleanup.py:171: error: Argument 1 to "findall" of "Pattern" has incompatible type "mmap"; expected "bytes"
filemanager/process/check/cleanup.py:226: error: Argument 1 to "search" of "Pattern" has incompatible type "str"; expected "bytes"
filemanager/process/check/cleanup.py:233: error: Argument 1 to "search" of "Pattern" has incompatible type "str"; expected "bytes"
filemanager/process/check/cleanup.py:239: error: Argument 1 to "search" of "Pattern" has incompatible type "str"; expected "bytes"
filemanager/process/check/cleanup.py:247: error: Argument 1 to "search" of "Pattern" has incompatible type "str"; expected "bytes"
filemanager/process/check/cleanup.py:253: error: Unsupported operand types for + ("bytes" and "str")
filemanager/process/check/cleanup.py:256: error: Argument 1 to "search" of "Pattern" has incompatible type "str"; expected "bytes"
filemanager/controllers/checkpoint.py:122: error: "print_exc" does not return a value
filemanager/controllers/checkpoint.py:233: error: "print_exc" does not return a value
filemanager/controllers/checkpoint.py:282: error: "print_exc" does not return a value
filemanager/controllers/upload.py:101: error: Incompatible types in assignment (expression has type "None", variable has type "UploadWorkspace")
filemanager/controllers/upload.py:177: error: Argument 1 to "create" of "FileMutationsWorkspace" has incompatible type "Union[str, None, Any]"; expected "str"
filemanager/controllers/files.py:221: error: "UploadFileSecurityError" has no attribute "description" |
The initial implementation of the file manager service focused on replicating the behavior of the legacy file checks, and so closely reproduced much of the logic and abstractions. This implementation was quite fast in local tests, correct, and had excellent test coverage over known problematic uploads.
Problem: EFS I/O is too slow to run checks
EFS I/O limits scale with overall volume size, which makes things difficult from the start. But even if we provision a high level of I/O ahead of time, there are too many low-level reads and writes to perform file checks on EFS directly. A modestly sized tarball could take around 12 seconds to unpack and check, which is not acceptable.
So we decided that we need to refactor the application to perform checks on a local disk (e.g. in memory), and then move files over to EFS at the end of the request. This way we are keeping I/O to a minimum.
Problem: the core checking and file-shuffling routines were too complex
Because the initial implementation stuck closely to the logic of the legacy system, the file management and checking logic had grown to nearly 3,000 lines largely contained in a single class, with really long methods doing the actual checking. This made it hard to understand what checks were being performed, and make changes that were predictable.
Since we were going to refactor to address the I/O issues, we decided to go ahead and perform an initial refactor for clarity, composability, and extensibility.
Refactor
Here is an overview of the refactor in 2019-06:
Abstracted away the underlying filesystem logic by encapsulating it in
adapters. A
SimpleStorageAdapterimplements logic that is close to the original implementation (on a single volume). AQuarantineStorageAdapterimplements the two-volume logic needed to do fast checks before shipping data to EFS. The storage adapters implement an API/protocol that is formalized in the domain.Separated the file/workspace checks from the upload workspace class itself using multiple-dispatch (something like the visitor pattern). The advantage of this pattern is that we can add more checks, reorder the checks, etc, without creating an even more enormous class. We can also test them separately, which we should start to do...
BaseCheckerclass.SynchronousCheckingStrategyis the first such implementation.
UploadWorkspaceclass is assigned a set of checks and a strategy, which are applied via theUploadWorkspace.perform_checksmethod.For clarity sake, decomposed most of the
UploadWorkspaceinto mixins that add sets offunctionality.
Consolidated the storage of metadata about the workspace.
UploadWorkspacewith its storage adapter. This means that loading the workspace requires only a single function call, and there is no need to keep track of both a filesystem-based workspace object and a database object (both of which represent the workspace) in the controllers.Since the core classes were significantly altered, much of the work involved modifying the extensive test suite to use the new internal APIs, and modifying the controllers to leverage the new patterns in the domain and processes. Since this involved going through the test suite one case at a time, I took the liberty of breaking things up into more manageable pieces:
(
tests/test_api/), smallerTestCases, and smallertest_methods. Hopefully this makes it easier to find relevant tests and also see
what specifically is being tested.
Removed some cruft that wasn't being used and wasn't likely to be used
(async boilerplate, etc).