def populate(self): app = self.app pt = app.pt uuid = app.uuid self.partition_dict = p = {} self.replicate_dict = {} self.source_dict = {} self.ttid_set = set() last_tid, last_trans_dict, last_obj_dict, _ = app.dm.getLastIDs() next_tid = app.dm.getBackupTID() or last_tid next_tid = add64(next_tid, 1) if next_tid else ZERO_TID outdated_list = [] for offset in xrange(pt.getPartitions()): for cell in pt.getCellList(offset): if cell.getUUID() == uuid and not cell.isCorrupted(): self.partition_dict[offset] = p = Partition() if cell.isOutOfDate(): outdated_list.append(offset) try: p.next_trans = add64(last_trans_dict[offset], 1) except KeyError: p.next_trans = ZERO_TID p.next_obj = last_obj_dict.get(offset, ZERO_TID) p.max_ttid = INVALID_TID else: p.next_trans = p.next_obj = next_tid p.max_ttid = None if outdated_list: self.app.master_conn.ask(Packets.AskUnfinishedTransactions(), offset_list=outdated_list)
def checkRange(self, conn, *args): if self.conn_dict.get(conn, self) != conn.getPeerId(): # Ignore answers to old requests, # because we did nothing to cancel them. logging.info("ignored AnswerCheck*Range%r", args) return self.conn_dict[conn] = args answer_set = set(self.conn_dict.itervalues()) if len(answer_set) > 1: for answer in answer_set: if type(answer) is not tuple: return # TODO: Automatically tell corrupted cells to fix their data # if we know a good source. # For the moment, tell master to put them in CORRUPTED state # and keep up checking if useful. uuid = self.app.uuid args = None if self.source is None else self.conn_dict[ None if self.source.getUUID() == uuid else self.source.getConnection()] uuid_list = [] for conn, answer in self.conn_dict.items(): if answer != args: del self.conn_dict[conn] if conn is None: uuid_list.append(uuid) else: uuid_list.append(conn.getUUID()) self.app.closeClient(conn) p = Packets.NotifyPartitionCorrupted(self.partition, uuid_list) self.app.master_conn.send(p) if len(self.conn_dict) <= 1: logging.warning("check of partition %u aborted", self.partition) self.queue.clear() self._nextPartition() return try: count, _, max_tid = args except ValueError: # AnswerCheckSerialRange count, _, self.next_tid, _, max_oid = args if count < CHECK_COUNT: logging.debug("partition %u checked from %s to %s", self.partition, dump(self.min_tid), dump(self.max_tid)) self._nextPartition() return self.next_oid = add64(max_oid, 1) else: # AnswerCheckTIDRange if count < CHECK_COUNT: self.next_tid = self.min_tid self.next_oid = ZERO_OID else: self.next_tid = add64(max_tid, 1) self._nextRange()
def checkRange(self, conn, *args): if self.conn_dict.get(conn, self) != conn.getPeerId(): # Ignore answers to old requests, # because we did nothing to cancel them. logging.info("ignored AnswerCheck*Range%r", args) return self.conn_dict[conn] = args answer_set = set(self.conn_dict.itervalues()) if len(answer_set) > 1: for answer in answer_set: if type(answer) is not tuple: return # TODO: Automatically tell corrupted cells to fix their data # if we know a good source. # For the moment, tell master to put them in CORRUPTED state # and keep up checking if useful. uuid = self.app.uuid args = None if self.source is None else self.conn_dict[ None if self.source.getUUID() == uuid else self.source.getConnection()] uuid_list = [] for conn, answer in self.conn_dict.items(): if answer != args: del self.conn_dict[conn] if conn is None: uuid_list.append(uuid) else: uuid_list.append(conn.getUUID()) self.app.closeClient(conn) p = Packets.NotifyPartitionCorrupted(self.partition, uuid_list) self.app.master_conn.notify(p) if len(self.conn_dict) <= 1: logging.warning("check of partition %u aborted", self.partition) self.queue.clear() self._nextPartition() return try: count, _, max_tid = args except ValueError: # AnswerCheckSerialRange count, _, self.next_tid, _, max_oid = args if count < CHECK_COUNT: logging.debug("partition %u checked from %s to %s", self.partition, dump(self.min_tid), dump(self.max_tid)) self._nextPartition() return self.next_oid = add64(max_oid, 1) else: # AnswerCheckTIDRange if count < CHECK_COUNT: self.next_tid = self.min_tid self.next_oid = ZERO_OID else: self.next_tid = add64(max_tid, 1) self._nextRange()
def notifyPartitionChanges(self, cell_list): """This is a callback from MasterOperationHandler.""" abort = False added_list = [] app = self.app last_tid, last_trans_dict, last_obj_dict, _ = app.dm.getLastIDs() for offset, uuid, state in cell_list: if uuid == app.uuid: if state in (CellStates.DISCARDED, CellStates.CORRUPTED): try: del self.partition_dict[offset] except KeyError: continue self.replicate_dict.pop(offset, None) self.source_dict.pop(offset, None) abort = abort or self.current_partition == offset elif state == CellStates.OUT_OF_DATE: assert offset not in self.partition_dict self.partition_dict[offset] = p = Partition() try: p.next_trans = add64(last_trans_dict[offset], 1) except KeyError: p.next_trans = ZERO_TID p.next_obj = last_obj_dict.get(offset, ZERO_TID) p.max_ttid = INVALID_TID added_list.append(offset) if added_list: self.app.master_conn.ask(Packets.AskUnfinishedTransactions(), offset_list=added_list) if abort: self.abort()
def getTransaction(self, oid_list): self._last_ttid = ttid = add64(self._last_ttid, 1) transaction = oid_list, 'user', 'desc', 'ext', False, ttid H = "0" * 20 object_list = [(oid, self.db.holdData(H, '', 1), None) for oid in oid_list] return (transaction, object_list)
def getTransaction(self, oid_list): self._last_ttid = ttid = add64(self._last_ttid, 1) transaction = oid_list, 'user', 'desc', 'ext', False, ttid H = "0" * 20 object_list = [(oid, self.db.holdData(H, oid, '', 1), None) for oid in oid_list] return (transaction, object_list)
def notifyReplicationDone(self, node, offset, tid): app = self.app cell = app.pt.getCell(offset, node.getUUID()) tid_list = self.tid_list[offset] if tid_list: # may be empty if the cell is out-of-date # or if we're not fully initialized if tid < tid_list[0]: cell.replicating = tid else: try: tid = add64(tid_list[bisect(tid_list, tid)], -1) except IndexError: last_tid = app.getLastTransaction() if tid < last_tid: tid = last_tid node.send(Packets.Replicate(tid, '', {offset: None})) logging.debug("partition %u: updating backup_tid of %r to %s", offset, cell, dump(tid)) cell.backup_tid = tid # TODO: Provide invalidation feedback about new txns to read-only # clients connected to backup cluster. Not only here but also # hooked to in-progress feedback from fetchObjects (storage). # Forget tids we won't need anymore. cell_list = app.pt.getCellList(offset, readable=True) del tid_list[:bisect(tid_list, min(x.backup_tid for x in cell_list))] primary_node = self.primary_partition_dict.get(offset) primary = primary_node is node result = None if primary else app.pt.setUpToDate(node, offset) assert cell.isReadable() if result: # was out-of-date if primary_node is not None: max_tid, = [ x.backup_tid for x in cell_list if x.getNode() is primary_node ] if tid < max_tid: cell.replicating = max_tid logging.debug( "ask %s to replicate partition %u up to %s from %s", uuid_str(node.getUUID()), offset, dump(max_tid), uuid_str(primary_node.getUUID())) node.send( Packets.Replicate(max_tid, '', {offset: primary_node.getAddress()})) else: if app.getClusterState() == ClusterStates.BACKINGUP: self.triggerBackup(node) if primary: # Notify secondary storages that they can replicate from # primary ones, even if they are already replicating. p = Packets.Replicate(tid, '', {offset: node.getAddress()}) for cell in cell_list: if max(cell.backup_tid, cell.replicating) < tid: cell.replicating = tid logging.debug( "ask %s to replicate partition %u up to %s from %s", uuid_str(cell.getUUID()), offset, dump(tid), uuid_str(node.getUUID())) cell.getNode().send(p) return result
def askObject(self, conn, oid, serial, tid): backup_tid = self.app.dm.getBackupTID() if serial: if serial > backup_tid: # obj lookup will find nothing, but return properly either # OidDoesNotExist or OidNotFound serial = ZERO_TID elif tid: tid = min(tid, add64(backup_tid, 1)) # limit "latest obj" query to tid <= backup_tid else: tid = add64(backup_tid, 1) super(ClientReadOnlyOperationHandler, self).askObject(conn, oid, serial, tid)
def getBackupTID(self): outdated_set = set(self.app.pt.getOutdatedOffsetListFor(self.app.uuid)) tid = INVALID_TID for offset, p in self.partition_dict.iteritems(): if offset not in outdated_set: tid = min(tid, p.next_trans, p.next_obj) if ZERO_TID != tid != INVALID_TID: return add64(tid, -1) return ZERO_TID
def _(): # Unfortunately, copyTransactionsFrom does not abort in case # of failure, so we have to reopen. zodb = storageFromString(self._storage) try: self.min_tid = util.add64(zodb.lastTransaction(), 1) zodb.copyTransactionsFrom(self) finally: zodb.close()
def testResumingReplication(self, cluster): """ Check from where replication resumes for an OUT_OF_DATE cell that has a hole, which is possible because OUT_OF_DATE cells are writable. """ ask = [] def logReplication(conn, packet): if isinstance( packet, (Packets.AskFetchTransactions, Packets.AskFetchObjects)): ask.append(packet._args[2:]) def getTIDList(): return [t.tid for t in c.db().storage.iterator()] s0, s1 = cluster.storage_list t, c = cluster.getTransaction() r = c.root() # s1 is UP_TO_DATE and it has the initial transaction. # Let's outdate it: replication will have to resume just after this # transaction, regardless of future written transactions. # To make sure, we get a hole in the cell, we block replication. s1.stop() cluster.join((s1, )) r._p_changed = 1 t.commit() s1.resetNode() with Patch(replicator.Replicator, connected=lambda *_: None): s1.start() self.tic() r._p_changed = 1 t.commit() self.tic() s1.stop() cluster.join((s1, )) tids = getTIDList() s1.resetNode() # Initialization done. Now we check that replication is correct # and efficient. with ConnectionFilter() as f: f.add(logReplication) s1.start() self.tic() self.assertEqual([], cluster.getOutdatedCells()) s0.stop() cluster.join((s0, )) self.assertEqual(tids, getTIDList()) t0_next = add64(tids[0], 1) self.assertEqual(ask, [ (t0_next, tids[2], tids[2:]), (t0_next, tids[2], ZERO_OID, { tids[2]: [ZERO_OID] }), ])
def notifyReplicationDone(self, node, offset, tid): app = self.app cell = app.pt.getCell(offset, node.getUUID()) tid_list = self.tid_list[offset] if tid_list: # may be empty if the cell is out-of-date # or if we're not fully initialized if tid < tid_list[0]: cell.replicating = tid else: try: tid = add64(tid_list[bisect(tid_list, tid)], -1) except IndexError: last_tid = app.getLastTransaction() if tid < last_tid: tid = last_tid node.notify(Packets.Replicate(tid, '', {offset: None})) logging.debug("partition %u: updating backup_tid of %r to %s", offset, cell, dump(tid)) cell.backup_tid = tid # Forget tids we won't need anymore. cell_list = app.pt.getCellList(offset, readable=True) del tid_list[:bisect(tid_list, min(x.backup_tid for x in cell_list))] primary_node = self.primary_partition_dict.get(offset) primary = primary_node is node result = None if primary else app.pt.setUpToDate(node, offset) assert cell.isReadable() if result: # was out-of-date if primary_node is not None: max_tid, = [x.backup_tid for x in cell_list if x.getNode() is primary_node] if tid < max_tid: cell.replicating = max_tid logging.debug( "ask %s to replicate partition %u up to %s from %s", uuid_str(node.getUUID()), offset, dump(max_tid), uuid_str(primary_node.getUUID())) node.notify(Packets.Replicate(max_tid, '', {offset: primary_node.getAddress()})) else: if app.getClusterState() == ClusterStates.BACKINGUP: self.triggerBackup(node) if primary: # Notify secondary storages that they can replicate from # primary ones, even if they are already replicating. p = Packets.Replicate(tid, '', {offset: node.getAddress()}) for cell in cell_list: if max(cell.backup_tid, cell.replicating) < tid: cell.replicating = tid logging.debug( "ask %s to replicate partition %u up to %s from %s", uuid_str(cell.getUUID()), offset, dump(tid), uuid_str(node.getUUID())) cell.getNode().notify(p) return result
def iterator(app, start=None, stop=None): """NEO transaction iterator""" if start is None: start = ZERO_TID stop = min(stop or MAX_TID, app.last_tid) while 1: max_tid, chunk = app.transactionLog(start, stop, CHUNK_LENGTH) if not chunk: break # nothing more for txn in chunk: yield Transaction(app, txn) start = add64(max_tid, 1)
def testInvalidTTID(self): cluster = NEOCluster() try: cluster.start() client = cluster.client txn = transaction.Transaction() client.tpc_begin(txn) txn_context = client._txn_container.get(txn) txn_context["ttid"] = add64(txn_context["ttid"], 1) self.assertRaises(POSException.StorageError, client.tpc_finish, txn, None) finally: cluster.stop()
def iterator(self): db = self._db np = self._np offset_list = xrange(np) while 1: with db: # Check the partition table at the beginning of every # transaction. Once the import is finished and at least one # cell is replicated, it is possible that some of this node # get outdated. In this case, wait for the next PT change. if np == len(db._readable_set): while 1: tid_list = [] max_tid = MAX_TID for offset in offset_list: x = db.getReplicationTIDList( self.min_tid, max_tid, self.chunk_size, offset) tid_list += x if len(x) == self.chunk_size: max_tid = x[-1] if not tid_list: break tid_list.sort() for tid in tid_list: if self._stop.is_set(): return yield TransactionRecord(db, tid) if tid == max_tid: break else: self.min_tid = util.add64(tid, 1) break self.min_tid = util.add64(tid, 1) if not self._event.is_set(): self._idle.set() self._event.wait() self._idle.clear() self._event.clear() if self._stop.is_set(): break
def testInvalidTTID(self): cluster = NEOCluster() try: cluster.start() client = cluster.client txn = transaction.Transaction() client.tpc_begin(txn) txn_context = client._txn_container.get(txn) txn_context['ttid'] = add64(txn_context['ttid'], 1) self.assertRaises(POSException.StorageError, client.tpc_finish, txn, None, lambda tid: None) finally: cluster.stop()
def finish(self): offset = self.current_partition tid = self.replicate_tid del self.current_partition, self.replicate_tid p = self.partition_dict[offset] p.next_obj = add64(tid, 1) self.updateBackupTID() if not p.max_ttid: p = Packets.NotifyReplicationDone(offset, tid) self.app.master_conn.notify(p) logging.debug("partition %u replicated up to %s from %r", offset, dump(tid), self.current_node) self.getCurrentConnection().setReconnectionNoDelay() self._nextPartition()
def fetchObjects(self, min_tid=None, min_oid=ZERO_OID): offset = self.current_partition p = self.partition_dict[offset] max_tid = self.replicate_tid dm = self.app.dm if min_tid: p.next_obj = min_tid self.updateBackupTID() dm.updateCellTID(offset, add64(min_tid, -1)) dm.commit() # like in fetchTransactions else: min_tid = p.next_obj p.next_trans = add64(max_tid, 1) object_dict = {} for serial, oid in dm.getReplicationObjectList(min_tid, max_tid, FETCH_COUNT, offset, min_oid): try: object_dict[serial].append(oid) except KeyError: object_dict[serial] = [oid] self._conn_msg_id = self.current_node.ask( Packets.AskFetchObjects(offset, FETCH_COUNT, min_tid, max_tid, min_oid, object_dict))
def finish(self): offset = self.current_partition tid = self.replicate_tid del self.current_partition, self._conn_msg_id, self.replicate_tid p = self.partition_dict[offset] p.next_obj = add64(tid, 1) self.updateBackupTID() if p.max_ttid or offset in self.replicate_dict and \ offset not in self.source_dict: logging.debug("unfinished transactions: %r", self.ttid_set) else: self.app.tm.replicated(offset, tid) logging.debug("partition %u replicated up to %s from %r", offset, dump(tid), self.current_node) self.getCurrentConnection().setReconnectionNoDelay() self._nextPartition()
def fetchObjects(self, min_tid=None, min_oid=ZERO_OID): offset = self.current_partition p = self.partition_dict[offset] max_tid = self.replicate_tid if min_tid: p.next_obj = min_tid else: min_tid = p.next_obj p.next_trans = add64(max_tid, 1) object_dict = {} for serial, oid in self.app.dm.getReplicationObjectList(min_tid, max_tid, FETCH_COUNT, offset, min_oid): try: object_dict[serial].append(oid) except KeyError: object_dict[serial] = [oid] self.current_node.getConnection().ask(Packets.AskFetchObjects( offset, FETCH_COUNT, min_tid, max_tid, min_oid, object_dict))
def backup(self, tid, source_dict): next_tid = None for offset, source in source_dict.iteritems(): if source: self.source_dict[offset] = source self.replicate_dict[offset] = tid elif offset != self.current_partition and \ offset not in self.replicate_dict: # The master did its best to avoid useless replication orders # but there may still be a few, and we may receive redundant # update notification of backup_tid. # So, we do nothing here if we are already replicating. p = self.partition_dict[offset] if not next_tid: next_tid = add64(tid, 1) p.next_trans = p.next_obj = next_tid if next_tid: self.updateBackupTID() self._nextPartition()
def fetchObjects(self, min_tid=None, min_oid=ZERO_OID): offset = self.current_partition p = self.partition_dict[offset] max_tid = self.replicate_tid if min_tid: p.next_obj = min_tid else: min_tid = p.next_obj p.next_trans = add64(max_tid, 1) object_dict = {} for serial, oid in self.app.dm.getReplicationObjectList( min_tid, max_tid, FETCH_COUNT, offset, min_oid): try: object_dict[serial].append(oid) except KeyError: object_dict[serial] = [oid] self.current_node.getConnection().ask( Packets.AskFetchObjects(offset, FETCH_COUNT, min_tid, max_tid, min_oid, object_dict))
def notifyPartitionChanges(self, cell_list): """This is a callback from MasterOperationHandler.""" abort = False added_list = [] discarded_list = [] readable_list = [] app = self.app last_tid, last_trans_dict, last_obj_dict, _ = app.dm.getLastIDs() for offset, uuid, state in cell_list: if uuid == app.uuid: if state in (CellStates.DISCARDED, CellStates.CORRUPTED): try: del self.partition_dict[offset] except KeyError: continue self.replicate_dict.pop(offset, None) self.source_dict.pop(offset, None) abort = abort or self.current_partition == offset discarded_list.append(offset) elif state == CellStates.OUT_OF_DATE: assert offset not in self.partition_dict self.partition_dict[offset] = p = Partition() try: p.next_trans = add64(last_trans_dict[offset], 1) except KeyError: p.next_trans = ZERO_TID p.next_obj = last_obj_dict.get(offset, ZERO_TID) p.max_ttid = INVALID_TID added_list.append(offset) else: assert state in (CellStates.UP_TO_DATE, CellStates.FEEDING), state readable_list.append(offset) tm = app.tm if added_list: tm.replicating(added_list) if discarded_list: tm.discarded(discarded_list) if readable_list: tm.readable(readable_list) if abort: self.abort()
def invalidatePartitions(self, tid, prev_tid, partition_set): app = self.app app.setLastTransaction(tid) pt = app.pt trigger_set = set() untouched_dict = defaultdict(dict) for offset in xrange(pt.getPartitions()): try: last_max_tid = self.tid_list[offset][-1] except IndexError: last_max_tid = prev_tid if offset in partition_set: primary_list = [] node_list = [] cell_list = pt.getCellList(offset, readable=True) for cell in cell_list: node = cell.getNode() assert node.isConnected(), node if cell.backup_tid == prev_tid: if prev_tid == tid: # Connecting to upstream: any node is that is # up-to-date wrt upstream is candidate for being # primary. assert self.ignore_invalidations if app.isStorageReady(node.getUUID()): primary_list.append(node) continue # Let's given 4 TID t0,t1,t2,t3: if a cell is only # modified by t0 & t3 and has all data for t0, 4 values # are possible for its 'backup_tid' until it replicates # up to t3: t0, t1, t2 or t3 - 1 # Choosing the smallest one (t0) is easier to implement # but when leaving backup mode, we would always lose # data if the last full transaction does not modify # all partitions. t1 is wrong for the same reason. # So we have chosen the highest one (t3 - 1). # t2 should also work but maybe harder to implement. cell.backup_tid = add64(tid, -1) logging.debug( "partition %u: updating backup_tid of %r to %s", offset, cell, dump(cell.backup_tid)) else: assert cell.backup_tid < last_max_tid, ( cell.backup_tid, last_max_tid, prev_tid, tid) if app.isStorageReady(node.getUUID()): node_list.append(node) # Make sure we have a primary storage for this partition. if offset not in self.primary_partition_dict: self.primary_partition_dict[offset] = \ random.choice(primary_list or node_list) if node_list: self.tid_list[offset].append(tid) if primary_list: # Resume replication to secondary cells. self._triggerSecondary( self.primary_partition_dict[offset], offset, tid, cell_list) else: trigger_set.update(node_list) else: # Partition not touched, so increase 'backup_tid' of all # "up-to-date" replicas, without having to replicate. for cell in pt.getCellList(offset, readable=True): if last_max_tid <= cell.backup_tid: cell.backup_tid = tid untouched_dict[cell.getNode()][offset] = None elif last_max_tid <= cell.replicating: # Same for 'replicating' to avoid useless orders. logging.debug( "silently update replicating order" " of %s for partition %u, up to %s", uuid_str(cell.getUUID()), offset, dump(tid)) cell.replicating = tid for node, untouched_dict in untouched_dict.iteritems(): if app.isStorageReady(node.getUUID()): node.send(Packets.Replicate(tid, '', untouched_dict)) for node in trigger_set: self.triggerBackup(node) count = sum(map(len, self.tid_list)) if self.debug_tid_count < count: logging.debug("Maximum number of tracked tids: %u", count) self.debug_tid_count = count
def invalidatePartitions(self, tid, partition_set): app = self.app prev_tid = app.getLastTransaction() app.setLastTransaction(tid) pt = app.pt trigger_set = set() untouched_dict = defaultdict(dict) for offset in xrange(pt.getPartitions()): try: last_max_tid = self.tid_list[offset][-1] except IndexError: last_max_tid = prev_tid if offset in partition_set: self.tid_list[offset].append(tid) node_list = [] for cell in pt.getCellList(offset, readable=True): node = cell.getNode() assert node.isConnected(), node if cell.backup_tid == prev_tid: # Let's given 4 TID t0,t1,t2,t3: if a cell is only # modified by t0 & t3 and has all data for t0, 4 values # are possible for its 'backup_tid' until it replicates # up to t3: t0, t1, t2 or t3 - 1 # Choosing the smallest one (t0) is easier to implement # but when leaving backup mode, we would always lose # data if the last full transaction does not modify # all partitions. t1 is wrong for the same reason. # So we have chosen the highest one (t3 - 1). # t2 should also work but maybe harder to implement. cell.backup_tid = add64(tid, -1) logging.debug( "partition %u: updating backup_tid of %r to %s", offset, cell, dump(cell.backup_tid)) else: assert cell.backup_tid < last_max_tid, ( cell.backup_tid, last_max_tid, prev_tid, tid) if app.isStorageReady(node.getUUID()): node_list.append(node) assert node_list trigger_set.update(node_list) # Make sure we have a primary storage for this partition. if offset not in self.primary_partition_dict: self.primary_partition_dict[offset] = \ random.choice(node_list) else: # Partition not touched, so increase 'backup_tid' of all # "up-to-date" replicas, without having to replicate. for cell in pt.getCellList(offset, readable=True): if last_max_tid <= cell.backup_tid: cell.backup_tid = tid untouched_dict[cell.getNode()][offset] = None elif last_max_tid <= cell.replicating: # Same for 'replicating' to avoid useless orders. logging.debug("silently update replicating order" " of %s for partition %u, up to %s", uuid_str(cell.getUUID()), offset, dump(tid)) cell.replicating = tid for node, untouched_dict in untouched_dict.iteritems(): if app.isStorageReady(node.getUUID()): node.notify(Packets.Replicate(tid, '', untouched_dict)) for node in trigger_set: self.triggerBackup(node) count = sum(map(len, self.tid_list)) if self.debug_tid_count < count: logging.debug("Maximum number of tracked tids: %u", count) self.debug_tid_count = count
def testResumingBackupReplication(self, backup): upstream = backup.upstream t, c = upstream.getTransaction() r = c.root() r[1] = PCounter() t.commit() r[2] = ob = PCounter() tids = [] def newTransaction(): r._p_changed = ob._p_changed = 1 with upstream.moduloTID(0): t.commit() self.tic() tids.append(r._p_serial) def getTIDList(storage): return storage.dm.getReplicationTIDList(tids[0], MAX_TID, 9, 0) newTransaction() self.assertEqual(u64(ob._p_oid), 2) getBackupTid = backup.master.pt.getBackupTid # Check when an OUT_OF_DATE cell has more data than an UP_TO_DATE one. primary = backup.master.backup_app.primary_partition_dict[0]._uuid slave, primary = sorted(backup.storage_list, key=lambda x: x.uuid == primary) with ConnectionFilter() as f: @f.delayAnswerFetchTransactions def delay(conn, x={None: 0, primary.uuid: 0}): return x.pop(conn.getUUID(), 1) newTransaction() self.assertEqual(getBackupTid(), tids[1]) primary.stop() backup.join((primary, )) primary.resetNode() primary.start() self.tic() primary, slave = slave, primary self.assertEqual(tids, getTIDList(slave)) self.assertEqual(tids[:1], getTIDList(primary)) self.assertEqual(getBackupTid(), add64(tids[1], -1)) self.assertEqual(f.filtered_count, 3) self.tic() self.assertEqual(4, self.checkBackup(backup)) self.assertEqual(getBackupTid(min), tids[1]) # Check that replication resumes from the maximum possible tid # (for UP_TO_DATE cells of a backup cluster). More precisely: # - cells are handled independently (done here by blocking replication # of partition 1 to keep the backup TID low) # - trans and obj are also handled independently (with FETCH_COUNT=1, # we interrupt replication of obj in the middle of a transaction) slave.stop() backup.join((slave, )) ask = [] def delayReplicate(conn, packet): if isinstance(packet, Packets.AskFetchObjects): if len(ask) == 6: return True elif not isinstance(packet, Packets.AskFetchTransactions): return ask.append(packet._args) conn, = upstream.master.getConnectionList(backup.master) with ConnectionFilter() as f, Patch( replicator.Replicator, _nextPartitionSortKey=lambda orig, self, offset: offset): f.add(delayReplicate) delayReconnect = f.delayAskLastTransaction() conn.close() newTransaction() newTransaction() newTransaction() self.assertFalse(ask) self.assertEqual(f.filtered_count, 1) with Patch(replicator, FETCH_COUNT=1): f.remove(delayReconnect) self.tic() t1_next = add64(tids[1], 1) self.assertEqual( ask, [ # trans (0, 1, t1_next, tids[4], []), (0, 1, tids[3], tids[4], []), (0, 1, tids[4], tids[4], []), # obj (0, 1, t1_next, tids[4], ZERO_OID, {}), (0, 1, tids[2], tids[4], p64(2), {}), (0, 1, tids[3], tids[4], ZERO_OID, {}), ]) del ask[:] max_ask = None backup.stop() newTransaction() backup.start((primary, )) n = replicator.FETCH_COUNT t4_next = add64(tids[4], 1) self.assertEqual(ask, [ (0, n, t4_next, tids[5], []), (0, n, tids[3], tids[5], ZERO_OID, { tids[3]: [ZERO_OID] }), (1, n, t1_next, tids[5], []), (1, n, t1_next, tids[5], ZERO_OID, {}), ]) self.tic() self.assertEqual(2, self.checkBackup(backup))