Properly handle error while saving files

When an error occurs while saving a file we need to put the associated
file editor back in the dirty editors list. Otherwise data may be
lost. Prior to this fix once i/o failed, the editor was considered clean
and i/o would not be retried unless the editor was modified again. So
for example if a designer attribute was changed, but the i/o failed, it
would not get saved again, even if a change was made in the blocks
editor and even if the “Save project” menu was used.

Note: We do not re-schedule the failed i/o but instead depend on another
user change (anywhere) or explicit use of the “Save project” menu. I
considered re-scheduling the i/o but am concerned that if i/o is failing
due to a server problem, a lot of clients retrying i/o may make it worse
via the “thundering herd!”

Change-Id: Ife50c3c20d407b4e009c639bc36c2596331342d7
parent c2bd83d4
...@@ -14,7 +14,6 @@ import com.google.appinventor.client.OdeAsyncCallback; ...@@ -14,7 +14,6 @@ import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.editor.youngandroid.YaBlocksEditor; import com.google.appinventor.client.editor.youngandroid.YaBlocksEditor;
import com.google.appinventor.client.editor.youngandroid.YailGenerationException; import com.google.appinventor.client.editor.youngandroid.YailGenerationException;
import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.settings.project.ProjectSettings; import com.google.appinventor.client.settings.project.ProjectSettings;
import com.google.appinventor.shared.rpc.BlocksTruncatedException; import com.google.appinventor.shared.rpc.BlocksTruncatedException;
import com.google.appinventor.shared.rpc.project.FileDescriptorWithContent; import com.google.appinventor.shared.rpc.project.FileDescriptorWithContent;
...@@ -24,6 +23,7 @@ import com.google.gwt.user.client.Command; ...@@ -24,6 +23,7 @@ import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Timer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -54,6 +54,7 @@ public final class EditorManager { ...@@ -54,6 +54,7 @@ public final class EditorManager {
// Fields used for saving and auto-saving. // Fields used for saving and auto-saving.
private final Set<ProjectSettings> dirtyProjectSettings; private final Set<ProjectSettings> dirtyProjectSettings;
private final Set<FileEditor> dirtyFileEditors; private final Set<FileEditor> dirtyFileEditors;
private final HashMap<String,FileEditor> pendingFileEditors;
private final Timer autoSaveTimer; private final Timer autoSaveTimer;
private boolean autoSaveIsScheduled; private boolean autoSaveIsScheduled;
private long autoSaveRequestTime; private long autoSaveRequestTime;
...@@ -71,6 +72,7 @@ public final class EditorManager { ...@@ -71,6 +72,7 @@ public final class EditorManager {
dirtyProjectSettings = new HashSet<ProjectSettings>(); dirtyProjectSettings = new HashSet<ProjectSettings>();
dirtyFileEditors = new HashSet<FileEditor>(); dirtyFileEditors = new HashSet<FileEditor>();
pendingFileEditors = new HashMap<String,FileEditor>();
autoSaveTimer = new Timer() { autoSaveTimer = new Timer() {
@Override @Override
...@@ -192,7 +194,7 @@ public final class EditorManager { ...@@ -192,7 +194,7 @@ public final class EditorManager {
if (!fileEditor.isDamaged()) { // Don't save damaged files if (!fileEditor.isDamaged()) { // Don't save damaged files
dirtyFileEditors.add(fileEditor); dirtyFileEditors.add(fileEditor);
} else { } else {
OdeLog.log("Not saving blocks for " + fileEditor.getFileId() + " because it is damaged."); Ode.CLog("Not saving blocks for " + fileEditor.getFileId() + " because it is damaged.");
} }
scheduleAutoSaveTimer(); scheduleAutoSaveTimer();
} }
...@@ -254,6 +256,7 @@ public final class EditorManager { ...@@ -254,6 +256,7 @@ public final class EditorManager {
FileDescriptorWithContent fileContent = new FileDescriptorWithContent( FileDescriptorWithContent fileContent = new FileDescriptorWithContent(
fileEditor.getProjectId(), fileEditor.getFileId(), fileEditor.getRawFileContent()); fileEditor.getProjectId(), fileEditor.getFileId(), fileEditor.getRawFileContent());
filesToSave.add(fileContent); filesToSave.add(fileContent);
pendingFileEditors.put(fileEditor.getFileId(), fileEditor); // pending save
} }
dirtyFileEditors.clear(); dirtyFileEditors.clear();
...@@ -276,6 +279,9 @@ public final class EditorManager { ...@@ -276,6 +279,9 @@ public final class EditorManager {
@Override @Override
public void execute() { public void execute() {
if (pendingSaveOperations.decrementAndGet() == 0) { if (pendingSaveOperations.decrementAndGet() == 0) {
// We get here when all save operations have completed, either
// with success or not.
pendingFileEditors.clear(); // Failed I/O will be back in dirtyFileEditors
// Execute the afterSaving command if one was given. // Execute the afterSaving command if one was given.
if (afterSaving != null) { if (afterSaving != null) {
afterSaving.execute(); afterSaving.execute();
...@@ -386,6 +392,7 @@ public final class EditorManager { ...@@ -386,6 +392,7 @@ public final class EditorManager {
final long projectId = fileDescriptor.getProjectId(); final long projectId = fileDescriptor.getProjectId();
final String fileId = fileDescriptor.getFileId(); final String fileId = fileDescriptor.getFileId();
final String content = fileDescriptor.getContent(); final String content = fileDescriptor.getContent();
Ode.CLog("Saving fileId " + fileId + " for projectId " + projectId);
Ode.getInstance().getProjectService().save2(Ode.getInstance().getSessionId(), Ode.getInstance().getProjectService().save2(Ode.getInstance().getSessionId(),
projectId, fileId, false, content, new OdeAsyncCallback<Long>(MESSAGES.saveErrorMultipleFiles()) { projectId, fileId, false, content, new OdeAsyncCallback<Long>(MESSAGES.saveErrorMultipleFiles()) {
@Override @Override
...@@ -410,8 +417,23 @@ public final class EditorManager { ...@@ -410,8 +417,23 @@ public final class EditorManager {
if (caught instanceof BlocksTruncatedException) { if (caught instanceof BlocksTruncatedException) {
Ode.getInstance().blocksTruncatedDialog(projectId, fileId, content, this); Ode.getInstance().blocksTruncatedDialog(projectId, fileId, content, this);
} else { } else {
// We mark the file editor as dirty again because the save failed.
//
// Note: I considered re-scheduling the auto-save and decided against
// it. One reason we might be getting errors is due to a problem with
// the server. If a lot of clients start re-scheduling saves, this might
// make the situation worse due to the "thundering Herd!" So we compromise
// we mark the editors as dirty, so the next update by the user to any
// file will retry all of the non-saved files. The "Save Project" menu
// item will also re-attempt the failed I/O
if (pendingFileEditors.containsKey(fileId)) {
dirtyFileEditors.add(pendingFileEditors.get(fileId));
}
super.onFailure(caught); super.onFailure(caught);
} }
if (afterSavingFiles != null) { // Need to call this to decrement the count
afterSavingFiles.execute(); // of files saved (or not in this case)
}
} }
}); });
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment