def transaction(self, compare, success=None, failure=None): """ Perform a transaction. Example usage: .. code-block:: python etcd.transaction( compare=[ etcd.transactions.value('/doot/testing') == 'doot', etcd.transactions.version('/doot/testing') > 0, ], success=[ etcd.transactions.put('/doot/testing', 'success'), ], failure=[ etcd.transactions.put('/doot/testing', 'failure'), ] ) :param compare: A list of comparisons to make :param success: A list of operations to perform if all the comparisons are true :param failure: A list of operations to perform if any of the comparisons are false :return: A tuple of (operation status, responses) """ compare = [c.build_message() for c in compare] success_ops = self._ops_to_requests(success) failure_ops = self._ops_to_requests(failure) transaction_request = etcdrpc.TxnRequest(compare=compare, success=success_ops, failure=failure_ops) txn_response = self.kvstub.Txn( transaction_request, self.timeout, credentials=self.call_credentials, metadata=self.metadata ) responses = [] for response in txn_response.responses: response_type = response.WhichOneof('response') if response_type in ['response_put', 'response_delete_range', 'response_txn']: responses.append(response) elif response_type == 'response_range': range_kvs = [] for kv in response.response_range.kvs: range_kvs.append((kv.value, KVMetadata(kv, txn_response.header))) responses.append(range_kvs) return txn_response.succeeded, responses
def transaction(self, compare, success=None, failure=None): """ Perform a transaction. Example usage: .. code-block:: python etcd.transaction( compare=[ etcd.transactions.value('/doot/testing') == 'doot', etcd.transactions.version('/doot/testing') > 0, ], success=[ etcd.transactions.put('/doot/testing', 'success'), ], failure=[ etcd.transactions.put('/doot/testing', 'failure'), ] ) :param compare: A list of comparisons to make :param success: A list of operations to perform if all the comparisons are true :param failure: A list of operations to perform if any of the comparisons are false """ compare = [c.build_message() for c in compare] success_ops = self._ops_to_requests(success) failure_ops = self._ops_to_requests(failure) transaction_request = etcdrpc.TxnRequest(compare=compare, success=success_ops, failure=failure_ops) txn_response = self.kvstub.Txn(transaction_request) responses = [] for response in txn_response.responses: response_type = response.WhichOneof('response') if response_type == 'response_put': responses.append(None) elif response_type == 'response_range': range_kvs = [] for kv in response.response_range.kvs: range_kvs.append((kv.key, kv.value)) responses.append(range_kvs) return txn_response.succeeded, responses
def _ops_to_requests(self, ops): """ Return a list of grpc requests. Returns list from an input list of etcd3.transactions.{Put, Get, Delete, Txn} objects. """ request_ops = [] for op in ops: if isinstance(op, transactions.Put): request = self._build_put_request(op.key, op.value, op.lease, op.prev_kv) request_op = etcdrpc.RequestOp(request_put=request) request_ops.append(request_op) elif isinstance(op, transactions.Get): request = self._build_get_range_request(op.key, op.range_end) request_op = etcdrpc.RequestOp(request_range=request) request_ops.append(request_op) elif isinstance(op, transactions.Delete): request = self._build_delete_request(op.key, op.range_end, op.prev_kv) request_op = etcdrpc.RequestOp(request_delete_range=request) request_ops.append(request_op) elif isinstance(op, transactions.Txn): compare = [c.build_message() for c in op.compare] success_ops = self._ops_to_requests(op.success) failure_ops = self._ops_to_requests(op.failure) request = etcdrpc.TxnRequest(compare=compare, success=success_ops, failure=failure_ops) request_op = etcdrpc.RequestOp(request_txn=request) request_ops.append(request_op) else: raise Exception( 'Unknown request class {}'.format(op.__class__)) return request_ops
def push(self, kvs: List[Dict[str, any]], ks_delete: List[str] = None, ttl: int = None) -> bool: """ Method to submit a list of key-value pairs and delete a list of keys from the server as a single transaction :param kvs: List of KV pair :param ks_delete: List of keys to delete before the push of the new ones. Note that each key is read as a folder :param ttl: time to leave of the keys pushed, once expired the keys will be deleted :return: True if successful """ logger.debug(f"Calling push...") logger.debug(f"Preparing the transaction statement") ops = [] # check if we need to request a lease for the ttl if ttl: try: lease = self._lease(ttl) except EngineException: raise EngineException("Not able to push keys") # first delete the keys requested if ks_delete is not None and len(ks_delete) != 0: for kd in ks_delete: # every key is deleted with prefix=True range_end = self._incr_last_byte(kd) delete = self._server._build_delete_request(kd, range_end) request_op = etcdrpc.RequestOp(request_delete_range=delete) ops.append(request_op) # Prepare the transaction with a put operation for each KV pair for kv in kvs: k = kv["key"] v = kv["value"] put = self._server._build_put_request(k, v) if ttl: put.lease = lease request_op = etcdrpc.RequestOp(request_put=put) ops.append(request_op) transaction_request = etcdrpc.TxnRequest(success=ops) # commit transaction logger.debug(f"Committing the transaction statement: {ops}") try_again = True while try_again: try: try_again = False txn_response = self._server.kvstub.Txn( transaction_request, self._server.timeout, credentials=self._server.call_credentials, metadata=self._server.metadata) except grpc._channel._InactiveRpcError as e: if e._state.code.name == "UNAUTHENTICATED": # it seems that sometimes the token expires, so re-init the server and try again try_again = True logger.debug(f"Error {e}, trying again", exc_info=True) self._initialise_server() else: raise e assert txn_response.succeeded, f'Not able to execute the transaction' logger.debug(f"Transaction completed") # read the header if hasattr(txn_response, 'header'): h = txn_response.header rev = int(h.revision) logger.debug(f"New server revision {rev}") return True