|
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 |
|
3 | | -"""Main module.""" |
| 3 | +# TODO |
| 4 | +""" |
| 5 | +module comment |
| 6 | +""" |
| 7 | +import boto3 |
| 8 | +import botocore |
| 9 | +import datetime |
| 10 | +import socket |
| 11 | +import time |
| 12 | +import threading |
| 13 | +import uuid |
| 14 | + |
| 15 | + |
| 16 | +class DynamoDBLockClient: |
| 17 | + # TODO |
| 18 | + """ |
| 19 | + class comment |
| 20 | + """ |
| 21 | + |
| 22 | + DEFAULT_TABLE_NAME = 'lockTable' |
| 23 | + DEFAULT_PARTITION_KEY_NAME = 'key' |
| 24 | + DEFAULT_SORT_KEY_NAME = 'sort_key' |
| 25 | + DEFAULT_LEASE_DURATION = datetime.timedelta(seconds=30) |
| 26 | + DEFAULT_HEARTBEAT_PERIOD = datetime.timedelta(seconds=10) |
| 27 | + |
| 28 | + def __init__(self, |
| 29 | + dynamodb_client, |
| 30 | + table_name = DEFAULT_TABLE_NAME, |
| 31 | + partition_key_name = DEFAULT_PARTITION_KEY_NAME, |
| 32 | + sort_key_name = DEFAULT_SORT_KEY_NAME, |
| 33 | + owner_name = None, |
| 34 | + lease_duration = DEFAULT_LEASE_DURATION, |
| 35 | + heartbeat_period = DEFAULT_HEARTBEAT_PERIOD |
| 36 | + ): |
| 37 | + """ |
| 38 | + The constructor that support default values for all/most of the arguments. |
| 39 | + |
| 40 | + :param dynamodb_client: boto3.DynamoDB.Client - mandatory argument |
| 41 | + :param table_name: str - defaults to 'lockTable' |
| 42 | + :param partition_key_name: str - defaults to 'key' |
| 43 | + :param sort_key_name: str - defaults to 'sort_key' |
| 44 | + :param owner_name: str - defaults to hostname + uuid |
| 45 | + :param lease_duration: datetime.timedelta - The length of time that the lease for the lock |
| 46 | + will be granted for. i.e. if there is no heartbeat for this period of time, then |
| 47 | + the lock will be considered as expired. Defaults to 30 seconds. |
| 48 | + :param heartbeat_period: datetime.timedelta - How often to update DynamoDB to note that the |
| 49 | + instance is still running. It is recommended to make this at least 3 times smaller |
| 50 | + than the leaseDuration. Defaults to 10 seconds. |
| 51 | + """ |
| 52 | + self.uuid = uuid.uuid4() |
| 53 | + self.dynamodb_client = dynamodb_client |
| 54 | + self.table_name = table_name |
| 55 | + self.partition_key_name = partition_key_name |
| 56 | + self.sort_key_name = sort_key_name |
| 57 | + self.owner_name = owner_name if owner_name else socket.getfqdn() + self.uuid |
| 58 | + self.lease_duration = lease_duration |
| 59 | + self.heartbeat_period = heartbeat_period |
| 60 | + self.background_thread = self._start_background_thread() |
| 61 | + self.locks = {} |
| 62 | + self.shutting_down = False |
| 63 | + |
| 64 | + def _start_background_thread(self): |
| 65 | + """ |
| 66 | + Creates and starts a daemon thread - that calls DynamoDBLockClient.run() method. |
| 67 | + """ |
| 68 | + background_thread = threading.Thread( |
| 69 | + name='DynamoDBLockClient-' + uuid, |
| 70 | + target=self |
| 71 | + ) |
| 72 | + background_thread.daemon = True |
| 73 | + background_thread.start() |
| 74 | + return background_thread |
| 75 | + |
| 76 | + def run(self): |
| 77 | + """ |
| 78 | + Iterates over all the locks owned by this lock-client, and renews their leases. |
| 79 | + """ |
| 80 | + while (True): |
| 81 | + if self.shutting_down: return None |
| 82 | + |
| 83 | + start_time = datetime.datetime.utcnow() |
| 84 | + for lock in self.locks.itervalues(): |
| 85 | + self.send_heartbeat(lock) |
| 86 | + end_time = datetime.datetime.utcnow() |
| 87 | + |
| 88 | + if self.shutting_down: return None |
| 89 | + |
| 90 | + elapsed_time = (end_time - start_time) |
| 91 | + if elapsed_time < self.heartbeat_period: |
| 92 | + time.sleep( (self.heartbeat_period - elapsed_time).total_seconds() ) |
| 93 | + |
| 94 | + def send_heartbeat(self, lock): |
| 95 | + """ |
| 96 | + Renews the lease for the given lock |
| 97 | + |
| 98 | + :param lock: DynamoDBLock instance - that needs its lease to be renewed |
| 99 | + """ |
| 100 | + with lock.lock: |
| 101 | + try: |
| 102 | + # TODO actual impl |
| 103 | + print |
| 104 | + # update |
| 105 | + # table |
| 106 | + # keys |
| 107 | + # SET #ld = :ld, #rvn = :newRvn |
| 108 | + # attribute_exists(#pk) AND attribute_exists(#sk) AND #rvn = :rvn AND #on = :on |
| 109 | + # attribute-names |
| 110 | + # attribute-values |
| 111 | + except botocore.exceptions.ClientError as e: |
| 112 | + if e.response['Error']['Code'] == 'ConditionalCheckFailedException': |
| 113 | + # TODO: need to handle the case where the lock was "stolen" by someone else... |
| 114 | + print |
| 115 | + else: |
| 116 | + print |
| 117 | + except: |
| 118 | + print |
| 119 | + |
| 120 | + |
| 121 | + def close(self): |
| 122 | + """ |
| 123 | + Shuts down the background thread and releases all locks. |
| 124 | + """ |
| 125 | + self.shutting_down = True |
| 126 | + self.background_thread.join() |
| 127 | + self.release_all_locks() |
| 128 | + |
| 129 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 130 | + """ |
| 131 | + Calls the close() method |
| 132 | + """ |
| 133 | + self.close() |
| 134 | + |
| 135 | + def acquire_lock(self, |
| 136 | + partition_key, |
| 137 | + sort_key = None, |
| 138 | + retry_period = datetime.timedelta(seconds=2), |
| 139 | + retry_timeout = datetime.timedelta(seconds=5), |
| 140 | + additional_attributes = {} |
| 141 | + ): |
| 142 | + """""" |
| 143 | + |
| 144 | + def release_lock(self, |
| 145 | + lock, |
| 146 | + best_effort = False |
| 147 | + ): |
| 148 | + """""" |
| 149 | + with lock.lock: |
| 150 | + try: |
| 151 | + del self.locks[lock.uuid] |
| 152 | + # TODO actual impl |
| 153 | + # delete |
| 154 | + # table |
| 155 | + # keys |
| 156 | + # attribute_exists(#pk) AND attribute_exists(#sk) AND #rvn = :rvn AND #on = :on |
| 157 | + # attribute-names |
| 158 | + # attribute-values |
| 159 | + except botocore.exceptions.ClientError as e: |
| 160 | + if e.response['Error']['Code'] == 'ConditionalCheckFailedException': |
| 161 | + # TODO: need to handle the case where the lock was "stolen" by someone else... |
| 162 | + print |
| 163 | + else: |
| 164 | + # if not best_effort: raise error |
| 165 | + print |
| 166 | + except: |
| 167 | + print |
| 168 | + |
| 169 | + |
| 170 | + # TODO: make this private? |
| 171 | + def release_all_locks(self): |
| 172 | + """""" |
| 173 | + for lock in self.locks.itervalues(): |
| 174 | + self.release_lock(lock) |
| 175 | + |
| 176 | + # get_lock |
| 177 | + # get_all_locks |
| 178 | + |
| 179 | + |
| 180 | + |
| 181 | +class DynamoDBLock: |
| 182 | + # TODO |
| 183 | + """ |
| 184 | + class comment |
| 185 | + """ |
| 186 | + |
| 187 | + def __init__(self, |
| 188 | + partition_key, |
| 189 | + sort_key = None, |
| 190 | + retry_period = datetime.timedelta(seconds=2), |
| 191 | + retry_timeout = datetime.timedelta(seconds=5), |
| 192 | + additional_attributes = {} |
| 193 | + ): |
| 194 | + # TODO |
| 195 | + """""" |
0 commit comments