Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(82)

Side by Side Diff: chrome/browser/sync/engine/syncer_util.cc

Issue 2844037: Fix handling of undeletion within the syncer. (Closed) Base URL: http://src.chromium.org/git/chromium.git
Patch Set: Whitespace. Created 10 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/sync/engine/syncer_unittest.cc ('k') | chrome/browser/sync/engine/syncproto.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/sync/engine/syncer_util.h" 5 #include "chrome/browser/sync/engine/syncer_util.h"
6 6
7 #include <set> 7 #include <set>
8 #include <string> 8 #include <string>
9 #include <vector> 9 #include <vector>
10 10
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
125 syncable::WriteTransaction* trans, 125 syncable::WriteTransaction* trans,
126 syncable::MutableEntry* entry, 126 syncable::MutableEntry* entry,
127 const syncable::Id& new_id) { 127 const syncable::Id& new_id) {
128 syncable::Directory::ChildHandles children; 128 syncable::Directory::ChildHandles children;
129 ChangeEntryIDAndUpdateChildren(trans, entry, new_id, &children); 129 ChangeEntryIDAndUpdateChildren(trans, entry, new_id, &children);
130 } 130 }
131 131
132 // static 132 // static
133 void SyncerUtil::AttemptReuniteClientTag(syncable::WriteTransaction* trans, 133 void SyncerUtil::AttemptReuniteClientTag(syncable::WriteTransaction* trans,
134 const SyncEntity& server_entry) { 134 const SyncEntity& server_entry) {
135 if (!server_entry.has_client_defined_unique_tag() ||
136 server_entry.client_defined_unique_tag().empty()) {
137 return;
138 }
135 139
136 // Expected entry points of this function: 140 // Expected entry points of this function:
137 // SyncEntity has NOT been applied to SERVER fields. 141 // SyncEntity has NOT been applied to SERVER fields.
138 // SyncEntity has NOT been applied to LOCAL fields. 142 // SyncEntity has NOT been applied to LOCAL fields.
139 // DB has not yet been modified, no entries created for this update. 143 // DB has not yet been modified, no entries created for this update.
140 144
141 // When a server sends down a client tag, the following cases can occur: 145 // When a server sends down a client tag, the following cases can occur:
142 // 1) Client has entry for tag already, ID is server style, matches 146 // 1) Client has entry for tag already, ID is server style, matches
143 // 2) Client has entry for tag already, ID is server, doesn't match. 147 // 2) Client has entry for tag already, ID is server, doesn't match.
144 // 3) Client has entry for tag already, ID is local, (never matches) 148 // 3) Client has entry for tag already, ID is local, (never matches)
145 // 4) Client has no entry for tag 149 // 4) Client has no entry for tag
146 150
147 // Case 1, we don't have to do anything since the update will 151 // Case 1, we don't have to do anything since the update will
148 // work just fine. Update will end up in the proper entry, via ID lookup. 152 // work just fine. Update will end up in the proper entry, via ID lookup.
149 // Case 2 - Should never happen. We'd need to change the 153 // Case 2 - Should never happen. We'd need to change the
150 // ID of the local entry, we refuse. We'll skip this in VERIFY. 154 // ID of the local entry, we refuse. We'll skip this in VERIFY.
151 // Case 3 - We need to replace the local ID with the server ID. Conflict 155 // Case 3 - We need to replace the local ID with the server ID so that
152 // resolution must occur, but this is prior to update application! This case 156 // this update gets targeted at the correct local entry; we expect conflict
153 // should be rare. For now, clobber client changes entirely. 157 // resolution to occur.
154 // Case 4 - Perfect. Same as case 1. 158 // Case 4 - Perfect. Same as case 1.
155 159
156 syncable::MutableEntry local_entry(trans, syncable::GET_BY_CLIENT_TAG, 160 syncable::MutableEntry local_entry(trans, syncable::GET_BY_CLIENT_TAG,
157 server_entry.client_defined_unique_tag()); 161 server_entry.client_defined_unique_tag());
158 162
159 // The SyncAPI equivalent of this function will return !good if IS_DEL. 163 // The SyncAPI equivalent of this function will return !good if IS_DEL.
160 // The syncable version will return good even if IS_DEL. 164 // The syncable version will return good even if IS_DEL.
161 // TODO(chron): Unit test the case with IS_DEL and make sure. 165 // TODO(chron): Unit test the case with IS_DEL and make sure.
162 if (local_entry.good()) { 166 if (local_entry.good()) {
163 if (local_entry.Get(ID).ServerKnows()) { 167 if (local_entry.Get(ID).ServerKnows()) {
164 // In release config, this will just continue and we'll 168 // In release config, this will just continue and we'll
165 // throw VERIFY_FAIL later. 169 // throw VERIFY_FAIL later.
166 // This is Case 1 on success, Case 2 if it fails. 170 // This is Case 1 on success, Case 2 if it fails.
167 DCHECK(local_entry.Get(ID) == server_entry.id()); 171 DCHECK(local_entry.Get(ID) == server_entry.id());
168 } else { 172 } else {
169 // Case 3: We have a local entry with the same client tag. 173 // Case 3: We have a local entry with the same client tag.
170 // We can't have two updates with the same client tag though. 174 // We should change the ID of the local entry to the server entry.
171 // One of these has to go. Let's delete the client entry and move it 175 // This will result in an server ID with base version == 0, but that's
172 // aside. This will cause a delete + create. The client API user must 176 // a legal state for an item with a client tag. By changing the ID,
173 // handle this correctly. In this situation the client must have created 177 // server_entry will now be applied to local_entry.
174 // this entry but not yet committed it for the first time. Usually the 178 ChangeEntryIDAndUpdateChildren(trans, &local_entry, server_entry.id());
175 // client probably wants the server data for this instead. 179 DCHECK(0 == local_entry.Get(BASE_VERSION) ||
176 // Other strategies to handle this are a bit flaky. 180 CHANGES_VERSION == local_entry.Get(BASE_VERSION));
177 DCHECK(local_entry.Get(IS_UNAPPLIED_UPDATE) == false);
178 local_entry.Put(IS_UNSYNCED, false);
179 local_entry.Put(IS_DEL, true);
180 // Needs to get out of the index before our update can be put in.
181 local_entry.Put(UNIQUE_CLIENT_TAG, "");
182 } 181 }
183 } 182 }
184 // Case 4: Client has no entry for tag, all green. 183 // Case 4: Client has no entry for tag, all green.
185 } 184 }
186 185
187 // static 186 // static
188 void SyncerUtil::AttemptReuniteLostCommitResponses( 187 void SyncerUtil::AttemptReuniteLostCommitResponses(
189 syncable::WriteTransaction* trans, 188 syncable::WriteTransaction* trans,
190 const SyncEntity& server_entry, 189 const SyncEntity& server_entry,
191 const string& client_id) { 190 const string& client_id) {
192 // If a commit succeeds, but the response does not come back fast enough 191 // If a commit succeeds, but the response does not come back fast enough
193 // then the syncer might assume that it was never committed. 192 // then the syncer might assume that it was never committed.
194 // The server will track the client that sent up the original commit and 193 // The server will track the client that sent up the original commit and
195 // return this in a get updates response. When this matches a local 194 // return this in a get updates response. When this matches a local
196 // uncommitted item, we must mutate our local item and version to pick up 195 // uncommitted item, we must mutate our local item and version to pick up
197 // the committed version of the same item whose commit response was lost. 196 // the committed version of the same item whose commit response was lost.
198 // There is however still a race condition if the server has not 197 // There is however still a race condition if the server has not
199 // completed the commit by the time the syncer tries to get updates 198 // completed the commit by the time the syncer tries to get updates
200 // again. To mitigate this, we need to have the server time out in 199 // again. To mitigate this, we need to have the server time out in
201 // a reasonable span, our commit batches have to be small enough 200 // a reasonable span, our commit batches have to be small enough
202 // to process within our HTTP response "assumed alive" time. 201 // to process within our HTTP response "assumed alive" time.
203 202
204 // We need to check if we have a that didn't get its server 203 // We need to check if we have an entry that didn't get its server
205 // id updated correctly. The server sends down a client ID 204 // id updated correctly. The server sends down a client ID
206 // and a local (negative) id. If we have a entry by that 205 // and a local (negative) id. If we have a entry by that
207 // description, we should update the ID and version to the 206 // description, we should update the ID and version to the
208 // server side ones to avoid multiple commits to the same name. 207 // server side ones to avoid multiple commits to the same name.
209 if (server_entry.has_originator_cache_guid() && 208 if (server_entry.has_originator_cache_guid() &&
210 server_entry.originator_cache_guid() == client_id) { 209 server_entry.originator_cache_guid() == client_id) {
211 syncable::Id server_id = syncable::Id::CreateFromClientString( 210 syncable::Id server_id = syncable::Id::CreateFromClientString(
212 server_entry.originator_client_item_id()); 211 server_entry.originator_client_item_id());
213 CHECK(!server_id.ServerKnows()); 212 DCHECK(!server_id.ServerKnows());
214 syncable::MutableEntry local_entry(trans, GET_BY_ID, server_id); 213 syncable::MutableEntry local_entry(trans, GET_BY_ID, server_id);
215 214
216 // If it exists, then our local client lost a commit response. 215 // If it exists, then our local client lost a commit response.
217 if (local_entry.good() && !local_entry.Get(IS_DEL)) { 216 if (local_entry.good() && !local_entry.Get(IS_DEL)) {
218 int64 old_version = local_entry.Get(BASE_VERSION); 217 int64 old_version = local_entry.Get(BASE_VERSION);
219 int64 new_version = server_entry.version(); 218 int64 new_version = server_entry.version();
220 CHECK(old_version <= 0); 219 DCHECK(old_version <= 0);
221 CHECK(new_version > 0); 220 DCHECK(new_version > 0);
222 // Otherwise setting the base version could cause a consistency failure. 221 // Otherwise setting the base version could cause a consistency failure.
223 // An entry should never be version 0 and SYNCED. 222 // An entry should never be version 0 and SYNCED.
224 CHECK(local_entry.Get(IS_UNSYNCED)); 223 DCHECK(local_entry.Get(IS_UNSYNCED));
225 224
226 // Just a quick sanity check. 225 // Just a quick sanity check.
227 CHECK(!local_entry.Get(ID).ServerKnows()); 226 DCHECK(!local_entry.Get(ID).ServerKnows());
228 227
229 LOG(INFO) << "Reuniting lost commit response IDs" << 228 LOG(INFO) << "Reuniting lost commit response IDs" <<
230 " server id: " << server_entry.id() << " local id: " << 229 " server id: " << server_entry.id() << " local id: " <<
231 local_entry.Get(ID) << " new version: " << new_version; 230 local_entry.Get(ID) << " new version: " << new_version;
232 231
233 local_entry.Put(BASE_VERSION, new_version); 232 local_entry.Put(BASE_VERSION, new_version);
234 233
235 ChangeEntryIDAndUpdateChildren(trans, &local_entry, server_entry.id()); 234 ChangeEntryIDAndUpdateChildren(trans, &local_entry, server_entry.id());
236 235
237 // We need to continue normal processing on this update after we 236 // We need to continue normal processing on this update after we
238 // reunited its ID. 237 // reunited its ID.
239 } 238 }
240 // !local_entry.Good() means we don't have a left behind entry for this 239 // !local_entry.Good() means we don't have a left behind entry for this
241 // ID. We successfully committed before. In the future we should get rid 240 // ID in sync database. This could happen if we crashed after successfully
242 // of this system and just have client side generated IDs as a whole. 241 // committing an item that never was flushed to disk.
243 } 242 }
244 } 243 }
245 244
246 // static 245 // static
247 UpdateAttemptResponse SyncerUtil::AttemptToUpdateEntry( 246 UpdateAttemptResponse SyncerUtil::AttemptToUpdateEntry(
248 syncable::WriteTransaction* const trans, 247 syncable::WriteTransaction* const trans,
249 syncable::MutableEntry* const entry, 248 syncable::MutableEntry* const entry,
250 ConflictResolver* resolver, 249 ConflictResolver* resolver,
251 Cryptographer* cryptographer) { 250 Cryptographer* cryptographer) {
252 251
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after
345 344
346 } // namespace 345 } // namespace
347 346
348 // Pass in name and checksum because of UTF8 conversion. 347 // Pass in name and checksum because of UTF8 conversion.
349 // static 348 // static
350 void SyncerUtil::UpdateServerFieldsFromUpdate( 349 void SyncerUtil::UpdateServerFieldsFromUpdate(
351 MutableEntry* local_entry, 350 MutableEntry* local_entry,
352 const SyncEntity& server_entry, 351 const SyncEntity& server_entry,
353 const string& name) { 352 const string& name) {
354 if (server_entry.deleted()) { 353 if (server_entry.deleted()) {
354 if (local_entry->Get(SERVER_IS_DEL)) {
355 // If we already think the item is server-deleted, we're done.
356 // Skipping these cases prevents our committed deletions from coming
357 // back and overriding subsequent undeletions. For non-deleted items,
358 // the version number check has a similar effect.
359 return;
360 }
355 // The server returns very lightweight replies for deletions, so we don't 361 // The server returns very lightweight replies for deletions, so we don't
356 // clobber a bunch of fields on delete. 362 // clobber a bunch of fields on delete.
357 local_entry->Put(SERVER_IS_DEL, true); 363 local_entry->Put(SERVER_IS_DEL, true);
358 local_entry->Put(SERVER_VERSION, 364 if (!local_entry->Get(UNIQUE_CLIENT_TAG).empty()) {
359 std::max(local_entry->Get(SERVER_VERSION), 365 // Items identified by the client unique tag are undeletable; when
360 local_entry->Get(BASE_VERSION)) + 1L); 366 // they're deleted, they go back to version 0.
367 local_entry->Put(SERVER_VERSION, 0);
368 } else {
369 // Otherwise, fake a server version by bumping the local number.
370 local_entry->Put(SERVER_VERSION,
371 std::max(local_entry->Get(SERVER_VERSION),
372 local_entry->Get(BASE_VERSION)) + 1);
373 }
361 local_entry->Put(IS_UNAPPLIED_UPDATE, true); 374 local_entry->Put(IS_UNAPPLIED_UPDATE, true);
362 return; 375 return;
363 } 376 }
364 377
365 CHECK(local_entry->Get(ID) == server_entry.id()) 378 DCHECK(local_entry->Get(ID) == server_entry.id())
366 << "ID Changing not supported here"; 379 << "ID Changing not supported here";
367 local_entry->Put(SERVER_PARENT_ID, server_entry.parent_id()); 380 local_entry->Put(SERVER_PARENT_ID, server_entry.parent_id());
368 local_entry->Put(SERVER_NON_UNIQUE_NAME, name); 381 local_entry->Put(SERVER_NON_UNIQUE_NAME, name);
369 local_entry->Put(SERVER_VERSION, server_entry.version()); 382 local_entry->Put(SERVER_VERSION, server_entry.version());
370 local_entry->Put(SERVER_CTIME, 383 local_entry->Put(SERVER_CTIME,
371 ServerTimeToClientTime(server_entry.ctime())); 384 ServerTimeToClientTime(server_entry.ctime()));
372 local_entry->Put(SERVER_MTIME, 385 local_entry->Put(SERVER_MTIME,
373 ServerTimeToClientTime(server_entry.mtime())); 386 ServerTimeToClientTime(server_entry.mtime()));
374 local_entry->Put(SERVER_IS_DIR, server_entry.IsFolder()); 387 local_entry->Put(SERVER_IS_DIR, server_entry.IsFolder());
375 if (server_entry.has_server_defined_unique_tag()) { 388 if (server_entry.has_server_defined_unique_tag()) {
(...skipping 122 matching lines...) Expand 10 before | Expand all | Expand 10 after
498 LOG(INFO) << "Splitting server information, local entry: " << *entry << 511 LOG(INFO) << "Splitting server information, local entry: " << *entry <<
499 " server entry: " << new_entry; 512 " server entry: " << new_entry;
500 } 513 }
501 514
502 // This function is called on an entry when we can update the user-facing data 515 // This function is called on an entry when we can update the user-facing data
503 // from the server data. 516 // from the server data.
504 // static 517 // static
505 void SyncerUtil::UpdateLocalDataFromServerData( 518 void SyncerUtil::UpdateLocalDataFromServerData(
506 syncable::WriteTransaction* trans, 519 syncable::WriteTransaction* trans,
507 syncable::MutableEntry* entry) { 520 syncable::MutableEntry* entry) {
508 CHECK(!entry->Get(IS_UNSYNCED)); 521 DCHECK(!entry->Get(IS_UNSYNCED));
509 CHECK(entry->Get(IS_UNAPPLIED_UPDATE)); 522 DCHECK(entry->Get(IS_UNAPPLIED_UPDATE));
510 523
511 LOG(INFO) << "Updating entry : " << *entry; 524 LOG(INFO) << "Updating entry : " << *entry;
512 // Start by setting the properties that determine the model_type. 525 // Start by setting the properties that determine the model_type.
513 entry->Put(SPECIFICS, entry->Get(SERVER_SPECIFICS)); 526 entry->Put(SPECIFICS, entry->Get(SERVER_SPECIFICS));
514 entry->Put(IS_DIR, entry->Get(SERVER_IS_DIR)); 527 entry->Put(IS_DIR, entry->Get(SERVER_IS_DIR));
515 // This strange dance around the IS_DEL flag avoids problems when setting 528 // This strange dance around the IS_DEL flag avoids problems when setting
516 // the name. 529 // the name.
517 // TODO(chron): Is this still an issue? Unit test this codepath. 530 // TODO(chron): Is this still an issue? Unit test this codepath.
518 if (entry->Get(SERVER_IS_DEL)) { 531 if (entry->Get(SERVER_IS_DEL)) {
519 entry->Put(IS_DEL, true); 532 entry->Put(IS_DEL, true);
(...skipping 196 matching lines...) Expand 10 before | Expand all | Expand 10 after
716 return VERIFY_FAIL; 729 return VERIFY_FAIL;
717 } 730 }
718 } 731 }
719 732
720 if (!deleted && 733 if (!deleted &&
721 (same_id->Get(SERVER_IS_DEL) || 734 (same_id->Get(SERVER_IS_DEL) ||
722 (!same_id->Get(IS_UNSYNCED) && same_id->Get(IS_DEL) && 735 (!same_id->Get(IS_UNSYNCED) && same_id->Get(IS_DEL) &&
723 same_id->Get(BASE_VERSION) > 0))) { 736 same_id->Get(BASE_VERSION) > 0))) {
724 // An undelete. The latter case in the above condition is for 737 // An undelete. The latter case in the above condition is for
725 // when the server does not give us an update following the 738 // when the server does not give us an update following the
726 // commit of a delete, before undeleting. Undeletion is possible 739 // commit of a delete, before undeleting.
727 // in the server's storage backend, so it's possible on the client, 740 // Undeletion is common for items that reuse the client-unique tag.
728 // though not expected to be something that is commonly possible.
729 VerifyResult result = 741 VerifyResult result =
730 SyncerUtil::VerifyUndelete(trans, entry, same_id); 742 SyncerUtil::VerifyUndelete(trans, entry, same_id);
731 if (VERIFY_UNDECIDED != result) 743 if (VERIFY_UNDECIDED != result)
732 return result; 744 return result;
733 } 745 }
734 } 746 }
735 if (same_id->Get(BASE_VERSION) > 0) { 747 if (same_id->Get(BASE_VERSION) > 0) {
736 // We've committed this entry in the past. 748 // We've committed this entry in the past.
737 if (is_directory != same_id->Get(IS_DIR) || 749 if (is_directory != same_id->Get(IS_DIR) ||
738 model_type != same_id->GetModelType()) { 750 model_type != same_id->GetModelType()) {
(...skipping 23 matching lines...) Expand all
762 } 774 }
763 return VERIFY_SUCCESS; 775 return VERIFY_SUCCESS;
764 } 776 }
765 777
766 // Assumes we have an existing entry; verify an update that seems to be 778 // Assumes we have an existing entry; verify an update that seems to be
767 // expressing an 'undelete' 779 // expressing an 'undelete'
768 // static 780 // static
769 VerifyResult SyncerUtil::VerifyUndelete(syncable::WriteTransaction* trans, 781 VerifyResult SyncerUtil::VerifyUndelete(syncable::WriteTransaction* trans,
770 const SyncEntity& entry, 782 const SyncEntity& entry,
771 syncable::MutableEntry* same_id) { 783 syncable::MutableEntry* same_id) {
784 // TODO(nick): We hit this path for items deleted items that the server
785 // tells us to re-create; only deleted items with positive base versions
786 // will hit this path. However, it's not clear how such an undeletion
787 // would actually succeed on the server; in the protocol, a base
788 // version of 0 is required to undelete an object. This codepath
789 // should be deprecated in favor of client-tag style undeletion
790 // (where items go to version 0 when they're deleted), or else
791 // removed entirely (if this type of undeletion is indeed impossible).
772 CHECK(same_id->good()); 792 CHECK(same_id->good());
773 LOG(INFO) << "Server update is attempting undelete. " << *same_id 793 LOG(INFO) << "Server update is attempting undelete. " << *same_id
774 << "Update:" << SyncerProtoUtil::SyncEntityDebugString(entry); 794 << "Update:" << SyncerProtoUtil::SyncEntityDebugString(entry);
775 // Move the old one aside and start over. It's too tricky to get the old one 795 // Move the old one aside and start over. It's too tricky to get the old one
776 // back into a state that would pass CheckTreeInvariants(). 796 // back into a state that would pass CheckTreeInvariants().
777 if (same_id->Get(IS_DEL)) { 797 if (same_id->Get(IS_DEL)) {
798 DCHECK(same_id->Get(UNIQUE_CLIENT_TAG).empty())
799 << "Doing move-aside undeletion on client-tagged item.";
778 same_id->Put(ID, trans->directory()->NextId()); 800 same_id->Put(ID, trans->directory()->NextId());
779 same_id->Put(UNIQUE_CLIENT_TAG, ""); 801 same_id->Put(UNIQUE_CLIENT_TAG, "");
780 same_id->Put(BASE_VERSION, CHANGES_VERSION); 802 same_id->Put(BASE_VERSION, CHANGES_VERSION);
781 same_id->Put(SERVER_VERSION, 0); 803 same_id->Put(SERVER_VERSION, 0);
782 return VERIFY_SUCCESS; 804 return VERIFY_SUCCESS;
783 } 805 }
784 if (entry.version() < same_id->Get(SERVER_VERSION)) { 806 if (entry.version() < same_id->Get(SERVER_VERSION)) {
785 LOG(WARNING) << "Update older than current server version for" << 807 LOG(WARNING) << "Update older than current server version for" <<
786 *same_id << "Update:" << SyncerProtoUtil::SyncEntityDebugString(entry); 808 *same_id << "Update:" << SyncerProtoUtil::SyncEntityDebugString(entry);
787 return VERIFY_SUCCESS; // Expected in new sync protocol. 809 return VERIFY_SUCCESS; // Expected in new sync protocol.
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
841 863
842 // |update_entry| is considered to be somewhere after |candidate|, so store 864 // |update_entry| is considered to be somewhere after |candidate|, so store
843 // it as the upper bound. 865 // it as the upper bound.
844 closest_sibling = candidate.Get(ID); 866 closest_sibling = candidate.Get(ID);
845 } 867 }
846 868
847 return closest_sibling; 869 return closest_sibling;
848 } 870 }
849 871
850 } // namespace browser_sync 872 } // namespace browser_sync
OLDNEW
« no previous file with comments | « chrome/browser/sync/engine/syncer_unittest.cc ('k') | chrome/browser/sync/engine/syncproto.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698