Merge lp:~thisfred/u1db/validate_transaction_id_of_target into lp:u1db
- validate_transaction_id_of_target
- Merge into trunk
Proposed by
Eric Casteleijn
Status: | Superseded |
---|---|
Proposed branch: | lp:~thisfred/u1db/validate_transaction_id_of_target |
Merge into: | lp:u1db |
Prerequisite: | lp:~thisfred/u1db/txid_in_sync_exchange |
Diff against target: |
879 lines (+491/-37) 14 files modified
include/u1db/u1db.h (+4/-0) include/u1db/u1db_internal.h (+31/-0) src/u1db.c (+152/-16) src/u1db_sync_target.c (+21/-13) u1db/backends/__init__.py (+44/-0) u1db/backends/inmemory.py (+12/-0) u1db/backends/sqlite_backend.py (+22/-0) u1db/errors.py (+8/-0) u1db/sync.py (+8/-5) u1db/tests/c_backend_wrapper.pyx (+50/-0) u1db/tests/test_backends.py (+108/-1) u1db/tests/test_c_backend.py (+8/-0) u1db/tests/test_sqlite_backend.py (+5/-2) u1db/tests/test_sync.py (+18/-0) |
To merge this branch: | bzr merge lp:~thisfred/u1db/validate_transaction_id_of_target |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu One hackers | Pending | ||
Review via email: mp+110626@code.launchpad.net |
This proposal supersedes a proposal from 2012-06-15.
This proposal has been superseded by a proposal from 2012-06-19.
Commit message
Added a validation function that checks that the generation + txid the sync target knows about us is correct.
Description of the change
Added a validation function that checks that the generation + txid the sync target knows about us is correct.
To post a comment you must log in.
- 338. By Eric Casteleijn
-
merged trunk resolved conflicts
- 339. By Eric Casteleijn
-
merged txid branch
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'include/u1db/u1db.h' |
2 | --- include/u1db/u1db.h 2012-06-18 18:24:19 +0000 |
3 | +++ include/u1db/u1db.h 2012-06-18 18:24:19 +0000 |
4 | @@ -78,6 +78,10 @@ |
5 | #define U1DB_CONVERGED 3 |
6 | #define U1DB_CONFLICTED 4 |
7 | |
8 | +// Used by validate_source_gen_and_trans_id |
9 | +#define U1DB_INVALID_TRANSACTION_ID -20 |
10 | +#define U1DB_INVALID_GENERATION -21 |
11 | + |
12 | /** |
13 | * The basic constructor for a new connection. |
14 | */ |
15 | |
16 | === modified file 'include/u1db/u1db_internal.h' |
17 | --- include/u1db/u1db_internal.h 2012-06-18 18:24:19 +0000 |
18 | +++ include/u1db/u1db_internal.h 2012-06-18 18:24:19 +0000 |
19 | @@ -22,6 +22,7 @@ |
20 | #include <stdarg.h> |
21 | #include "u1db/u1db.h" |
22 | #include "u1db/compat.h" |
23 | +#include "u1db/u1db_vectorclock.h" |
24 | |
25 | typedef struct sqlite3 sqlite3; |
26 | typedef struct sqlite3_stmt sqlite3_stmt; |
27 | @@ -208,11 +209,41 @@ |
28 | int *state, int *at_gen); |
29 | |
30 | /** |
31 | + * Validate source generation and transaction id. |
32 | + * |
33 | + * @param replica_uid uid of source replica at the time of the |
34 | + * change we are syncing. |
35 | + * @param replica_gen Generation of the replica at the time of the |
36 | + * change we are syncing. |
37 | + * @param replica_trans_id Transaction id of the replica at the time of the |
38 | + * change we are syncing. |
39 | + * @param cur_vcr Vector clock for the document in the database. |
40 | + * @param other_vcr Vector clock of the document being put. |
41 | + * @param state (OUT) 0 for success, U1DB_SUPERSEDED if the document is |
42 | + * superseded. |
43 | + */ |
44 | +int u1db__validate_source(u1database *db, const char *replica_uid, |
45 | + int replica_gen, const char *replica_trans_id, |
46 | + u1db_vectorclock *cur_vcr, |
47 | + u1db_vectorclock *other_vcr, int *state); |
48 | + |
49 | +/** |
50 | * Internal API, Get the global database rev. |
51 | */ |
52 | int u1db__get_generation(u1database *db, int *generation); |
53 | |
54 | /** |
55 | + * Internal API, Get the global database rev and transaction id. |
56 | + */ |
57 | +int u1db__get_generation_info(u1database *db, int *generation, |
58 | + char **trans_id); |
59 | + |
60 | +/** |
61 | + * Internal API, Validate generation and transaction id. |
62 | + */ |
63 | +int u1db_validate_gen_and_trans_id(u1database *db, int generation, |
64 | + const char *trans_id); |
65 | +/** |
66 | * Internal API, Allocate a new document id, for cases when callers do not |
67 | * supply their own. Callers of this API are expected to free the result. |
68 | */ |
69 | |
70 | === modified file 'src/u1db.c' |
71 | --- src/u1db.c 2012-06-12 12:35:05 +0000 |
72 | +++ src/u1db.c 2012-06-18 18:24:19 +0000 |
73 | @@ -722,6 +722,41 @@ |
74 | return status; |
75 | } |
76 | |
77 | +int |
78 | +u1db__validate_source(u1database *db, const char *replica_uid, int replica_gen, |
79 | + const char *replica_trans_id, u1db_vectorclock *cur, |
80 | + u1db_vectorclock *other, int *state) |
81 | +{ |
82 | + int old_generation; |
83 | + char *old_trans_id = NULL; |
84 | + int status = U1DB_OK; |
85 | + |
86 | + *state = U1DB_OK; |
87 | + status = u1db__get_sync_gen_info( |
88 | + db, replica_uid, &old_generation, &old_trans_id); |
89 | + if (status != U1DB_OK) |
90 | + goto finish; |
91 | + if (replica_gen < old_generation) { |
92 | + if (u1db__vectorclock_is_newer(cur, other)) { |
93 | + *state = U1DB_SUPERSEDED; |
94 | + goto finish; |
95 | + } |
96 | + status = U1DB_INVALID_GENERATION; |
97 | + goto finish; |
98 | + } |
99 | + if (replica_gen > old_generation) |
100 | + goto finish; |
101 | + if (strcmp(replica_trans_id, old_trans_id) != 0) { |
102 | + status = U1DB_INVALID_TRANSACTION_ID; |
103 | + goto finish; |
104 | + } |
105 | + *state = U1DB_SUPERSEDED; |
106 | +finish: |
107 | + if (old_trans_id != NULL) |
108 | + free(old_trans_id); |
109 | + return status; |
110 | +} |
111 | + |
112 | |
113 | int |
114 | u1db__put_doc_if_newer(u1database *db, u1db_document *doc, int save_conflict, |
115 | @@ -748,6 +783,31 @@ |
116 | status = lookup_doc(db, doc->doc_id, &stored_doc_rev, &stored_content, |
117 | &stored_content_len, &statement); |
118 | if (status != SQLITE_OK) { goto finish; } |
119 | + // TODO: u1db__vectorclock_from_str returns NULL if there is an error |
120 | + // in the vector clock, or if we run out of memory... Probably |
121 | + // shouldn't be U1DB_NOMEM |
122 | + stored_vc = u1db__vectorclock_from_str(stored_doc_rev); |
123 | + if (stored_vc == NULL) { |
124 | + status = U1DB_NOMEM; |
125 | + goto finish; |
126 | + } |
127 | + new_vc = u1db__vectorclock_from_str(doc->doc_rev); |
128 | + if (new_vc == NULL) { |
129 | + status = U1DB_NOMEM; |
130 | + goto finish; |
131 | + } |
132 | + if (replica_uid != NULL && replica_trans_id != NULL) { |
133 | + status = u1db__validate_source( |
134 | + db, replica_uid, replica_gen, replica_trans_id, stored_vc, new_vc, |
135 | + state); |
136 | + if (status != U1DB_OK) { |
137 | + goto finish; |
138 | + } |
139 | + if (*state != U1DB_OK) { |
140 | + status = u1db__get_generation(db, at_gen); |
141 | + goto finish; |
142 | + } |
143 | + } |
144 | if (stored_doc_rev == NULL) { |
145 | status = U1DB_OK; |
146 | *state = U1DB_INSERTED; |
147 | @@ -757,19 +817,6 @@ |
148 | *state = U1DB_CONVERGED; |
149 | store = 0; |
150 | } else { |
151 | - // TODO: u1db__vectorclock_from_str returns NULL if there is an error |
152 | - // in the vector clock, or if we run out of memory... Probably |
153 | - // shouldn't be U1DB_NOMEM |
154 | - stored_vc = u1db__vectorclock_from_str(stored_doc_rev); |
155 | - if (stored_vc == NULL) { |
156 | - status = U1DB_NOMEM; |
157 | - goto finish; |
158 | - } |
159 | - new_vc = u1db__vectorclock_from_str(doc->doc_rev); |
160 | - if (new_vc == NULL) { |
161 | - status = U1DB_NOMEM; |
162 | - goto finish; |
163 | - } |
164 | if (u1db__vectorclock_is_newer(new_vc, stored_vc)) { |
165 | // Just take the newer version |
166 | store = 1; |
167 | @@ -818,8 +865,8 @@ |
168 | (stored_doc_rev != NULL)); |
169 | } |
170 | if (status == U1DB_OK && replica_uid != NULL) { |
171 | - status = u1db__set_sync_info(db, replica_uid, replica_gen, |
172 | - replica_trans_id); |
173 | + status = u1db__set_sync_info( |
174 | + db, replica_uid, replica_gen, replica_trans_id); |
175 | } |
176 | if (status == U1DB_OK && at_gen != NULL) { |
177 | status = u1db__get_generation(db, at_gen); |
178 | @@ -1182,14 +1229,26 @@ |
179 | status = U1DB_OK; |
180 | *gen = 0; |
181 | *trans_id = strdup(""); |
182 | + if (*trans_id == NULL) { |
183 | + status = U1DB_NOMEM; |
184 | + goto finish; |
185 | + } |
186 | } else if (status == SQLITE_ROW) { |
187 | status = U1DB_OK; |
188 | *gen = sqlite3_column_int(statement, 0); |
189 | tmp = (const char *)sqlite3_column_text(statement, 1); |
190 | if (tmp == NULL) { |
191 | *trans_id = strdup(""); |
192 | + if (*trans_id == NULL) { |
193 | + status = U1DB_NOMEM; |
194 | + goto finish; |
195 | + } |
196 | } else { |
197 | *trans_id = strdup(tmp); |
198 | + if (*trans_id == NULL) { |
199 | + status = U1DB_NOMEM; |
200 | + goto finish; |
201 | + } |
202 | } |
203 | } |
204 | finish: |
205 | @@ -1329,6 +1388,80 @@ |
206 | return status; |
207 | } |
208 | |
209 | +int |
210 | +u1db__get_generation_info(u1database *db, int *generation, char **trans_id) |
211 | +{ |
212 | + int status; |
213 | + const char *tmp; |
214 | + |
215 | + sqlite3_stmt *statement; |
216 | + if (db == NULL || generation == NULL) { |
217 | + return U1DB_INVALID_PARAMETER; |
218 | + } |
219 | + status = sqlite3_prepare_v2(db->sql_handle, |
220 | + "SELECT max(generation), transaction_id FROM transaction_log", -1, |
221 | + &statement, NULL); |
222 | + if (status != SQLITE_OK) { |
223 | + return status; |
224 | + } |
225 | + status = sqlite3_step(statement); |
226 | + if (status == SQLITE_DONE) { |
227 | + // No records, we are at rev 0 |
228 | + status = SQLITE_OK; |
229 | + *generation = 0; |
230 | + } else if (status == SQLITE_ROW) { |
231 | + status = SQLITE_OK; |
232 | + *generation = sqlite3_column_int(statement, 0); |
233 | + tmp = (const char *)sqlite3_column_text(statement, 1); |
234 | + if (tmp == NULL) { |
235 | + *trans_id = NULL; |
236 | + } else { |
237 | + *trans_id = strdup(tmp); |
238 | + if (*trans_id == NULL) { |
239 | + status = U1DB_NOMEM; |
240 | + } |
241 | + } |
242 | + } |
243 | + sqlite3_finalize(statement); |
244 | + return status; |
245 | +} |
246 | + |
247 | +int |
248 | +u1db_validate_gen_and_trans_id(u1database *db, int generation, |
249 | + const char *trans_id) |
250 | +{ |
251 | + int status = U1DB_OK; |
252 | + sqlite3_stmt *statement; |
253 | + |
254 | + if (generation == 0) |
255 | + return status; |
256 | + if (db == NULL) { |
257 | + return U1DB_INVALID_PARAMETER; |
258 | + } |
259 | + status = sqlite3_prepare_v2(db->sql_handle, |
260 | + "SELECT transaction_id FROM transaction_log WHERE generation = ?", -1, |
261 | + &statement, NULL); |
262 | + if (status != SQLITE_OK) { goto finish; } |
263 | + status = sqlite3_bind_int(statement, 1, generation); |
264 | + if (status != SQLITE_OK) { goto finish; } |
265 | + status = sqlite3_step(statement); |
266 | + if (status == SQLITE_DONE) { |
267 | + status = U1DB_INVALID_GENERATION; |
268 | + goto finish; |
269 | + } else if (status == SQLITE_ROW) { |
270 | + // Note: We may want to handle the column containing NULL |
271 | + if (strcmp(trans_id, |
272 | + (const char *)sqlite3_column_text(statement, 0)) == 0) { |
273 | + status = U1DB_OK; |
274 | + goto finish; |
275 | + } |
276 | + status = U1DB_INVALID_TRANSACTION_ID; |
277 | + } |
278 | +finish: |
279 | + sqlite3_finalize(statement); |
280 | + return status; |
281 | +} |
282 | + |
283 | char * |
284 | u1db__allocate_doc_id(u1database *db) |
285 | { |
286 | @@ -1462,7 +1595,10 @@ |
287 | // Note: We may want to handle the column containing NULL |
288 | tmp = (const char *)sqlite3_column_text(statement, 1); |
289 | if (tmp == NULL) { |
290 | - *trans_id = NULL; |
291 | + *trans_id = strdup(""); |
292 | + if (*trans_id == NULL) { |
293 | + status = U1DB_NOMEM; |
294 | + } |
295 | } else { |
296 | *trans_id = strdup(tmp); |
297 | if (*trans_id == NULL) { |
298 | |
299 | === modified file 'src/u1db_sync_target.c' |
300 | --- src/u1db_sync_target.c 2012-06-18 18:24:19 +0000 |
301 | +++ src/u1db_sync_target.c 2012-06-18 18:24:19 +0000 |
302 | @@ -24,7 +24,7 @@ |
303 | static int st_get_sync_info(u1db_sync_target *st, |
304 | const char *source_replica_uid, |
305 | const char **st_replica_uid, int *st_gen, int *source_gen, |
306 | - char **trans_id); |
307 | + char **source_trans_id); |
308 | |
309 | static int st_record_sync_info(u1db_sync_target *st, |
310 | const char *source_replica_uid, int source_gen, const char *trans_id); |
311 | @@ -106,7 +106,7 @@ |
312 | static int |
313 | st_get_sync_info(u1db_sync_target *st, const char *source_replica_uid, |
314 | const char **st_replica_uid, int *st_gen, int *source_gen, |
315 | - char **trans_id) |
316 | + char **source_trans_id) |
317 | { |
318 | int status = U1DB_OK; |
319 | u1database *db; |
320 | @@ -125,8 +125,8 @@ |
321 | db = (u1database *)st->implementation; |
322 | status = u1db_get_replica_uid(db, st_replica_uid); |
323 | if (status != U1DB_OK) { goto finish; } |
324 | - status = u1db__get_sync_gen_info(db, source_replica_uid, source_gen, |
325 | - trans_id); |
326 | + status = u1db__get_sync_gen_info( |
327 | + db, source_replica_uid, source_gen, source_trans_id); |
328 | if (status != U1DB_OK) { goto finish; } |
329 | status = u1db__get_generation(db, st_gen); |
330 | finish: |
331 | @@ -614,6 +614,9 @@ |
332 | status = target->get_sync_info(target, local_uid, &target_uid, &target_gen, |
333 | &local_gen_known_by_target, &local_trans_id_known_by_target); |
334 | if (status != U1DB_OK) { goto finish; } |
335 | + status = u1db_validate_gen_and_trans_id( |
336 | + db, local_gen_known_by_target, local_trans_id_known_by_target); |
337 | + if (status != U1DB_OK) { goto finish; } |
338 | status = u1db__get_sync_gen_info(db, target_uid, |
339 | &target_gen_known_by_local, &target_trans_id_known_by_local); |
340 | if (status != U1DB_OK) { goto finish; } |
341 | @@ -623,8 +626,9 @@ |
342 | // Before we start the sync exchange, get the list of doc_ids that we want |
343 | // to send. We have to do this first, so that local_gen_before_sync will |
344 | // match exactly the list of doc_ids we send |
345 | - status = u1db_whats_changed(db, &local_gen, &local_trans_id, |
346 | - (void*)&to_send_state, whats_changed_to_doc_ids); |
347 | + status = u1db_whats_changed( |
348 | + db, &local_gen, &local_trans_id, (void*)&to_send_state, |
349 | + whats_changed_to_doc_ids); |
350 | if (status != U1DB_OK) { goto finish; } |
351 | if (local_gen == local_gen_known_by_target |
352 | && target_gen == target_gen_known_by_local) |
353 | @@ -644,19 +648,23 @@ |
354 | &target_gen_known_by_local, &target_trans_id_known_by_local, |
355 | &return_doc_state, return_doc_to_insert_from_target); |
356 | if (status != U1DB_OK) { goto finish; } |
357 | - status = u1db__get_generation(db, &local_gen); |
358 | + if (local_trans_id != NULL) { |
359 | + free(local_trans_id); |
360 | + } |
361 | + status = u1db__get_generation_info(db, &local_gen, &local_trans_id); |
362 | if (status != U1DB_OK) { goto finish; } |
363 | // Now we successfully sent and received docs, make sure we record the |
364 | // current remote generation |
365 | - status = u1db__set_sync_info(db, target_uid, target_gen_known_by_local, |
366 | - "T-sid"); |
367 | + status = u1db__set_sync_info( |
368 | + db, target_uid, target_gen_known_by_local, |
369 | + target_trans_id_known_by_local); |
370 | if (status != U1DB_OK) { goto finish; } |
371 | if (return_doc_state.num_inserted > 0 && |
372 | - ((*local_gen_before_sync + return_doc_state.num_inserted) |
373 | - == local_gen)) |
374 | + ((*local_gen_before_sync + return_doc_state.num_inserted) |
375 | + == local_gen)) |
376 | { |
377 | - status = target->record_sync_info(target, local_uid, local_gen, |
378 | - "T-sid"); |
379 | + status = target->record_sync_info( |
380 | + target, local_uid, local_gen, local_trans_id); |
381 | if (status != U1DB_OK) { goto finish; } |
382 | } |
383 | finish: |
384 | |
385 | === modified file 'u1db/backends/__init__.py' |
386 | --- u1db/backends/__init__.py 2012-05-30 14:39:36 +0000 |
387 | +++ u1db/backends/__init__.py 2012-06-18 18:24:19 +0000 |
388 | @@ -53,8 +53,17 @@ |
389 | raise errors.InvalidDocId() |
390 | |
391 | def _get_generation(self): |
392 | + """Return the current generation. |
393 | + |
394 | + """ |
395 | raise NotImplementedError(self._get_generation) |
396 | |
397 | + def _get_generation_info(self): |
398 | + """Return the current generation and transaction id. |
399 | + |
400 | + """ |
401 | + raise NotImplementedError(self._get_generation_info) |
402 | + |
403 | def _get_doc(self, doc_id): |
404 | """Extract the document from storage. |
405 | |
406 | @@ -93,6 +102,35 @@ |
407 | result.append(doc) |
408 | return result |
409 | |
410 | + def validate_gen_and_trans_id(self, generation, trans_id): |
411 | + """Validate the generation and transaction id. |
412 | + |
413 | + Raises an InvalidGeneration when the generation does not exist, and an |
414 | + InvalidTransactionId when it does but with a different transaction id. |
415 | + |
416 | + """ |
417 | + raise NotImplementedError(self.validate_gen_and_trans_id) |
418 | + |
419 | + def _validate_source(self, other_replica_uid, other_generation, |
420 | + other_transaction_id, cur_vcr, other_vcr): |
421 | + """Validate the new generation and transaction id. |
422 | + |
423 | + other_generation must be greater than what we have stored for this |
424 | + replica, *or* it must be the same and the transaction_id must be the |
425 | + same as well. |
426 | + """ |
427 | + old_generation, old_transaction_id = self._get_sync_gen_info( |
428 | + other_replica_uid) |
429 | + if other_generation < old_generation: |
430 | + if cur_vcr.is_newer(other_vcr): |
431 | + return 'superseded' |
432 | + raise errors.InvalidGeneration |
433 | + if other_generation > old_generation: |
434 | + return 'ok' |
435 | + if other_transaction_id == old_transaction_id: |
436 | + return 'superseded' |
437 | + raise errors.InvalidTransactionId |
438 | + |
439 | def _put_doc_if_newer(self, doc, save_conflict, replica_uid=None, |
440 | replica_gen=None, replica_trans_id=None): |
441 | cur_doc = self._get_doc(doc.doc_id) |
442 | @@ -101,6 +139,12 @@ |
443 | cur_vcr = VectorClockRev(None) |
444 | else: |
445 | cur_vcr = VectorClockRev(cur_doc.rev) |
446 | + if replica_uid is not None and replica_gen is not None: |
447 | + state = self._validate_source( |
448 | + replica_uid, replica_gen, replica_trans_id, cur_vcr, |
449 | + doc_vcr) |
450 | + if state != 'ok': |
451 | + return state, self._get_generation() |
452 | if doc_vcr.is_newer(cur_vcr): |
453 | self._put_and_update_indexes(cur_doc, doc) |
454 | self._prune_conflicts(doc, doc_vcr) |
455 | |
456 | === modified file 'u1db/backends/inmemory.py' |
457 | --- u1db/backends/inmemory.py 2012-06-12 15:44:43 +0000 |
458 | +++ u1db/backends/inmemory.py 2012-06-18 18:24:19 +0000 |
459 | @@ -79,6 +79,18 @@ |
460 | def _get_generation(self): |
461 | return len(self._transaction_log) |
462 | |
463 | + def _get_generation_info(self): |
464 | + return len(self._transaction_log), self._transaction_log[-1][1] |
465 | + |
466 | + def validate_gen_and_trans_id(self, generation, trans_id): |
467 | + if generation == 0: |
468 | + return |
469 | + if generation > len(self._transaction_log): |
470 | + raise errors.InvalidGeneration |
471 | + if self._transaction_log[generation - 1][1] == trans_id: |
472 | + return |
473 | + raise errors.InvalidTransactionId |
474 | + |
475 | def put_doc(self, doc): |
476 | if doc.doc_id is None: |
477 | raise errors.InvalidDocId() |
478 | |
479 | === modified file 'u1db/backends/sqlite_backend.py' |
480 | --- u1db/backends/sqlite_backend.py 2012-06-12 15:44:43 +0000 |
481 | +++ u1db/backends/sqlite_backend.py 2012-06-18 18:24:19 +0000 |
482 | @@ -265,6 +265,28 @@ |
483 | return 0 |
484 | return val |
485 | |
486 | + def _get_generation_info(self): |
487 | + c = self._db_handle.cursor() |
488 | + c.execute( |
489 | + 'SELECT max(generation), transaction_id FROM transaction_log ') |
490 | + val = c.fetchone() |
491 | + if val[0] is None: |
492 | + return(0, val[1]) |
493 | + return val |
494 | + |
495 | + def validate_gen_and_trans_id(self, generation, trans_id): |
496 | + if generation == 0: |
497 | + return |
498 | + c = self._db_handle.cursor() |
499 | + c.execute( |
500 | + 'SELECT transaction_id FROM transaction_log WHERE generation = ?', |
501 | + (generation,)) |
502 | + val = c.fetchone() |
503 | + if val is None: |
504 | + raise errors.InvalidGeneration |
505 | + if val[0] != trans_id: |
506 | + raise errors.InvalidTransactionId |
507 | + |
508 | def _get_transaction_log(self): |
509 | c = self._db_handle.cursor() |
510 | c.execute("SELECT doc_id, transaction_id FROM transaction_log" |
511 | |
512 | === modified file 'u1db/errors.py' |
513 | --- u1db/errors.py 2012-05-25 18:14:06 +0000 |
514 | +++ u1db/errors.py 2012-06-18 18:24:19 +0000 |
515 | @@ -39,6 +39,14 @@ |
516 | wire_description = "invalid document id" |
517 | |
518 | |
519 | +class InvalidTransactionId(U1DBError): |
520 | + """Invalid transaction for generation.""" |
521 | + |
522 | + |
523 | +class InvalidGeneration(U1DBError): |
524 | + """Generation was previously synced with a different transaction id.""" |
525 | + |
526 | + |
527 | class ConflictedDoc(U1DBError): |
528 | """The document is conflicted, you must call resolve before put()""" |
529 | |
530 | |
531 | === modified file 'u1db/sync.py' |
532 | --- u1db/sync.py 2012-06-18 18:24:19 +0000 |
533 | +++ u1db/sync.py 2012-06-18 18:24:19 +0000 |
534 | @@ -83,11 +83,11 @@ |
535 | record with the target that they are fully up to date with our |
536 | new generation. |
537 | """ |
538 | - cur_gen = self.source._get_generation() |
539 | + cur_gen, trans_id = self.source._get_generation_info() |
540 | if (cur_gen == start_generation + self.num_inserted |
541 | - and self.num_inserted > 0): |
542 | - self.sync_target.record_sync_info(self.source._replica_uid, |
543 | - cur_gen, 'T-sid') |
544 | + and self.num_inserted > 0): |
545 | + self.sync_target.record_sync_info( |
546 | + self.source._replica_uid, cur_gen, trans_id) |
547 | |
548 | def sync(self, callback=None): |
549 | """Synchronize documents between source and target.""" |
550 | @@ -98,6 +98,8 @@ |
551 | target_my_trans_id) = sync_target.get_sync_info( |
552 | self.source._replica_uid) |
553 | # what's changed since that generation and this current gen |
554 | + self.source.validate_gen_and_trans_id( |
555 | + target_my_gen, target_my_trans_id) |
556 | my_gen, _, changes = self.source.whats_changed(target_my_gen) |
557 | |
558 | # this source last-seen database generation for the target |
559 | @@ -121,7 +123,8 @@ |
560 | self.source._replica_uid, target_last_known_gen, |
561 | return_doc_cb=self._insert_doc_from_target) |
562 | # record target synced-up-to generation including applying what we sent |
563 | - self.source._set_sync_info(self.target_replica_uid, new_gen, 'T-sid') |
564 | + self.source._set_sync_info( |
565 | + self.target_replica_uid, new_gen, new_trans_id) |
566 | |
567 | # if gapless record current reached generation with target |
568 | self._record_sync_info_with_the_target(my_gen) |
569 | |
570 | === modified file 'u1db/tests/c_backend_wrapper.pyx' |
571 | --- u1db/tests/c_backend_wrapper.pyx 2012-06-18 18:24:19 +0000 |
572 | +++ u1db/tests/c_backend_wrapper.pyx 2012-06-18 18:24:19 +0000 |
573 | @@ -80,6 +80,10 @@ |
574 | int u1db_get_all_docs(u1database *db, int include_deleted, int *generation, |
575 | void *context, u1db_doc_callback cb) |
576 | int u1db_put_doc(u1database *db, u1db_document *doc) |
577 | + int u1db__validate_source(u1database *db, const_char_ptr replica_uid, |
578 | + int replica_gen, const_char_ptr replica_trans_id, |
579 | + u1db_vectorclock *cur_vcr, |
580 | + u1db_vectorclock *other_vcr, int *state) |
581 | int u1db__put_doc_if_newer(u1database *db, u1db_document *doc, |
582 | int save_conflict, char *replica_uid, |
583 | int replica_gen, char *replica_trans_id, |
584 | @@ -131,6 +135,8 @@ |
585 | int U1DB_BROKEN_SYNC_STREAM |
586 | int U1DB_DUPLICATE_INDEX_NAME |
587 | int U1DB_INDEX_DOES_NOT_EXIST |
588 | + int U1DB_INVALID_GENERATION |
589 | + int U1DB_INVALID_TRANSACTION_ID |
590 | int U1DB_INTERNAL_ERROR |
591 | |
592 | int U1DB_INSERTED |
593 | @@ -198,6 +204,9 @@ |
594 | |
595 | |
596 | int u1db__get_generation(u1database *, int *db_rev) |
597 | + int u1db__get_generation_info(u1database *, int *db_rev, char **trans_id) |
598 | + int u1db_validate_gen_and_trans_id(u1database *, int db_rev, |
599 | + const_char_ptr trans_id) |
600 | char *u1db__allocate_doc_id(u1database *) |
601 | int u1db__sql_close(u1database *) |
602 | int u1db__sql_is_open(u1database *) |
603 | @@ -580,6 +589,10 @@ |
604 | raise errors.IndexNameTakenError() |
605 | if status == U1DB_INDEX_DOES_NOT_EXIST: |
606 | raise errors.IndexDoesNotExist |
607 | + if status == U1DB_INVALID_GENERATION: |
608 | + raise errors.InvalidGeneration |
609 | + if status == U1DB_INVALID_TRANSACTION_ID: |
610 | + raise errors.InvalidTransactionId |
611 | raise RuntimeError('%s (status: %s)' % (context, status)) |
612 | |
613 | |
614 | @@ -912,6 +925,27 @@ |
615 | u1db_put_doc(self._db, doc._doc)) |
616 | return doc.rev |
617 | |
618 | + def _validate_source(self, replica_uid, replica_gen, replica_trans_id, |
619 | + cur_vcr, other_vcr): |
620 | + cdef const_char_ptr c_uid, c_trans_id |
621 | + cdef int c_gen, state = 0 |
622 | + cdef VectorClockRev cur |
623 | + cdef VectorClockRev other |
624 | + |
625 | + cur = VectorClockRev(cur_vcr.as_str()) |
626 | + other = VectorClockRev(other_vcr.as_str()) |
627 | + c_uid = replica_uid |
628 | + c_trans_id = replica_trans_id |
629 | + c_gen = replica_gen |
630 | + handle_status( |
631 | + "invalid generation or transaction id", |
632 | + u1db__validate_source( |
633 | + self._db, c_uid, c_gen, c_trans_id, cur._clock, other._clock, |
634 | + &state)) |
635 | + if state == U1DB_SUPERSEDED: |
636 | + return 'superseded' |
637 | + return 'ok' |
638 | + |
639 | def _put_doc_if_newer(self, CDocument doc, save_conflict, replica_uid=None, |
640 | replica_gen=None, replica_trans_id=None): |
641 | cdef char *c_uid, *c_trans_id |
642 | @@ -1033,6 +1067,22 @@ |
643 | u1db__get_generation(self._db, &generation)) |
644 | return generation |
645 | |
646 | + def _get_generation_info(self): |
647 | + cdef int generation |
648 | + cdef char *trans_id |
649 | + handle_status("get_generation_info", |
650 | + u1db__get_generation_info(self._db, &generation, &trans_id)) |
651 | + raw_trans_id = None |
652 | + if trans_id != NULL: |
653 | + raw_trans_id = trans_id |
654 | + free(trans_id) |
655 | + return generation, raw_trans_id |
656 | + |
657 | + def validate_gen_and_trans_id(self, generation, trans_id): |
658 | + handle_status( |
659 | + "validate_gen_and_trans_id", |
660 | + u1db_validate_gen_and_trans_id(self._db, generation, trans_id)) |
661 | + |
662 | def _get_sync_gen_info(self, replica_uid): |
663 | cdef int generation, status |
664 | cdef char *trans_id = NULL |
665 | |
666 | === modified file 'u1db/tests/test_backends.py' |
667 | --- u1db/tests/test_backends.py 2012-06-18 18:24:19 +0000 |
668 | +++ u1db/tests/test_backends.py 2012-06-18 18:24:19 +0000 |
669 | @@ -392,6 +392,112 @@ |
670 | # The database wasn't altered |
671 | self.assertGetDoc(self.db, doc1.doc_id, doc1.rev, simple_doc, False) |
672 | |
673 | + def test_put_doc_if_newer_newer_generation(self): |
674 | + self.db._set_sync_info('other', 1, 'T-sid') |
675 | + doc = self.make_document('doc_id', 'other:2', simple_doc) |
676 | + state, _ = self.db._put_doc_if_newer( |
677 | + doc, save_conflict=False, replica_uid='other', replica_gen=2, |
678 | + replica_trans_id='T-irrelevant') |
679 | + self.assertEqual('inserted', state) |
680 | + |
681 | + def test_put_doc_if_newer_same_generation_same_txid(self): |
682 | + self.db._set_sync_info('other', 1, 'T-sid') |
683 | + doc = self.make_document('doc_id', 'other:2', simple_doc) |
684 | + state, _ = self.db._put_doc_if_newer( |
685 | + doc, save_conflict=False, replica_uid='other', replica_gen=1, |
686 | + replica_trans_id='T-sid') |
687 | + self.assertEqual('superseded', state) |
688 | + |
689 | + def test_put_doc_if_newer_wrong_transaction_id(self): |
690 | + self.db._set_sync_info('other', 1, 'T-sid') |
691 | + doc = self.make_document('doc_id', 'other:1', simple_doc) |
692 | + self.assertRaises( |
693 | + errors.InvalidTransactionId, |
694 | + self.db._put_doc_if_newer, doc, save_conflict=False, |
695 | + replica_uid='other', replica_gen=1, replica_trans_id='T-sad') |
696 | + |
697 | + def test_put_doc_if_newer_old_generation_older_doc(self): |
698 | + orig_doc = '{"new": "doc"}' |
699 | + doc = self.db.create_doc(orig_doc) |
700 | + doc_rev1 = doc.rev |
701 | + doc.set_json(simple_doc) |
702 | + self.db.put_doc(doc) |
703 | + self.db._set_sync_info('other', 5, 'T-sid') |
704 | + older_doc = self.make_document(doc.doc_id, doc_rev1, simple_doc) |
705 | + state, _ = self.db._put_doc_if_newer( |
706 | + older_doc, save_conflict=False, replica_uid='other', replica_gen=3, |
707 | + replica_trans_id='T-irrelevant') |
708 | + self.assertEqual('superseded', state) |
709 | + |
710 | + def test_put_doc_if_newer_old_generation_newer_doc(self): |
711 | + self.db._set_sync_info('other', 5, 'T-sid') |
712 | + doc = self.make_document('doc_id', 'other:1', simple_doc) |
713 | + self.assertRaises( |
714 | + errors.InvalidGeneration, |
715 | + self.db._put_doc_if_newer, doc, save_conflict=False, |
716 | + replica_uid='other', replica_gen=1, replica_trans_id='T-sad') |
717 | + |
718 | + def test_validate_gen_and_trans_id(self): |
719 | + self.db.create_doc(simple_doc) |
720 | + gen, trans_id = self.db._get_generation_info() |
721 | + self.db.validate_gen_and_trans_id(gen, trans_id) |
722 | + |
723 | + def test_validate_gen_and_trans_id_invalid_txid(self): |
724 | + self.db.create_doc(simple_doc) |
725 | + gen, _ = self.db._get_generation_info() |
726 | + self.assertRaises( |
727 | + errors.InvalidTransactionId, |
728 | + self.db.validate_gen_and_trans_id, gen, 'wrong') |
729 | + |
730 | + def test_validate_gen_and_trans_id_invalid_txid(self): |
731 | + self.db.create_doc(simple_doc) |
732 | + gen, trans_id = self.db._get_generation_info() |
733 | + self.assertRaises( |
734 | + errors.InvalidGeneration, |
735 | + self.db.validate_gen_and_trans_id, gen + 1, trans_id) |
736 | + |
737 | + def test_validate_source_gen_and_trans_id_same(self): |
738 | + self.db._set_sync_info('other', 1, 'T-sid') |
739 | + v1 = vectorclock.VectorClockRev('other:1|self:1') |
740 | + v2 = vectorclock.VectorClockRev('other:2|self:2') |
741 | + self.assertEqual( |
742 | + 'superseded', |
743 | + self.db._validate_source('other', 1, 'T-sid', v1, v2)) |
744 | + |
745 | + def test_validate_source_gen_newer(self): |
746 | + self.db._set_sync_info('other', 1, 'T-sid') |
747 | + v1 = vectorclock.VectorClockRev('other:1|self:1') |
748 | + v2 = vectorclock.VectorClockRev('other:2|self:2') |
749 | + self.assertEqual( |
750 | + 'ok', |
751 | + self.db._validate_source('other', 2, 'T-whatevs', v1, v2)) |
752 | + |
753 | + def test_validate_source_wrong_txid(self): |
754 | + self.db._set_sync_info('other', 1, 'T-sid') |
755 | + v1 = vectorclock.VectorClockRev('other:1|self:1') |
756 | + v2 = vectorclock.VectorClockRev('other:2|self:2') |
757 | + self.assertRaises( |
758 | + errors.InvalidTransactionId, |
759 | + self.db._validate_source, 'other', 1, 'T-sad', v1, v2) |
760 | + |
761 | + def test_validate_source_gen_older_and_vcr_older(self): |
762 | + self.db._set_sync_info('other', 1, 'T-sid') |
763 | + self.db._set_sync_info('other', 2, 'T-sod') |
764 | + v1 = vectorclock.VectorClockRev('other:1|self:1') |
765 | + v2 = vectorclock.VectorClockRev('other:2|self:2') |
766 | + self.assertEqual( |
767 | + 'superseded', |
768 | + self.db._validate_source('other', 1, 'T-sid', v2, v1)) |
769 | + |
770 | + def test_validate_source_gen_older_vcr_newer(self): |
771 | + self.db._set_sync_info('other', 1, 'T-sid') |
772 | + self.db._set_sync_info('other', 2, 'T-sod') |
773 | + v1 = vectorclock.VectorClockRev('other:1|self:1') |
774 | + v2 = vectorclock.VectorClockRev('other:2|self:2') |
775 | + self.assertRaises( |
776 | + errors.InvalidGeneration, |
777 | + self.db._validate_source, 'other', 1, 'T-sid', v1, v2) |
778 | + |
779 | def test_put_doc_if_newer_replica_uid(self): |
780 | doc1 = self.db.create_doc(simple_doc) |
781 | self.db._set_sync_info('other', 1, 'T-sid') |
782 | @@ -636,7 +742,8 @@ |
783 | doc2 = self.make_document(doc1.doc_id, 'alternate:1', nested_doc) |
784 | self.db._put_doc_if_newer(doc2, save_conflict=True) |
785 | self.assertTrue(doc2.has_conflicts) |
786 | - self.assertGetDoc(self.db, doc1.doc_id, 'alternate:1', nested_doc, True) |
787 | + self.assertGetDoc( |
788 | + self.db, doc1.doc_id, 'alternate:1', nested_doc, True) |
789 | self.assertGetDocConflicts(self.db, doc1.doc_id, |
790 | [('alternate:1', nested_doc), (doc1.rev, None)]) |
791 | |
792 | |
793 | === modified file 'u1db/tests/test_c_backend.py' |
794 | --- u1db/tests/test_c_backend.py 2012-06-18 18:24:19 +0000 |
795 | +++ u1db/tests/test_c_backend.py 2012-06-18 18:24:19 +0000 |
796 | @@ -73,6 +73,14 @@ |
797 | db.create_doc(tests.simple_doc) |
798 | self.assertEqual(1, db._get_generation()) |
799 | |
800 | + def test__get_generation_info(self): |
801 | + db = c_backend_wrapper.CDatabase(':memory:') |
802 | + self.assertEqual((0, None), db._get_generation_info()) |
803 | + db.create_doc(tests.simple_doc) |
804 | + info = db._get_generation_info() |
805 | + self.assertEqual(1, info[0]) |
806 | + self.assertTrue(info[1].startswith('T-')) |
807 | + |
808 | def test__set_replica_uid(self): |
809 | db = c_backend_wrapper.CDatabase(':memory:') |
810 | self.assertIsNot(None, db._replica_uid) |
811 | |
812 | === modified file 'u1db/tests/test_sqlite_backend.py' |
813 | --- u1db/tests/test_sqlite_backend.py 2012-05-31 16:31:31 +0000 |
814 | +++ u1db/tests/test_sqlite_backend.py 2012-06-18 18:24:19 +0000 |
815 | @@ -152,6 +152,9 @@ |
816 | def test__get_generation(self): |
817 | self.assertEqual(0, self.db._get_generation()) |
818 | |
819 | + def test__get_generation_info(self): |
820 | + self.assertEqual((0, None), self.db._get_generation_info()) |
821 | + |
822 | def test_create_index(self): |
823 | self.db.create_index('test-idx', "key") |
824 | self.assertEqual([('test-idx', ["key"])], self.db.list_indexes()) |
825 | @@ -261,7 +264,7 @@ |
826 | def test__open_database_with_factory(self): |
827 | temp_dir = self.createTempDir(prefix='u1db-test-') |
828 | path = temp_dir + '/test.sqlite' |
829 | - db = sqlite_backend.SQLitePartialExpandDatabase(path) |
830 | + sqlite_backend.SQLitePartialExpandDatabase(path) |
831 | db2 = sqlite_backend.SQLiteDatabase._open_database( |
832 | path, document_factory=TestAlternativeDocument) |
833 | self.assertEqual(TestAlternativeDocument, db2._factory) |
834 | @@ -322,7 +325,7 @@ |
835 | def test_open_database_with_factory(self): |
836 | temp_dir = self.createTempDir(prefix='u1db-test-') |
837 | path = temp_dir + '/existing.sqlite' |
838 | - db = sqlite_backend.SQLitePartialExpandDatabase(path) |
839 | + sqlite_backend.SQLitePartialExpandDatabase(path) |
840 | db2 = sqlite_backend.SQLiteDatabase.open_database( |
841 | path, create=False, document_factory=TestAlternativeDocument) |
842 | self.assertEqual(TestAlternativeDocument, db2._factory) |
843 | |
844 | === modified file 'u1db/tests/test_sync.py' |
845 | --- u1db/tests/test_sync.py 2012-06-18 18:24:19 +0000 |
846 | +++ u1db/tests/test_sync.py 2012-06-18 18:24:19 +0000 |
847 | @@ -662,6 +662,12 @@ |
848 | doc1 = self.db1.create_doc('{"a": 1}', doc_id='the-doc') |
849 | db3 = self.create_database('test3') |
850 | self.sync(self.db2, self.db1) |
851 | + self.assertEqual( |
852 | + self.db1._get_generation_info(), |
853 | + self.db2._get_sync_gen_info(self.db1._replica_uid)) |
854 | + self.assertEqual( |
855 | + self.db2._get_generation_info(), |
856 | + self.db1._get_sync_gen_info(self.db2._replica_uid)) |
857 | self.sync(db3, self.db1) |
858 | # update on 2 |
859 | doc2 = self.make_document('the-doc', doc1.rev, '{"a": 2}') |
860 | @@ -695,7 +701,19 @@ |
861 | self.db2.create_doc('{"b": 1}', doc_id='the-doc') |
862 | db3.create_doc('{"c": 1}', doc_id='the-doc') |
863 | self.sync(db3, self.db1) |
864 | + self.assertEqual( |
865 | + self.db1._get_generation_info(), |
866 | + db3._get_sync_gen_info(self.db1._replica_uid)) |
867 | + self.assertEqual( |
868 | + db3._get_generation_info(), |
869 | + self.db1._get_sync_gen_info(db3._replica_uid)) |
870 | self.sync(db3, self.db2) |
871 | + self.assertEqual( |
872 | + self.db2._get_generation_info(), |
873 | + db3._get_sync_gen_info(self.db2._replica_uid)) |
874 | + self.assertEqual( |
875 | + db3._get_generation_info(), |
876 | + self.db2._get_sync_gen_info(db3._replica_uid)) |
877 | self.assertEqual(3, len(db3.get_doc_conflicts('the-doc'))) |
878 | doc1.set_json('{"a": 2}') |
879 | self.db1.put_doc(doc1) |