-
-
Notifications
You must be signed in to change notification settings - Fork 64
feat(cdc) Actually load the csv file and import snapshot #407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
331899d
07ef2db
8c1d8af
02515de
a270659
9505713
6e661e4
484f30d
d088cf5
46ea320
bb4113e
5e9a528
3f2dee8
2ad23c8
05e2966
160b193
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,92 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from abc import ABC, abstractmethod | ||
| from typing import Any, Mapping, Optional, Sequence, Type | ||
|
|
||
| from snuba.processor import MessageProcessor | ||
| from snuba.writer import WriterTableRow | ||
|
|
||
| KAFKA_ONLY_PARTITION = 0 # CDC only works with single partition topics. So partition must be 0 | ||
|
|
||
|
|
||
| class CdcMessageRow(ABC): | ||
| """ | ||
| Takes care of the data transformation from WAL to clickhouse and from | ||
| bulk load to clickhouse. The goal is to keep all these transformation | ||
| function in the same place because they ultimately have to be consistent | ||
| with the Clickhouse schema. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def from_wal( | ||
| cls, | ||
| offset: int, | ||
| columnnames: Sequence[str], | ||
| columnvalues: Sequence[Any], | ||
| ) -> CdcMessageRow: | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def from_bulk( | ||
| cls, | ||
| row: Mapping[str, Any], | ||
| ) -> CdcMessageRow: | ||
| raise NotImplementedError | ||
|
|
||
| @abstractmethod | ||
| def to_clickhouse(self) -> WriterTableRow: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class CdcProcessor(MessageProcessor): | ||
|
|
||
| def __init__(self, pg_table): | ||
| def __init__(self, pg_table: str, message_row_class: Type[CdcMessageRow]): | ||
| self.pg_table = pg_table | ||
| self._message_row_class = message_row_class | ||
|
|
||
| def _process_begin(self, offset): | ||
| def _process_begin(self, offset: int): | ||
| pass | ||
|
|
||
| def _process_commit(self, offset): | ||
| def _process_commit(self, offset: int): | ||
| pass | ||
|
|
||
| def _process_insert(self, offset, columnnames, columnvalues): | ||
| pass | ||
| def _process_insert(self, | ||
| offset: int, | ||
| columnnames: Sequence[str], | ||
| columnvalues: Sequence[Any], | ||
| ) -> Optional[WriterTableRow]: | ||
| return self._message_row_class.from_wal( | ||
| offset, | ||
| columnnames, | ||
| columnvalues | ||
| ).to_clickhouse() | ||
|
|
||
| def _process_update(self, offset, key, columnnames, columnvalues): | ||
| pass | ||
| def _process_update(self, | ||
| offset: int, | ||
| key: Mapping[str, Any], | ||
| columnnames: Sequence[str], | ||
| columnvalues: Sequence[Any], | ||
| ) -> Optional[WriterTableRow]: | ||
| old_key = dict(zip(key['keynames'], key['keyvalues'])) | ||
| new_key = { | ||
| key: columnvalues[columnnames.index(key)] | ||
| for key | ||
| in key['keynames'] | ||
| } | ||
| # We cannot support a change in the identity of the record | ||
| # clickhouse will use the identity column to find rows to merge. | ||
| # if we change it, merging won't work. | ||
| assert old_key == new_key, 'Changing Primary Key is not supported.' | ||
| return self._message_row_class.from_wal( | ||
| offset, | ||
| columnnames, | ||
| columnvalues | ||
| ).to_clickhouse() | ||
|
|
||
| def _process_delete(self, offset, key): | ||
| def _process_delete(self, | ||
| offset: int, | ||
| key: Mapping[str, Any], | ||
| ) -> Optional[WriterTableRow]: | ||
| pass | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this different than the other
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was one but I cannot remember. Let me try to do it again, and then it should come back.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh yes. It was because, while for updates, we can simply create a new GroupedMessageRow with the new value since that value includes the identity key, for deletion this is more complex because we have to create a GroupedMessageRow with only the identity key populated. The rest is None and it is not in the WAL message. |
||
|
|
||
| def process_message(self, value, metadata): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,54 +1,99 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from datetime import datetime | ||
| from typing import Any, Mapping, Optional, Sequence | ||
|
|
||
| from dataclasses import dataclass | ||
| from dateutil.parser import parse as dateutil_parse | ||
| from snuba.datasets.cdc.cdcprocessors import CdcProcessor | ||
| from snuba.datasets.cdc.cdcprocessors import CdcProcessor, CdcMessageRow | ||
| from snuba.writer import WriterTableRow | ||
|
|
||
|
|
||
| class GroupedMessageProcessor(CdcProcessor): | ||
| @dataclass(frozen=True) | ||
| class GroupMessageRecord: | ||
| status: int | ||
| last_seen: datetime | ||
| first_seen: datetime | ||
| active_at: Optional[datetime] = None | ||
| first_release_id: Optional[int] = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good, thanks |
||
|
|
||
| def __init__(self, postgres_table): | ||
| super(GroupedMessageProcessor, self).__init__( | ||
| pg_table=postgres_table, | ||
| ) | ||
|
|
||
| def __build_record(self, offset, columnnames, columnvalues): | ||
| raw_data = dict(zip(columnnames, columnvalues)) | ||
| output = { | ||
| 'offset': offset, | ||
| 'id': raw_data['id'], | ||
| 'record_deleted': 0, | ||
| 'status': raw_data['status'], | ||
| 'last_seen': dateutil_parse(raw_data['last_seen']), | ||
| 'first_seen': dateutil_parse(raw_data['first_seen']), | ||
| 'active_at': dateutil_parse(raw_data['active_at']), | ||
| 'first_release_id': raw_data['first_release_id'], | ||
| } | ||
| @dataclass(frozen=True) | ||
| class GroupedMessageRow(CdcMessageRow): | ||
| offset: Optional[int] | ||
| id: int | ||
| record_deleted: bool | ||
| record_content: Optional[GroupMessageRecord] | ||
|
|
||
| return output | ||
| @classmethod | ||
| def from_wal(cls, | ||
| offset: int, | ||
| columnnames: Sequence[str], | ||
| columnvalues: Sequence[Any], | ||
| ) -> GroupedMessageRow: | ||
| raw_data = dict(zip(columnnames, columnvalues)) | ||
| return GroupedMessageRow( | ||
| offset=offset, | ||
| id=raw_data['id'], | ||
| record_deleted=False, | ||
| record_content=GroupMessageRecord( | ||
| status=raw_data['status'], | ||
| last_seen=dateutil_parse(raw_data['last_seen']), | ||
| first_seen=dateutil_parse(raw_data['first_seen']), | ||
| active_at=dateutil_parse(raw_data['active_at']), | ||
| first_release_id=raw_data['first_release_id'], | ||
| ) | ||
| ) | ||
|
|
||
| def _process_insert(self, offset, columnnames, columnvalues): | ||
| return self.__build_record(offset, columnnames, columnvalues) | ||
| @classmethod | ||
| def from_bulk(cls, | ||
| row: Mapping[str, Any], | ||
| ) -> GroupedMessageRow: | ||
| return GroupedMessageRow( | ||
| offset=None, | ||
| id=int(row['id']), | ||
| record_deleted=False, | ||
| record_content=GroupMessageRecord( | ||
| status=int(row['status']), | ||
| last_seen=dateutil_parse(row['last_seen']), | ||
| first_seen=dateutil_parse(row['first_seen']), | ||
| active_at=dateutil_parse(row['active_at']), | ||
| first_release_id=int(row['first_release_id']) if row['first_release_id'] else None, | ||
| ) | ||
| ) | ||
|
|
||
| def _process_delete(self, offset, key): | ||
| key_names = key['keynames'] | ||
| key_values = key['keyvalues'] | ||
| id = key_values[key_names.index('id')] | ||
| def to_clickhouse(self) -> WriterTableRow: | ||
| record = self.record_content | ||
| return { | ||
| 'offset': offset, | ||
| 'id': id, | ||
| 'record_deleted': 1, | ||
| 'status': None, | ||
| 'last_seen': None, | ||
| 'first_seen': None, | ||
| 'active_at': None, | ||
| 'first_release_id': None, | ||
| 'offset': self.offset if self.offset is not None else 0, | ||
| 'id': self.id, | ||
| 'record_deleted': 1 if self.record_deleted else 0, | ||
| 'status': None if not record else record.status, | ||
| 'last_seen': None if not record else record.last_seen, | ||
| 'first_seen': None if not record else record.first_seen, | ||
| 'active_at': None if not record else record.active_at, | ||
| 'first_release_id': None if not record else record.first_release_id, | ||
| } | ||
|
|
||
| def _process_update(self, offset, key, columnnames, columnvalues): | ||
| new_id = columnvalues[columnnames.index('id')] | ||
|
|
||
| class GroupedMessageProcessor(CdcProcessor): | ||
|
|
||
| def __init__(self, postgres_table): | ||
| super(GroupedMessageProcessor, self).__init__( | ||
| pg_table=postgres_table, | ||
| message_row_class=GroupedMessageRow, | ||
| ) | ||
|
|
||
| def _process_delete(self, | ||
| offset: int, | ||
| key: Mapping[str, Any], | ||
| ) -> Optional[WriterTableRow]: | ||
| key_names = key['keynames'] | ||
| key_values = key['keyvalues'] | ||
| old_id = key_values[key_names.index('id')] | ||
| # We cannot support a change in the identity of the record | ||
| # clickhouse will use the identity column to find rows to merge. | ||
| # if we change it, merging won't work. | ||
| assert old_id == new_id, 'Changing Primary Key is not supported.' | ||
| return self.__build_record(offset, columnnames, columnvalues) | ||
| id = key_values[key_names.index('id')] | ||
| return GroupedMessageRow( | ||
| offset=offset, | ||
| id=id, | ||
| record_deleted=True, | ||
| record_content=None | ||
| ).to_clickhouse() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is helpful, I didn't realize these are optional. Why does this differ than return type of
CdcMessageRow.to_clickhouse()? Shouldn't they be equivalent?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They differ because I wanted to let the dataset specific class decide whether to ignore a WAL row. For example we could have a dataset without deletions. In that case WAL deletion should produce no effect and just return None. Essentially we let the dataset a bit more autonomy on how to interpret the WAL if they want to override these methods.
On the other hand, when we have a valid CdcMessageRow instantiated, that has to be serialized into a write to clickhouse. I cannot think about a legit case where we would be able to instantiate CdcMessageRow but we may decide to return None