Skip to content

Commit 473de8e

Browse files
authored
Merge pull request #2002 from dhermes/fix-1524
Making HappyBase create_table() atomic.
2 parents 8e3caf9 + c43fc06 commit 473de8e

File tree

6 files changed

+86
-61
lines changed

6 files changed

+86
-61
lines changed

gcloud/bigtable/column_family.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,20 @@ def __eq__(self, other):
248248
def __ne__(self, other):
249249
return not self.__eq__(other)
250250

251-
def create(self):
252-
"""Create this column family."""
251+
def to_pb(self):
252+
"""Converts the column family to a protobuf.
253+
254+
:rtype: :class:`.table_v2_pb2.ColumnFamily`
255+
:returns: The converted current object.
256+
"""
253257
if self.gc_rule is None:
254-
column_family = table_v2_pb2.ColumnFamily()
258+
return table_v2_pb2.ColumnFamily()
255259
else:
256-
column_family = table_v2_pb2.ColumnFamily(
257-
gc_rule=self.gc_rule.to_pb())
260+
return table_v2_pb2.ColumnFamily(gc_rule=self.gc_rule.to_pb())
261+
262+
def create(self):
263+
"""Create this column family."""
264+
column_family = self.to_pb()
258265
request_pb = table_admin_v2_pb2.ModifyColumnFamiliesRequest(
259266
name=self._table.name)
260267
request_pb.modifications.add(
@@ -276,11 +283,7 @@ def update(self):
276283
Only the GC rule can be updated. By changing the column family ID,
277284
you will simply be referring to a different column family.
278285
"""
279-
if self.gc_rule is None:
280-
column_family = table_v2_pb2.ColumnFamily()
281-
else:
282-
column_family = table_v2_pb2.ColumnFamily(
283-
gc_rule=self.gc_rule.to_pb())
286+
column_family = self.to_pb()
284287
request_pb = table_admin_v2_pb2.ModifyColumnFamiliesRequest(
285288
name=self._table.name)
286289
request_pb.modifications.add(

gcloud/bigtable/happybase/connection.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -296,15 +296,6 @@ def create_table(self, name, families):
296296
The only column family options from HappyBase that are able to be
297297
used with Cloud Bigtable are ``max_versions`` and ``time_to_live``.
298298
299-
.. note::
300-
301-
This method is **not** atomic. The Cloud Bigtable API separates
302-
the creation of a table from the creation of column families. Thus
303-
this method needs to send 1 request for the table creation and 1
304-
request for each column family. If any of these fails, the method
305-
will fail, but the progress made towards completion cannot be
306-
rolled back.
307-
308299
Values in ``families`` represent column family options. In HappyBase,
309300
these are dictionaries, corresponding to the ``ColumnDescriptor``
310301
structure in the Thrift API. The accepted keys are:
@@ -354,19 +345,18 @@ def create_table(self, name, families):
354345
# Create table instance and then make API calls.
355346
name = self._table_name(name)
356347
low_level_table = _LowLevelTable(name, self._instance)
348+
column_families = (
349+
low_level_table.column_family(column_family_name, gc_rule=gc_rule)
350+
for column_family_name, gc_rule in six.iteritems(gc_rule_dict)
351+
)
357352
try:
358-
low_level_table.create()
353+
low_level_table.create(column_families=column_families)
359354
except face.NetworkError as network_err:
360355
if network_err.code == interfaces.StatusCode.ALREADY_EXISTS:
361356
raise AlreadyExists(name)
362357
else:
363358
raise
364359

365-
for column_family_name, gc_rule in gc_rule_dict.items():
366-
column_family = low_level_table.column_family(
367-
column_family_name, gc_rule=gc_rule)
368-
column_family.create()
369-
370360
def delete_table(self, name, disable=False):
371361
"""Delete the specified table.
372362

gcloud/bigtable/happybase/test_connection.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -370,14 +370,11 @@ def make_table(*args, **kwargs):
370370
col_fam_created.sort(key=operator.attrgetter('column_family_id'))
371371
self.assertEqual(col_fam_created[0].column_family_id, col_fam1)
372372
self.assertEqual(col_fam_created[0].gc_rule, mock_gc_rule)
373-
self.assertEqual(col_fam_created[0].create_calls, 1)
374373
self.assertEqual(col_fam_created[1].column_family_id, col_fam2)
375374
self.assertEqual(col_fam_created[1].gc_rule, mock_gc_rule)
376-
self.assertEqual(col_fam_created[1].create_calls, 1)
377375
self.assertEqual(col_fam_created[2].column_family_id,
378376
col_fam3.decode('utf-8'))
379377
self.assertEqual(col_fam_created[2].gc_rule, mock_gc_rule)
380-
self.assertEqual(col_fam_created[2].create_calls, 1)
381378

382379
def test_create_table_bad_type(self):
383380
instance = _Instance() # Avoid implicit environ check.
@@ -696,10 +693,6 @@ class _MockLowLevelColumnFamily(object):
696693
def __init__(self, column_family_id, gc_rule=None):
697694
self.column_family_id = column_family_id
698695
self.gc_rule = gc_rule
699-
self.create_calls = 0
700-
701-
def create(self):
702-
self.create_calls += 1
703696

704697

705698
class _MockLowLevelTable(object):
@@ -715,12 +708,11 @@ def __init__(self, *args, **kwargs):
715708
def delete(self):
716709
self.delete_calls += 1
717710

718-
def create(self):
711+
def create(self, column_families=()):
719712
self.create_calls += 1
713+
self.col_fam_created.extend(column_families)
720714
if self.create_error:
721715
raise self.create_error
722716

723717
def column_family(self, column_family_id, gc_rule=None):
724-
result = _MockLowLevelColumnFamily(column_family_id, gc_rule=gc_rule)
725-
self.col_fam_created.append(result)
726-
return result
718+
return _MockLowLevelColumnFamily(column_family_id, gc_rule=gc_rule)

gcloud/bigtable/table.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
bigtable_pb2 as data_messages_v2_pb2)
2020
from gcloud.bigtable._generated_v2 import (
2121
bigtable_table_admin_pb2 as table_admin_messages_v2_pb2)
22+
from gcloud.bigtable._generated_v2 import (
23+
table_pb2 as table_v2_pb2)
2224
from gcloud.bigtable.column_family import _gc_rule_from_pb
2325
from gcloud.bigtable.column_family import ColumnFamily
2426
from gcloud.bigtable.row import AppendRow
@@ -32,14 +34,9 @@ class Table(object):
3234
3335
.. note::
3436
35-
We don't define any properties on a table other than the name. As
36-
the proto says, in a request:
37-
38-
The ``name`` field of the Table and all of its ColumnFamilies must
39-
be left blank, and will be populated in the response.
40-
41-
This leaves only the ``current_operation`` and ``granularity``
42-
fields. The ``current_operation`` is only used for responses while
37+
We don't define any properties on a table other than the name.
38+
The only other fields are ``column_families`` and ``granularity``,
39+
The ``column_families`` are not stored locally and
4340
``granularity`` is an enum with only one value.
4441
4542
We can use a :class:`Table` to:
@@ -52,7 +49,7 @@ class Table(object):
5249
:type table_id: str
5350
:param table_id: The ID of the table.
5451
55-
:type instance: :class:`Cluster <.instance.Instance>`
52+
:type instance: :class:`Instance <.instance.Instance>`
5653
:param instance: The instance that owns the table.
5754
"""
5855

@@ -71,7 +68,7 @@ def name(self):
7168
7269
The table name is of the form
7370
74-
``"projects/../zones/../clusters/../tables/{table_id}"``
71+
``"projects/../instances/../tables/{table_id}"``
7572
7673
:rtype: str
7774
:returns: The table name.
@@ -136,24 +133,14 @@ def __eq__(self, other):
136133
def __ne__(self, other):
137134
return not self.__eq__(other)
138135

139-
def create(self, initial_split_keys=None):
136+
def create(self, initial_split_keys=None, column_families=()):
140137
"""Creates this table.
141138
142-
.. note::
143-
144-
Though a :class:`._generated_v2.table_pb2.Table` is also
145-
allowed (as the ``table`` property) in a create table request, we
146-
do not support it in this method. As mentioned in the
147-
:class:`Table` docstring, the name is the only useful property in
148-
the table proto.
149-
150139
.. note::
151140
152141
A create request returns a
153142
:class:`._generated_v2.table_pb2.Table` but we don't use
154-
this response. The proto definition allows for the inclusion of a
155-
``current_operation`` in the response, but it does not appear that
156-
the Cloud Bigtable API returns any operation.
143+
this response.
157144
158145
:type initial_split_keys: list
159146
:param initial_split_keys: (Optional) List of row keys that will be
@@ -163,15 +150,28 @@ def create(self, initial_split_keys=None):
163150
``"s1"`` and ``"s2"``, three tablets will be
164151
created, spanning the key ranges:
165152
``[, s1)``, ``[s1, s2)``, ``[s2, )``.
153+
154+
:type column_families: list
155+
:param column_families: (Optional) List or other iterable of
156+
:class:`.ColumnFamily` instances.
166157
"""
167-
split_pb = table_admin_messages_v2_pb2.CreateTableRequest.Split
168158
if initial_split_keys is not None:
159+
split_pb = table_admin_messages_v2_pb2.CreateTableRequest.Split
169160
initial_split_keys = [
170161
split_pb(key=key) for key in initial_split_keys]
162+
163+
table_pb = None
164+
if column_families:
165+
table_pb = table_v2_pb2.Table()
166+
for col_fam in column_families:
167+
curr_id = col_fam.column_family_id
168+
table_pb.column_families[curr_id].MergeFrom(col_fam.to_pb())
169+
171170
request_pb = table_admin_messages_v2_pb2.CreateTableRequest(
172171
initial_splits=initial_split_keys or [],
173172
parent=self._instance.name,
174173
table_id=self.table_id,
174+
table=table_pb,
175175
)
176176
client = self._instance._client
177177
# We expect a `._generated_v2.table_pb2.Table`

gcloud/bigtable/test_column_family.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,22 @@ def test___ne__(self):
388388
column_family2 = self._makeOne('column_family_id2', None)
389389
self.assertNotEqual(column_family1, column_family2)
390390

391+
def test_to_pb_no_rules(self):
392+
column_family = self._makeOne('column_family_id', None)
393+
pb_val = column_family.to_pb()
394+
expected = _ColumnFamilyPB()
395+
self.assertEqual(pb_val, expected)
396+
397+
def test_to_pb_with_rule(self):
398+
from gcloud.bigtable.column_family import MaxVersionsGCRule
399+
400+
gc_rule = MaxVersionsGCRule(1)
401+
column_family = self._makeOne('column_family_id', None,
402+
gc_rule=gc_rule)
403+
pb_val = column_family.to_pb()
404+
expected = _ColumnFamilyPB(gc_rule=gc_rule.to_pb())
405+
self.assertEqual(pb_val, expected)
406+
391407
def _create_test_helper(self, gc_rule=None):
392408
from gcloud.bigtable._generated_v2 import (
393409
bigtable_table_admin_pb2 as table_admin_v2_pb2)

gcloud/bigtable/test_table.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def test___ne__(self):
133133
table2 = self._makeOne('table_id2', 'instance2')
134134
self.assertNotEqual(table1, table2)
135135

136-
def _create_test_helper(self, initial_split_keys):
136+
def _create_test_helper(self, initial_split_keys, column_families=()):
137137
from gcloud._helpers import _to_bytes
138138
from gcloud.bigtable._testing import _FakeStub
139139

@@ -145,10 +145,18 @@ def _create_test_helper(self, initial_split_keys):
145145
splits_pb = [
146146
_CreateTableRequestSplitPB(key=_to_bytes(key))
147147
for key in initial_split_keys or ()]
148+
table_pb = None
149+
if column_families:
150+
table_pb = _TablePB()
151+
for cf in column_families:
152+
cf_pb = table_pb.column_families[cf.column_family_id]
153+
if cf.gc_rule is not None:
154+
cf_pb.gc_rule.MergeFrom(cf.gc_rule.to_pb())
148155
request_pb = _CreateTableRequestPB(
149156
initial_splits=splits_pb,
150157
parent=self.INSTANCE_NAME,
151158
table_id=self.TABLE_ID,
159+
table=table_pb,
152160
)
153161

154162
# Create response_pb
@@ -161,7 +169,8 @@ def _create_test_helper(self, initial_split_keys):
161169
expected_result = None # create() has no return value.
162170

163171
# Perform the method and check the result.
164-
result = table.create(initial_split_keys=initial_split_keys)
172+
result = table.create(initial_split_keys=initial_split_keys,
173+
column_families=column_families)
165174
self.assertEqual(result, expected_result)
166175
self.assertEqual(stub.method_calls, [(
167176
'CreateTable',
@@ -177,6 +186,21 @@ def test_create_with_split_keys(self):
177186
initial_split_keys = [b's1', b's2']
178187
self._create_test_helper(initial_split_keys)
179188

189+
def test_create_with_column_families(self):
190+
from gcloud.bigtable.column_family import ColumnFamily
191+
from gcloud.bigtable.column_family import MaxVersionsGCRule
192+
193+
cf_id1 = 'col-fam-id1'
194+
cf1 = ColumnFamily(cf_id1, None)
195+
cf_id2 = 'col-fam-id2'
196+
gc_rule = MaxVersionsGCRule(42)
197+
cf2 = ColumnFamily(cf_id2, None, gc_rule=gc_rule)
198+
199+
initial_split_keys = None
200+
column_families = [cf1, cf2]
201+
self._create_test_helper(initial_split_keys,
202+
column_families=column_families)
203+
180204
def _list_column_families_helper(self):
181205
from gcloud.bigtable._testing import _FakeStub
182206

0 commit comments

Comments
 (0)