-
Notifications
You must be signed in to change notification settings - Fork 3k
Add the StorageLite feature #6915
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
e6fefd0
to
368611a
Compare
Let's make sure those dependencies are moving forward |
5e92b01
to
0a55174
Compare
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.
The implementation is looking good. I'm already impressed, I thought the timeline was too short for creating a new filesystem.
However, I have a problem with how this is being integrated.
-
We don't need to introduce a new API
We have an established storage API (POSIX) that has already been adopted by users. We should not change directions without a good reason.
If we must introduce a new storage API, the interface should be available for all storage options that support it. There’s no point in having multiple storage options if the users can’t transition between the storage options.
-
StorageLite should not be tightly coupled to security
There is no technical reason StorageLite and security must be coupled. If it is an API problem, then we should fix our API so we can effectively add storage features without limiting users.
How can we move forward?
- Integrate StorageLite into the POSIX API
- Introduce POSIX secure layer
If POSIX API is proven to be inefficient/unworkable:
- Create KVStore API
- Integrate StorageLite into KVStore API
- Integrate LittleFS and FAT into KVStore API (POSIX adapter?)
- Introduce KVStore secure layer
These two options satisfy the above issues. I'm open to alternatives, but until the above issues are solved, merging this PR will be more disruptive than beneficial.
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
#ifndef MBED_DEVICEKEY_H |
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 this file got added by accident? (the ~HEAD part)
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.
@geky Your view is clear and you presented it several times.
Review of this PR should be done in terms of code quality and not feature definitions. There are good reasons to keep two different APIs for storage but this is not the place to discuss this.
Will appreciate your help on code quality review
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 hope you will find the review itself is constructive.
However this is the correct place to note why a PR should not be merged, otherwise the PR will be merged.
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.
The Head file is part of the dependent DeviceKey PR and should be handled there. @yossi2le are you aware of 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.
@davidsaada The DeviceKey PR looks fine, I have no idea why this file is showing on this PR.
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.
Maybe it was accidentally added while rebasing. Will check.
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.
No rush, you'll need to rebase when dependencies go in anyways
: FileSystem(name), _stlite(stlite), _flags(flags), _bd(0) | ||
{ | ||
MBED_ASSERT(stlite); | ||
mount(_bd); |
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 had to remove this mount call because the _bd pointer was NULL and the init assert would fail.
int ret = _stlite->file_exists((const uint8_t *) path, strlen(path) + 1); | ||
// Fail if file exists | ||
if (ret == STORAGELITE_SUCCESS) { | ||
return EEXIST; |
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.
Should this be negative?
} | ||
|
||
// We don't support opening for read-write, nor for appending | ||
if ((flags & O_RDWR) || (flags & O_APPEND)) { |
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.
Yay! Glad to see full flag support 👍
int StorageLiteFS::file_open(mbed::fs_file_t *file, const char *path, int flags) | ||
{ | ||
file_handle_data_t *handle = new file_handle_data_t; | ||
if (!handle) { |
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 won't come back as NULL, it will assert. I believe you need nothrow:
file_handle_data_t *handle = new (std::nothrow) file_handle_data_t;
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.
Case("StorageLiteFS_fread_with_fopen_w_mode", StorageLiteFS_fread_with_fopen_w_mode), | ||
Case("StorageLiteFS_fread_closed_file", StorageLiteFS_fread_closed_file), | ||
Case("StorageLiteFS_fread_twice_from_file", StorageLiteFS_fread_twice_from_file), | ||
Case("StorageLiteFS_fread_twice_separated_with_fclose", StorageLiteFS_fread_twice_separated_with_fclose), |
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.
Good amount of tests! We need these on all filesystems!
static const size_t bd_read_size = 1; | ||
|
||
StorageLite *stlite = NULL; | ||
HeapBlockDevice bd(bd_size, bd_read_size, bd_prog_size, bd_erase_size); |
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 also have an MBED_TEST_SIM_BLOCKDEVICE which I think is useful here. Otherwise the CI will try to run this on platforms with very little RAM:
https://github.com/ARMmbed/mbed-os/blob/master/features/filesystem/littlefs/TESTS/filesystem_recovery/wear_leveling/main.cpp#L48-L58
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.
Not sure whether you saw the beginning of this file, failing the support of this test if MBED_TEST_BLOCKDEVICE
is not present. This should prevent the execution of this test on low memory boards.
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.
Just to add: The test currently doesn't use the BD exported by MBED_TEST_BLOCKDEVICE
. Reason is that I wanted to have this white box test on different types of BDs (in term of sizes). I'll try adding this BD to the list of tested BDs.
static const char *file3_val1 = "Data value of file 3 is the following"; | ||
static const char *file4_name = "This is the name of file4"; | ||
static const char *file4_val1 = "Is this the value of file 4?"; | ||
static const char *file4_val2 = "What the hell is the value of file 4, god damn it!"; |
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.
😆
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.
General tip, even though this is a test - this is blowing static RAM probably unnecessarily - 4 words of RAM for 4 non-constant pointers.
Suggest static const char * const file4_name
or static const char file4_name[]
.
The presence of the pointer const
qualifier tends to fool one into thinking that the object is const
.
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.
It's in a test and probably optimized out anyways,
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.
Indeed, but it's a common pattern I see, so I like to raise awareness.
And you're right, as these are static
the compiler could well optimise the pointer storage away - if it's analysing enough of the source file at a time to be certain they're not modified.
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.
Good catch @kjbracey-arm. Adding that extra const
actually reduced code size, at least in the ARM GCC compiler.
@@ -0,0 +1,670 @@ | |||
/* |
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.
Out of curiousity what does "whitebox" mean?
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.
White box is a testing term meaning that the test is aware of the internal coding logic (opposite from black box). Here the test code is aware of edge cases (BD and erase sizes for example) and different code paths.
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.
Ah! That makes perfect sense, thanks
Case("StorageLite: White box test", white_box_test, greentea_failure_handler), | ||
Case("StorageLite: White box FS test", white_box_fs_test, greentea_failure_handler), | ||
Case("StorageLite: Multiple set test", multi_set_test, greentea_failure_handler), | ||
Case("StorageLite: Error inject test", error_inject_test, greentea_failure_handler), |
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 is a great number of tests, but I think we're missing a couple important ones:
- Simulated power-resilience testing: example
- Greentea-driven power-resilience testing: example
- Simulated wear-level testing: example
Note: these depend on some common files:
https://github.com/ARMmbed/mbed-os/tree/master/features/filesystem/littlefs/TESTS/COMMON
These already exist on top of the filesystem API, so it shouldn't be too difficult to adopt them here. Until then it would be a risk to advertise power resilience.
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. Adding reset and wear level tests now.
|
||
int StorageLiteFS::mount(BlockDevice *bd) | ||
{ | ||
_bd = bd; |
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.
Error handling/checking of input parameters must be done, what if bd is NULL?
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 don't check NULL pointers elsewhere. Worst case, code will hard fault, and the NULL dereference can easily be found with a debugger.
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.
Yup, and it’s not good. Recently the code base I am touching I try to add where ever I can, hence pointed out. Can be found with debugger but not good for end product
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.
Interesting, I think we disagree here. Should we also be asserting on misaligned pointers?
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.
Error handling does not mean only asserting, but to actually avoid sudden crash and respond with proper errror message/code to help application recover (if possible). Misaligned address access depends on type of underlying block device. You have a good point, I will check how SD driver behaves as I don’t remember on top of my mind. But device should terminate gracefully even in case of unaligned access with error and not crash suddenly.
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.
Every check we add increases code size. At some point we must trust the user is using our APIs correctly. I would consider a hard fault at the site of dereference a rather graceful error compared to other bugs they could run into with this API, ie buffer overruns in file_read.
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.
Strongly agree with @geky here. If you're going to check NULL pointers, you logically need to be checking for misaligned pointers, stale pointers, out-of-range pointers, corrupted data structures, so on and so forth.
If you are going to worry about the validity of pointers you need to be writing in something other than C or C++03.
Are you going to add if (this == NULL)
checks to the top of every method too? You would logically have to in the model you advocate.
And what happens in practice if you do return an error code on an invalid pointer? The caller probably ignores it. So we get subtle effects like we've had with some Mutex::lock()
s not working without anyone realising, causing hard to track-down problems.
Having said all that...
the NULL dereference can easily be found with a debugger.
pyOCD still can't show a backtrace on a Cortex-M HardFault, as far as I know. It's been like that for a few years now. Rather undermines our argument, if pyOCD is our preferred debugger.
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.
pyOCD still can't show a backtrace on a Cortex-M HardFault
Oh, this would be a good thing to fix!
|
||
int StorageLiteFS::remove(const char *filename) | ||
{ | ||
int ret = _stlite->remove((const uint8_t *) filename, strlen(filename) + 1); |
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.
Error handling/checking- filename
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.
Disagree, but the casts suggest a type problem - you should be passing filenames around as char *
pointers, just like any string. If you need to cast to uint8_t *
it should be lower level than this.
It this is converting to an opaque key, the opaque key's type should probably be void *
to make caller's lives easier. And I wouldn't have thought you needed to include the NUL byte as part of the key, if you're giving explicit key lengths.
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. The idea was to have opaque keys. will change to void *
and remove the null terminator.
{ | ||
size_t file_size; | ||
int ret = _stlite->get_file_size((const uint8_t *) name, strlen(name) + 1, file_size); | ||
st->st_size = file_size; |
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.
Error handling/checking- name / st. Accessing NULL st here can cause crash.
strcpy(handle->file_name, path); | ||
handle->open_flags = flags; | ||
|
||
*file = handle; |
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.
Error handling - user can pass NULL to file pointer.
Agree that all the basic functions here - get/set/remove/get_file_size/get_next are perfectly standard filing system APIs and should be accessible via There's no justification for not being able to do
Apps shouldn't need to be written to a special custom API to do something that simple. Certainly calling this a "filing system" is rather confusing given that it is not actually an mbed OS filing system. |
} | ||
} | ||
|
||
handle->file_name = new char[strlen(path) + 1]; |
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.
would consider verifying strlen(path) is smaller than max file name size to prevent memory corruption or security problem
@geky @kjbracey-arm Many of your observations were discussed in the design phase, so not sure they should also be discussed here as well. Still here's my take:
|
There may a reason for an "optimised" custom API. If this were an entirely internal component written just for the purposes of mbed-client, and you were prepared to put the necessary work into its "storage abstraction layer", then it would be fine to stop there. But that's still no reason to not support the standard API. There is nothing particularly unusual about a filing system with your restrictions, and there are many out there. And the majority of applications will work perfectly fine on them. LTFS is a good example - it's a tape filing system, so has all the usual restrictions of tape - no random access, no reclaiming of space when a file is deleted until the tape is reformatted. But it works fine for a lot of purposes with no custom code by just presenting itself as a standard POSIX filing system. Yes, dragging a load of files off a tape from Windows filer window can be inefficient, because it copies in alphabetical order rather than tape order, causing unnecessary seeking, but it works. And it doesn't preclude someone using a custom "tape-aware" program to do it better. So even if the native interface remains as it stands, it should be an urgent priority to "complete the job" by providing an actual And on atomicity, I was assuming the atomicity would be achieved simplistically by buffering the operation in the I'm not aware of a demand to manipulate excessively large items. If they were that large it would be equally unreasonable to expect them to be in a contiguous application buffer for the "set" or "get" as you currently do. Providing the POSIX interface could in some cases just move the required buffer from the app into the FileHandle, without increasing the total RAM. |
@davidsaada, I like your summary. However, I disagree with some of your assumptions:
|
0a55174
to
b15bdf3
Compare
Pushed a commit with a few changes suggested in the comments. Mainly trying to handle build issues. |
b15bdf3
to
690ad5e
Compare
690ad5e
to
7aa5f78
Compare
I strongly recommend to limit the PR review to code and not feature or product definitions. |
1dccac7
to
ffb3d18
Compare
@geky @kjbracey-arm @deepikabhavnani are you happy with the updates to this PR ? |
@adbridge From my POV, main gaps I still need to fill are the additional tests (resilience and wear level) and fixing the README according to the comments. |
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.
- +1 for the additional tests (resilience and wear level)
- At minimum we still need a KVStore interface (abstract base class) for the new API that StorageLite implements.
And I still have problems with the design as stated above, this is being reviewed internally.
Marking as DNM due to ongoing internal review. |
1. Return errors when attempting to erase a worn out erase unit or write to it. 2. Support erase of more than one erase unit (in one call).
fa20816
to
cfaaa70
Compare
cfaaa70
to
1234a0b
Compare
(as it's a post release weekend) |
Build : SUCCESSBuild number : 2225 Triggering tests/morph test |
Test : FAILUREBuild number : 2016 |
Exporter Build : SUCCESSBuild number : 1853 |
From our (@ARMmbed/mbed-os-maintainers) understanding, StorageLite requires a redesign before it can be reintroduced into Mbed OS. Since we don't support/allow PRs to remain open for development purposes, I'm going to close this PR. |
Description
StorageLite is a storage solution, providing a key value store like API of set/get data or a reduced POSIX API of open-write-close and open-read-close.
Design & documentation by @offirko
General and file system tests by @theamirocohen
Handbook documentation PR
This PR depends on the following open PRs:
#6757 - Buffered block device
#6559 - Flash simulator block device
#6480 - NVStore allocate_key API
#6642 - Device key
Pull request type