-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Dart File.writeAsString() method does not write to file if await is not done immediately #36087
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
Comments
You are asking the operating system to append to a file 100 times, before any of them get the chance to actually do the append. So, all of them see the existing zero-length file, then all of them increase the length, and then all of them write into the newly allocated space, overwriting each other. The documentation for |
Thank you very much for the answer. I had already opened a question on StackOverflow, if you'd like to add details there later (or when the issue is resolved) it could be helpful for others: https://stackoverflow.com/questions/54958346/dart-file-writeasstring-method-does-not-write-to-file-if-await-is-not-done-imm |
Yeah it doesn't look like appending is actually implemented in the Dart SDK. I'll turn this bug into a request for implementing the appending behavior. |
Alright, I understand how FileMode.append is implemented. It is implemented, but not quite as I expected. The file handled is seeked to the end after it's opened. That's why it's possible that the send() calls all write to the start of the file, which is where they were opened. If they are done sequentially, the correct file offset is used. The current documentation for FileMode.append is: /// Mode for opening a file for reading and writing to the
/// end of it. The file is created if it does not already exist.
static const append = const FileMode._internal(2); This technically doesn't mention the append semantics, though "writing to the end of it" does a bit suggest the The action items I see here is perhaps clarifying the documentation and potentially implementing the O_APPEND mode (and its Windows equivalent) as this is a useful feature for things like log files and such (although ensuring concurrent long writes to those files are atomic and don't interleave may not be possible without file locking). |
|
Synchronization is possible using if |
I experienced this problem also when I was calling |
This program in Go, which should be roughly equivalent to the Dart one, writes every line (though out-of-order, as I expected) and prints the expected output: package main
import (
"fmt"
"os"
"sync"
)
const filename = "results.txt"
func main() {
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go send(fmt.Sprintln(i), &wg)
}
wg.Wait()
print("Done")
}
func send(msg string, wg *sync.WaitGroup) {
defer wg.Done()
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
defer f.Close()
_, err = f.WriteString(msg)
if err != nil {
panic(err)
}
stat, err := f.Stat()
if err != nil {
panic(err)
}
fmt.Println(stat.Size())
} |
Yes, I confirm. I was reproducing the problem in C using a wrong combination of open(2) flags/mode that make me wrong about open(2) behavoir. Sorry for the noise! |
I believe this might help someone Basically here I'm handling things myself so that I don't have to rely on the system to handle it for me I could have used writeAsStringSync (which I imagine would have fixed this problem) but I didn't want to slow anything down since the data being saved is just for the user's convenience So I instead opted for simply creating a request Write function that I called "safeSave" You could just finish the current request and then process the next newest one
|
Here is the version that just keeps track of 2 things
|
Maybe we should document the behavior like It is unsafe to use filehandle.write() multiple times on the same file without waiting for the promise to be resolved (or rejected). For this scenario, use filehandle.createWriteStream(). |
Is there any solution? |
The safest solution is to wait for the previous future before writing the next string. |
Can someone help me with an answer here pretty please? what happens if i call this function multiple times in a row, without awaiting for it? Is it safe?
here's an answer from ChatGPT. Is it possible to get concurrent writes? Concurrent Writes: Since the saveData function is asynchronous and not being awaited, multiple instances of it will run concurrently. This can lead to race conditions where multiple writes to the file might overlap, causing data corruption or unexpected results. |
Seems ChatsGPT stumbled on the truth here. Not awaiting the writes directly, or wrapping then in an async function which does await the write, but then not awaiting that function call, gives the same result: the next operation is stated before the former has completed. |
@lrhn thanks! Can you elaborate a bit on "the next operation is started before the former has completed" ? I've added some prints to saveData and written this test to understand what's happening
When running this test, I can see in the output that the prints before file.writeAsString always come in consecutive order, the prints that come after don't:
So getting back to your statement: "the next operation is started before the former has completed" What exactly does it mean in this context, and why did ChatGPT stumble on the truth here? |
An Everything after that first The test of the body code needs to complete before the returned future is completed. That's why everything before the await happens in order, because it happens synchronously. Then the loop is done, control returns to the event loop, and then the awaited futures can start competing in whichever order they complete in. |
Thanks! The part that gets me intrigued is: and then the awaited futures can start competing in whichever order they complete in. What determines the order they complete in? In my test, the data that needs to be written to file as text does not change between the calls to saveData(). If the data would change, I would imagine that the call to saveData() that needed to write the "least amout of data" would complete first. I'm sure in practice it is not like that, and there are other things to consider.. I have a weird bug in production which occurs very rarely, and I'm trying to understand whether this could be the problem. There I'm calling saveData() 4 times in a row without awaiting them, and the data that needs to be written DOES change between consecutive calls. It's a JSON that grows with one field between each call. E.g. update json: // first call to save data update json: // 2nd call to save data update json: // 3rd call to save data the file ends up with a state of this sort "key1" : "value 1" I'm looking at this package & example, and I feel I'm hitting the same problem. Which can only mean that there could be concurrent writes happening and chatGPT is right. @lrhn thanks for taking the time to reply |
That depends on the operation. In this case the operation is an I/O operation, Which basically means "any order is possible", likely with a tendency towards operations that started earlier also ending earlier, but outliers can and will happen. If the For your code, you may want to have a "task scheduler" that performs operations when ready, and maybe even discard intermediate reads. import "dart:async" show Completer;
import "dart:collection" show Queue;
import "dart:convert" show Encoding, utf8;
import "dart:io" show File;
class AsyncWriter {
final File file;
final Queue<(Completer<void> result, Future<void> Function() write)>
_pendingWrites = Queue();
AsyncWriter(this.file);
void writeAsString(String string, {Encoding encoding = utf8}) {
var wasEmpty = _pendingWrites.isEmpty;
_pendingWrites.add((
Completer<void>.sync(),
() => file.writeAsString(string, encoding: encoding)
));
if (wasEmpty) _writeNext();
}
void _writeNext() async {
do {
// Skip to the last write request, ignore intermediate write requests.
var (completer, write) = _pendingWrites.last;
try {
try {
await write();
} finally {
_completeUntil(completer);
}
completer.complete(null);
} catch (e, s) {
completer.completeError(e, s);
}
// Continue if any new writes were added while writing this one.
} while (_pendingWrites.isNotEmpty);
}
// Removes pending writes up to and including the actual write just performed.
// Completes all prior operations.
void _completeUntil(Completer<void> actualCompleter) {
while (true) {
var (nextCompleter, _) = _pendingWrites.removeFirst();
if (identical(actualCompleter, nextCompleter)) return;
}
}
} Warning: Untested code. |
Thanks for the code example & explanation. In my case there was a much easier solution to fix the problem (there's a writeAsStringSync() which I can call just once). I was more interested to understand if it is possible to end up with data in a file that is not representative of any single saveData() call. e.g.
If there's a yes or no answer to this question, I would be interested to find out. |
I agree that this should be documented, especially as it seems to be platform specific (I can easily reproduce this issue on Windows, but cannot reproduce on MacOS) |
@Jordan-Nelson strange, I reported this issue originally on Linux, but just tried it on Mac M1 and the result is the same (and I am pretty sure it's the same on x86 arch). Tested on DartVM:
What behaviour did you see on Mac? |
dart --version
)Dart VM version: 2.2.0 (Unknown timestamp) on "linux_x64"
Linux 4.4.0-139-generic #165-Ubuntu SMP Wed Oct 24 10:58:50 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
N/A
I have the following Dart code that doesn't behave as I expected:
Expected behaviour
I expected the file to be written as soon as each call to
await futures[i]
in the second loop returned. However this does not seem to be happening.The file should contain one line for each index from 0 to 99.
The length of the file that is printed on each iteration of the
await
loop should show the file's length increasing on each call. Example:Observed behaviour
Only the last call in the loop seems to write to the file. The resulting file contains a line with 99 followed by an empty line.
The print calls in the second loop always print the same file length, 3:
The event loop seems to be somehow merging the calls and only actually executing the last call, even though I still get 100 different futures that I await in the second loop.
The argument for each call to
file.writeAsString()
is different on every call, so there should be no merging happening.If the code is modified to
await
on each call tosend()
, then the expected behaviour is observed, but that means the caller ofsend()
cannot proceed without waiting for the file to be written to, which is not the desired behaviour (the caller does wait later, in the second loop).Posted on StackOverflow: https://stackoverflow.com/questions/54958346/dart-file-writeasstring-method-does-not-write-to-file-if-await-is-not-done-imm
The text was updated successfully, but these errors were encountered: