Merge lp:~thisfred/u1db/validate_transaction_id_of_target into lp:u1db

Proposed by Eric Casteleijn
Status: Merged
Approved by: Eric Casteleijn
Approved revision: 339
Merged at revision: 332
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: 538 lines (+253/-24)
12 files modified
include/u1db/u1db_internal.h (+11/-0)
src/u1db.c (+92/-3)
src/u1db_sync_target.c (+21/-13)
u1db/backends/__init__.py (+18/-1)
u1db/backends/inmemory.py (+12/-0)
u1db/backends/sqlite_backend.py (+22/-0)
u1db/sync.py (+8/-5)
u1db/tests/c_backend_wrapper.pyx (+19/-0)
u1db/tests/test_backends.py (+19/-0)
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
Reviewer Review Type Date Requested Status
Samuele Pedroni Approve
Review via email: mp+111091@code.launchpad.net

This proposal supersedes a proposal from 2012-06-15.

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

Revision history for this message
Samuele Pedroni (pedronis) wrote :

looks good, as we discussed we need integration tests showing that the validate cases trigger in appropriate sync situations

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/u1db/u1db_internal.h'
2--- include/u1db/u1db_internal.h 2012-06-19 19:41:19 +0000
3+++ include/u1db/u1db_internal.h 2012-06-19 19:41:19 +0000
4@@ -233,6 +233,17 @@
5 int u1db__get_generation(u1database *db, int *generation);
6
7 /**
8+ * Internal API, Get the global database rev and transaction id.
9+ */
10+int u1db__get_generation_info(u1database *db, int *generation,
11+ char **trans_id);
12+
13+/**
14+ * Internal API, Validate generation and transaction id.
15+ */
16+int u1db_validate_gen_and_trans_id(u1database *db, int generation,
17+ const char *trans_id);
18+/**
19 * Internal API, Allocate a new document id, for cases when callers do not
20 * supply their own. Callers of this API are expected to free the result.
21 */
22
23=== modified file 'src/u1db.c'
24--- src/u1db.c 2012-06-19 19:16:35 +0000
25+++ src/u1db.c 2012-06-19 19:41:19 +0000
26@@ -900,8 +900,8 @@
27 (stored_doc_rev != NULL));
28 }
29 if (status == U1DB_OK && replica_uid != NULL) {
30- status = u1db__set_sync_info(db, replica_uid, replica_gen,
31- replica_trans_id);
32+ status = u1db__set_sync_info(
33+ db, replica_uid, replica_gen, replica_trans_id);
34 }
35 if (status == U1DB_OK && at_gen != NULL) {
36 status = u1db__get_generation(db, at_gen);
37@@ -1264,14 +1264,26 @@
38 status = U1DB_OK;
39 *gen = 0;
40 *trans_id = strdup("");
41+ if (*trans_id == NULL) {
42+ status = U1DB_NOMEM;
43+ goto finish;
44+ }
45 } else if (status == SQLITE_ROW) {
46 status = U1DB_OK;
47 *gen = sqlite3_column_int(statement, 0);
48 tmp = (const char *)sqlite3_column_text(statement, 1);
49 if (tmp == NULL) {
50 *trans_id = strdup("");
51+ if (*trans_id == NULL) {
52+ status = U1DB_NOMEM;
53+ goto finish;
54+ }
55 } else {
56 *trans_id = strdup(tmp);
57+ if (*trans_id == NULL) {
58+ status = U1DB_NOMEM;
59+ goto finish;
60+ }
61 }
62 }
63 finish:
64@@ -1411,6 +1423,80 @@
65 return status;
66 }
67
68+int
69+u1db__get_generation_info(u1database *db, int *generation, char **trans_id)
70+{
71+ int status;
72+ const char *tmp;
73+
74+ sqlite3_stmt *statement;
75+ if (db == NULL || generation == NULL) {
76+ return U1DB_INVALID_PARAMETER;
77+ }
78+ status = sqlite3_prepare_v2(db->sql_handle,
79+ "SELECT max(generation), transaction_id FROM transaction_log", -1,
80+ &statement, NULL);
81+ if (status != SQLITE_OK) {
82+ return status;
83+ }
84+ status = sqlite3_step(statement);
85+ if (status == SQLITE_DONE) {
86+ // No records, we are at rev 0
87+ status = SQLITE_OK;
88+ *generation = 0;
89+ } else if (status == SQLITE_ROW) {
90+ status = SQLITE_OK;
91+ *generation = sqlite3_column_int(statement, 0);
92+ tmp = (const char *)sqlite3_column_text(statement, 1);
93+ if (tmp == NULL) {
94+ *trans_id = NULL;
95+ } else {
96+ *trans_id = strdup(tmp);
97+ if (*trans_id == NULL) {
98+ status = U1DB_NOMEM;
99+ }
100+ }
101+ }
102+ sqlite3_finalize(statement);
103+ return status;
104+}
105+
106+int
107+u1db_validate_gen_and_trans_id(u1database *db, int generation,
108+ const char *trans_id)
109+{
110+ int status = U1DB_OK;
111+ sqlite3_stmt *statement;
112+
113+ if (generation == 0)
114+ return status;
115+ if (db == NULL) {
116+ return U1DB_INVALID_PARAMETER;
117+ }
118+ status = sqlite3_prepare_v2(db->sql_handle,
119+ "SELECT transaction_id FROM transaction_log WHERE generation = ?", -1,
120+ &statement, NULL);
121+ if (status != SQLITE_OK) { goto finish; }
122+ status = sqlite3_bind_int(statement, 1, generation);
123+ if (status != SQLITE_OK) { goto finish; }
124+ status = sqlite3_step(statement);
125+ if (status == SQLITE_DONE) {
126+ status = U1DB_INVALID_GENERATION;
127+ goto finish;
128+ } else if (status == SQLITE_ROW) {
129+ // Note: We may want to handle the column containing NULL
130+ if (strcmp(trans_id,
131+ (const char *)sqlite3_column_text(statement, 0)) == 0) {
132+ status = U1DB_OK;
133+ goto finish;
134+ }
135+ status = U1DB_INVALID_TRANSACTION_ID;
136+ }
137+finish:
138+ sqlite3_finalize(statement);
139+ return status;
140+}
141+
142 char *
143 u1db__allocate_doc_id(u1database *db)
144 {
145@@ -1544,7 +1630,10 @@
146 // Note: We may want to handle the column containing NULL
147 tmp = (const char *)sqlite3_column_text(statement, 1);
148 if (tmp == NULL) {
149- *trans_id = NULL;
150+ *trans_id = strdup("");
151+ if (*trans_id == NULL) {
152+ status = U1DB_NOMEM;
153+ }
154 } else {
155 *trans_id = strdup(tmp);
156 if (*trans_id == NULL) {
157
158=== modified file 'src/u1db_sync_target.c'
159--- src/u1db_sync_target.c 2012-06-19 19:41:19 +0000
160+++ src/u1db_sync_target.c 2012-06-19 19:41:19 +0000
161@@ -24,7 +24,7 @@
162 static int st_get_sync_info(u1db_sync_target *st,
163 const char *source_replica_uid,
164 const char **st_replica_uid, int *st_gen, int *source_gen,
165- char **trans_id);
166+ char **source_trans_id);
167
168 static int st_record_sync_info(u1db_sync_target *st,
169 const char *source_replica_uid, int source_gen, const char *trans_id);
170@@ -106,7 +106,7 @@
171 static int
172 st_get_sync_info(u1db_sync_target *st, const char *source_replica_uid,
173 const char **st_replica_uid, int *st_gen, int *source_gen,
174- char **trans_id)
175+ char **source_trans_id)
176 {
177 int status = U1DB_OK;
178 u1database *db;
179@@ -125,8 +125,8 @@
180 db = (u1database *)st->implementation;
181 status = u1db_get_replica_uid(db, st_replica_uid);
182 if (status != U1DB_OK) { goto finish; }
183- status = u1db__get_sync_gen_info(db, source_replica_uid, source_gen,
184- trans_id);
185+ status = u1db__get_sync_gen_info(
186+ db, source_replica_uid, source_gen, source_trans_id);
187 if (status != U1DB_OK) { goto finish; }
188 status = u1db__get_generation(db, st_gen);
189 finish:
190@@ -614,6 +614,9 @@
191 status = target->get_sync_info(target, local_uid, &target_uid, &target_gen,
192 &local_gen_known_by_target, &local_trans_id_known_by_target);
193 if (status != U1DB_OK) { goto finish; }
194+ status = u1db_validate_gen_and_trans_id(
195+ db, local_gen_known_by_target, local_trans_id_known_by_target);
196+ if (status != U1DB_OK) { goto finish; }
197 status = u1db__get_sync_gen_info(db, target_uid,
198 &target_gen_known_by_local, &target_trans_id_known_by_local);
199 if (status != U1DB_OK) { goto finish; }
200@@ -623,8 +626,9 @@
201 // Before we start the sync exchange, get the list of doc_ids that we want
202 // to send. We have to do this first, so that local_gen_before_sync will
203 // match exactly the list of doc_ids we send
204- status = u1db_whats_changed(db, &local_gen, &local_trans_id,
205- (void*)&to_send_state, whats_changed_to_doc_ids);
206+ status = u1db_whats_changed(
207+ db, &local_gen, &local_trans_id, (void*)&to_send_state,
208+ whats_changed_to_doc_ids);
209 if (status != U1DB_OK) { goto finish; }
210 if (local_gen == local_gen_known_by_target
211 && target_gen == target_gen_known_by_local)
212@@ -644,19 +648,23 @@
213 &target_gen_known_by_local, &target_trans_id_known_by_local,
214 &return_doc_state, return_doc_to_insert_from_target);
215 if (status != U1DB_OK) { goto finish; }
216- status = u1db__get_generation(db, &local_gen);
217+ if (local_trans_id != NULL) {
218+ free(local_trans_id);
219+ }
220+ status = u1db__get_generation_info(db, &local_gen, &local_trans_id);
221 if (status != U1DB_OK) { goto finish; }
222 // Now we successfully sent and received docs, make sure we record the
223 // current remote generation
224- status = u1db__set_sync_info(db, target_uid, target_gen_known_by_local,
225- "T-sid");
226+ status = u1db__set_sync_info(
227+ db, target_uid, target_gen_known_by_local,
228+ target_trans_id_known_by_local);
229 if (status != U1DB_OK) { goto finish; }
230 if (return_doc_state.num_inserted > 0 &&
231- ((*local_gen_before_sync + return_doc_state.num_inserted)
232- == local_gen))
233+ ((*local_gen_before_sync + return_doc_state.num_inserted)
234+ == local_gen))
235 {
236- status = target->record_sync_info(target, local_uid, local_gen,
237- "T-sid");
238+ status = target->record_sync_info(
239+ target, local_uid, local_gen, local_trans_id);
240 if (status != U1DB_OK) { goto finish; }
241 }
242 finish:
243
244=== modified file 'u1db/backends/__init__.py'
245--- u1db/backends/__init__.py 2012-06-19 19:16:35 +0000
246+++ u1db/backends/__init__.py 2012-06-19 19:41:19 +0000
247@@ -22,7 +22,6 @@
248 import u1db
249 from u1db import (
250 errors,
251- vectorclock,
252 )
253 import u1db.sync
254 from u1db.vectorclock import VectorClockRev
255@@ -54,8 +53,17 @@
256 raise errors.InvalidDocId()
257
258 def _get_generation(self):
259+ """Return the current generation.
260+
261+ """
262 raise NotImplementedError(self._get_generation)
263
264+ def _get_generation_info(self):
265+ """Return the current generation and transaction id.
266+
267+ """
268+ raise NotImplementedError(self._get_generation_info)
269+
270 def _get_doc(self, doc_id):
271 """Extract the document from storage.
272
273@@ -94,6 +102,15 @@
274 result.append(doc)
275 return result
276
277+ def validate_gen_and_trans_id(self, generation, trans_id):
278+ """Validate the generation and transaction id.
279+
280+ Raises an InvalidGeneration when the generation does not exist, and an
281+ InvalidTransactionId when it does but with a different transaction id.
282+
283+ """
284+ raise NotImplementedError(self.validate_gen_and_trans_id)
285+
286 def _validate_source(self, other_replica_uid, other_generation,
287 other_transaction_id, cur_vcr, other_vcr):
288 """Validate the new generation and transaction id.
289
290=== modified file 'u1db/backends/inmemory.py'
291--- u1db/backends/inmemory.py 2012-06-18 12:51:14 +0000
292+++ u1db/backends/inmemory.py 2012-06-19 19:41:19 +0000
293@@ -79,6 +79,18 @@
294 def _get_generation(self):
295 return len(self._transaction_log)
296
297+ def _get_generation_info(self):
298+ return len(self._transaction_log), self._transaction_log[-1][1]
299+
300+ def validate_gen_and_trans_id(self, generation, trans_id):
301+ if generation == 0:
302+ return
303+ if generation > len(self._transaction_log):
304+ raise errors.InvalidGeneration
305+ if self._transaction_log[generation - 1][1] == trans_id:
306+ return
307+ raise errors.InvalidTransactionId
308+
309 def put_doc(self, doc):
310 if doc.doc_id is None:
311 raise errors.InvalidDocId()
312
313=== modified file 'u1db/backends/sqlite_backend.py'
314--- u1db/backends/sqlite_backend.py 2012-06-18 12:51:14 +0000
315+++ u1db/backends/sqlite_backend.py 2012-06-19 19:41:19 +0000
316@@ -265,6 +265,28 @@
317 return 0
318 return val
319
320+ def _get_generation_info(self):
321+ c = self._db_handle.cursor()
322+ c.execute(
323+ 'SELECT max(generation), transaction_id FROM transaction_log ')
324+ val = c.fetchone()
325+ if val[0] is None:
326+ return(0, val[1])
327+ return val
328+
329+ def validate_gen_and_trans_id(self, generation, trans_id):
330+ if generation == 0:
331+ return
332+ c = self._db_handle.cursor()
333+ c.execute(
334+ 'SELECT transaction_id FROM transaction_log WHERE generation = ?',
335+ (generation,))
336+ val = c.fetchone()
337+ if val is None:
338+ raise errors.InvalidGeneration
339+ if val[0] != trans_id:
340+ raise errors.InvalidTransactionId
341+
342 def _get_transaction_log(self):
343 c = self._db_handle.cursor()
344 c.execute("SELECT doc_id, transaction_id FROM transaction_log"
345
346=== modified file 'u1db/sync.py'
347--- u1db/sync.py 2012-06-19 19:41:19 +0000
348+++ u1db/sync.py 2012-06-19 19:41:19 +0000
349@@ -83,11 +83,11 @@
350 record with the target that they are fully up to date with our
351 new generation.
352 """
353- cur_gen = self.source._get_generation()
354+ cur_gen, trans_id = self.source._get_generation_info()
355 if (cur_gen == start_generation + self.num_inserted
356- and self.num_inserted > 0):
357- self.sync_target.record_sync_info(self.source._replica_uid,
358- cur_gen, 'T-sid')
359+ and self.num_inserted > 0):
360+ self.sync_target.record_sync_info(
361+ self.source._replica_uid, cur_gen, trans_id)
362
363 def sync(self, callback=None):
364 """Synchronize documents between source and target."""
365@@ -98,6 +98,8 @@
366 target_my_trans_id) = sync_target.get_sync_info(
367 self.source._replica_uid)
368 # what's changed since that generation and this current gen
369+ self.source.validate_gen_and_trans_id(
370+ target_my_gen, target_my_trans_id)
371 my_gen, _, changes = self.source.whats_changed(target_my_gen)
372
373 # this source last-seen database generation for the target
374@@ -121,7 +123,8 @@
375 self.source._replica_uid, target_last_known_gen,
376 return_doc_cb=self._insert_doc_from_target)
377 # record target synced-up-to generation including applying what we sent
378- self.source._set_sync_info(self.target_replica_uid, new_gen, 'T-sid')
379+ self.source._set_sync_info(
380+ self.target_replica_uid, new_gen, new_trans_id)
381
382 # if gapless record current reached generation with target
383 self._record_sync_info_with_the_target(my_gen)
384
385=== modified file 'u1db/tests/c_backend_wrapper.pyx'
386--- u1db/tests/c_backend_wrapper.pyx 2012-06-19 19:41:19 +0000
387+++ u1db/tests/c_backend_wrapper.pyx 2012-06-19 19:41:19 +0000
388@@ -204,6 +204,9 @@
389
390
391 int u1db__get_generation(u1database *, int *db_rev)
392+ int u1db__get_generation_info(u1database *, int *db_rev, char **trans_id)
393+ int u1db_validate_gen_and_trans_id(u1database *, int db_rev,
394+ const_char_ptr trans_id)
395 char *u1db__allocate_doc_id(u1database *)
396 int u1db__sql_close(u1database *)
397 int u1db__sql_is_open(u1database *)
398@@ -1064,6 +1067,22 @@
399 u1db__get_generation(self._db, &generation))
400 return generation
401
402+ def _get_generation_info(self):
403+ cdef int generation
404+ cdef char *trans_id
405+ handle_status("get_generation_info",
406+ u1db__get_generation_info(self._db, &generation, &trans_id))
407+ raw_trans_id = None
408+ if trans_id != NULL:
409+ raw_trans_id = trans_id
410+ free(trans_id)
411+ return generation, raw_trans_id
412+
413+ def validate_gen_and_trans_id(self, generation, trans_id):
414+ handle_status(
415+ "validate_gen_and_trans_id",
416+ u1db_validate_gen_and_trans_id(self._db, generation, trans_id))
417+
418 def _get_sync_gen_info(self, replica_uid):
419 cdef int generation, status
420 cdef char *trans_id = NULL
421
422=== modified file 'u1db/tests/test_backends.py'
423--- u1db/tests/test_backends.py 2012-06-19 19:41:19 +0000
424+++ u1db/tests/test_backends.py 2012-06-19 19:41:19 +0000
425@@ -437,6 +437,25 @@
426 self.db._put_doc_if_newer, doc, save_conflict=False,
427 replica_uid='other', replica_gen=1, replica_trans_id='T-sad')
428
429+ def test_validate_gen_and_trans_id(self):
430+ self.db.create_doc(simple_doc)
431+ gen, trans_id = self.db._get_generation_info()
432+ self.db.validate_gen_and_trans_id(gen, trans_id)
433+
434+ def test_validate_gen_and_trans_id_invalid_txid(self):
435+ self.db.create_doc(simple_doc)
436+ gen, _ = self.db._get_generation_info()
437+ self.assertRaises(
438+ errors.InvalidTransactionId,
439+ self.db.validate_gen_and_trans_id, gen, 'wrong')
440+
441+ def test_validate_gen_and_trans_id_invalid_txid(self):
442+ self.db.create_doc(simple_doc)
443+ gen, trans_id = self.db._get_generation_info()
444+ self.assertRaises(
445+ errors.InvalidGeneration,
446+ self.db.validate_gen_and_trans_id, gen + 1, trans_id)
447+
448 def test_validate_source_gen_and_trans_id_same(self):
449 self.db._set_sync_info('other', 1, 'T-sid')
450 v1 = vectorclock.VectorClockRev('other:1|self:1')
451
452=== modified file 'u1db/tests/test_c_backend.py'
453--- u1db/tests/test_c_backend.py 2012-06-19 19:41:19 +0000
454+++ u1db/tests/test_c_backend.py 2012-06-19 19:41:19 +0000
455@@ -73,6 +73,14 @@
456 db.create_doc(tests.simple_doc)
457 self.assertEqual(1, db._get_generation())
458
459+ def test__get_generation_info(self):
460+ db = c_backend_wrapper.CDatabase(':memory:')
461+ self.assertEqual((0, None), db._get_generation_info())
462+ db.create_doc(tests.simple_doc)
463+ info = db._get_generation_info()
464+ self.assertEqual(1, info[0])
465+ self.assertTrue(info[1].startswith('T-'))
466+
467 def test__set_replica_uid(self):
468 db = c_backend_wrapper.CDatabase(':memory:')
469 self.assertIsNot(None, db._replica_uid)
470
471=== modified file 'u1db/tests/test_sqlite_backend.py'
472--- u1db/tests/test_sqlite_backend.py 2012-05-31 16:31:31 +0000
473+++ u1db/tests/test_sqlite_backend.py 2012-06-19 19:41:19 +0000
474@@ -152,6 +152,9 @@
475 def test__get_generation(self):
476 self.assertEqual(0, self.db._get_generation())
477
478+ def test__get_generation_info(self):
479+ self.assertEqual((0, None), self.db._get_generation_info())
480+
481 def test_create_index(self):
482 self.db.create_index('test-idx', "key")
483 self.assertEqual([('test-idx', ["key"])], self.db.list_indexes())
484@@ -261,7 +264,7 @@
485 def test__open_database_with_factory(self):
486 temp_dir = self.createTempDir(prefix='u1db-test-')
487 path = temp_dir + '/test.sqlite'
488- db = sqlite_backend.SQLitePartialExpandDatabase(path)
489+ sqlite_backend.SQLitePartialExpandDatabase(path)
490 db2 = sqlite_backend.SQLiteDatabase._open_database(
491 path, document_factory=TestAlternativeDocument)
492 self.assertEqual(TestAlternativeDocument, db2._factory)
493@@ -322,7 +325,7 @@
494 def test_open_database_with_factory(self):
495 temp_dir = self.createTempDir(prefix='u1db-test-')
496 path = temp_dir + '/existing.sqlite'
497- db = sqlite_backend.SQLitePartialExpandDatabase(path)
498+ sqlite_backend.SQLitePartialExpandDatabase(path)
499 db2 = sqlite_backend.SQLiteDatabase.open_database(
500 path, create=False, document_factory=TestAlternativeDocument)
501 self.assertEqual(TestAlternativeDocument, db2._factory)
502
503=== modified file 'u1db/tests/test_sync.py'
504--- u1db/tests/test_sync.py 2012-06-19 19:41:19 +0000
505+++ u1db/tests/test_sync.py 2012-06-19 19:41:19 +0000
506@@ -839,6 +839,12 @@
507 doc1 = self.db1.create_doc('{"a": 1}', doc_id='the-doc')
508 db3 = self.create_database('test3')
509 self.sync(self.db2, self.db1)
510+ self.assertEqual(
511+ self.db1._get_generation_info(),
512+ self.db2._get_sync_gen_info(self.db1._replica_uid))
513+ self.assertEqual(
514+ self.db2._get_generation_info(),
515+ self.db1._get_sync_gen_info(self.db2._replica_uid))
516 self.sync(db3, self.db1)
517 # update on 2
518 doc2 = self.make_document('the-doc', doc1.rev, '{"a": 2}')
519@@ -872,7 +878,19 @@
520 self.db2.create_doc('{"b": 1}', doc_id='the-doc')
521 db3.create_doc('{"c": 1}', doc_id='the-doc')
522 self.sync(db3, self.db1)
523+ self.assertEqual(
524+ self.db1._get_generation_info(),
525+ db3._get_sync_gen_info(self.db1._replica_uid))
526+ self.assertEqual(
527+ db3._get_generation_info(),
528+ self.db1._get_sync_gen_info(db3._replica_uid))
529 self.sync(db3, self.db2)
530+ self.assertEqual(
531+ self.db2._get_generation_info(),
532+ db3._get_sync_gen_info(self.db2._replica_uid))
533+ self.assertEqual(
534+ db3._get_generation_info(),
535+ self.db2._get_sync_gen_info(db3._replica_uid))
536 self.assertEqual(3, len(db3.get_doc_conflicts('the-doc')))
537 doc1.set_json('{"a": 2}')
538 self.db1.put_doc(doc1)

Subscribers

People subscribed via source and target branches