Skip to content

Commit 800c1d7

Browse files
authored
Merge branch 'main' into feature/rand-event
2 parents 547c05d + 558444e commit 800c1d7

File tree

14 files changed

+280
-80
lines changed

14 files changed

+280
-80
lines changed

.github/workflows/pytest.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ jobs:
2828
run: |
2929
docker compose create
3030
docker compose start
31-
# Wait for the services to accept connections,
32-
# TODO: do that smarter, poll connection attempt until it succeeds
33-
sleep 30
31+
echo "wait mysql server"
32+
33+
while :
34+
do
35+
if mysql -h 127.0.0.1 --user=root --execute "SELECT version();" 2>&1 >/dev/null && mysql -h 127.0.0.1 --port=3307 --user=root --execute "SELECT version();" 2>&1 >/dev/null; then
36+
break
37+
fi
38+
sleep 1
39+
done
40+
41+
echo "run pytest"
3442
3543
- name: Install dependencies
3644
run: |

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ python-mysql-replication
44
<a href="https://travis-ci.org/noplay/python-mysql-replication"><img src="https://travis-ci.org/noplay/python-mysql-replication.svg?branch=master"></a>&nbsp;
55
<a href="https://pypi.python.org/pypi/mysql-replication"><img src="http://img.shields.io/pypi/dm/mysql-replication.svg"></a>
66

7-
Pure Python Implementation of MySQL replication protocol build on top of PyMYSQL. This allow you to receive event like insert, update, delete with their datas and raw SQL queries.
7+
Pure Python Implementation of MySQL replication protocol build on top of PyMYSQL. This allows you to receive event like insert, update, delete with their datas and raw SQL queries.
88

99
Use cases
1010
===========
@@ -56,6 +56,11 @@ Limitations
5656

5757
https://python-mysql-replication.readthedocs.org/en/latest/limitations.html
5858

59+
Featured Books
60+
=============
61+
62+
[Data Pipelines Pocket Reference](https://www.oreilly.com/library/view/data-pipelines-pocket/9781492087823/) (by James Densmore, O'Reilly): Introduced and exemplified in Chapter 4: Data Ingestion: Extracting Data.
63+
5964
Projects using this library
6065
===========================
6166

docker-compose-test.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
version: '3.2'
2+
services:
3+
percona-5.7:
4+
platform: linux/amd64
5+
image: percona:5.7
6+
environment:
7+
MYSQL_ALLOW_EMPTY_PASSWORD: true
8+
MYSQL_DATABASE: pymysqlreplication_test
9+
ports:
10+
- 3306:3306
11+
command: mysqld --log-bin=mysql-bin.log --server-id 1 --binlog-format=row --gtid_mode=on --enforce-gtid-consistency=on --log_slave_updates
12+
restart: always
13+
networks:
14+
- default
15+
16+
percona-5.7-ctl:
17+
image: percona:5.7
18+
environment:
19+
MYSQL_ALLOW_EMPTY_PASSWORD: true
20+
MYSQL_DATABASE: pymysqlreplication_test
21+
ports:
22+
- 3307:3307
23+
command: mysqld --log-bin=mysql-bin.log --server-id 1 --binlog-format=row --gtid_mode=on --enforce-gtid-consistency=on --log_slave_updates -P 3307
24+
25+
pymysqlreplication:
26+
build:
27+
context: .
28+
dockerfile: test.Dockerfile
29+
args:
30+
BASE_IMAGE: python:3.11-alpine
31+
MYSQL_5_7: percona-5.7
32+
MYSQL_5_7_CTL: percona-5.7-ctl
33+
34+
command:
35+
- /bin/sh
36+
- -ce
37+
- |
38+
echo "wait mysql server"
39+
40+
while :
41+
do
42+
if mysql -h percona-5.7 --user=root --execute "USE pymysqlreplication_test;" 2>&1 >/dev/null && mysql -h percona-5.7-ctl --port=3307 --user=root --execute "USE pymysqlreplication_test;" 2>&1 >/dev/null; then
43+
break
44+
fi
45+
sleep 1
46+
done
47+
48+
echo "run pytest"
49+
pytest -k "not test_no_trailing_rotate_event and not test_end_log_pos"
50+
51+
working_dir: /pymysqlreplication
52+
networks:
53+
- default
54+
depends_on:
55+
- percona-5.7
56+
- percona-5.7-ctl
57+
58+
networks:
59+
default: {}

docker-compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,17 @@ services:
1515
ports:
1616
- 3307:3307
1717
command: mysqld --log-bin=mysql-bin.log --server-id 1 --binlog-format=row --gtid_mode=on --enforce-gtid-consistency=on --log_slave_updates -P 3307
18+
19+
mariadb-10.6:
20+
image: mariadb:10.6
21+
environment:
22+
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
23+
ports:
24+
- "3308:3306"
25+
command: |
26+
--server-id=1
27+
--default-authentication-plugin=mysql_native_password
28+
--log-bin=master-bin
29+
--binlog-format=row
30+
--log-slave-updates=on
31+

docs/developement.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ When it's possible we have an unit test.
2323
*pymysqlreplication/tests/* contains the test suite. The test suite
2424
use the standard *unittest* Python module.
2525

26-
**Be carefull** tests will reset the binary log of your MySQL server.
26+
**Be careful** tests will reset the binary log of your MySQL server.
2727

2828
Make sure you have the following configuration set in your mysql config file (usually my.cnf on development env):
2929

examples/mariadb_gtid/read_event.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pymysql
22

33
from pymysqlreplication import BinLogStreamReader, gtid
4-
from pymysqlreplication.event import GtidEvent, RotateEvent, MariadbGtidEvent, QueryEvent
4+
from pymysqlreplication.event import GtidEvent, RotateEvent, MariadbGtidEvent, QueryEvent,MariadbAnnotateRowsEvent
55
from pymysqlreplication.row_event import WriteRowsEvent, UpdateRowsEvent, DeleteRowsEvent
66

77
MARIADB_SETTINGS = {
@@ -65,10 +65,12 @@ def query_server_id(self):
6565
RotateEvent,
6666
WriteRowsEvent,
6767
UpdateRowsEvent,
68-
DeleteRowsEvent
68+
DeleteRowsEvent,
69+
MariadbAnnotateRowsEvent
6970
],
7071
auto_position=gtid,
71-
is_mariadb=True
72+
is_mariadb=True,
73+
annotate_rows_event=True
7274
)
7375

7476
print('Starting reading events from GTID ', gtid)

pymysqlreplication/binlogstream.py

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
QueryEvent, RotateEvent, FormatDescriptionEvent,
1515
XidEvent, GtidEvent, StopEvent, XAPrepareEvent,
1616
BeginLoadQueryEvent, ExecuteLoadQueryEvent,
17-
HeartbeatLogEvent, NotImplementedEvent,
18-
MariadbGtidEvent, RandEvent)
17+
HeartbeatLogEvent, NotImplementedEvent, MariadbGtidEvent,
18+
MariadbAnnotateRowsEvent, RandEvent)
1919
from .exceptions import BinLogNotEnabled
2020
from .row_event import (
2121
UpdateRowsEvent, WriteRowsEvent, DeleteRowsEvent, TableMapEvent)
@@ -142,6 +142,7 @@ def __init__(self, connection_settings, server_id,
142142
fail_on_table_metadata_unavailable=False,
143143
slave_heartbeat=None,
144144
is_mariadb=False,
145+
annotate_rows_event=False,
145146
ignore_decode_errors=False):
146147
"""
147148
Attributes:
@@ -167,7 +168,8 @@ def __init__(self, connection_settings, server_id,
167168
skip_to_timestamp: Ignore all events until reaching specified
168169
timestamp.
169170
report_slave: Report slave in SHOW SLAVE HOSTS.
170-
slave_uuid: Report slave_uuid in SHOW SLAVE HOSTS.
171+
slave_uuid: Report slave_uuid or replica_uuid in SHOW SLAVE HOSTS(MySQL 8.0.21-) or
172+
SHOW REPLICAS(MySQL 8.0.22+) depends on your MySQL version.
171173
fail_on_table_metadata_unavailable: Should raise exception if we
172174
can't get table information on
173175
row_events
@@ -179,6 +181,8 @@ def __init__(self, connection_settings, server_id,
179181
for semantics
180182
is_mariadb: Flag to indicate it's a MariaDB server, used with auto_position
181183
to point to Mariadb specific GTID.
184+
annotate_rows_event: Parameter value to enable annotate rows event in mariadb,
185+
used with 'is_mariadb'
182186
ignore_decode_errors: If true, any decode errors encountered
183187
when reading column data will be ignored.
184188
"""
@@ -220,6 +224,7 @@ def __init__(self, connection_settings, server_id,
220224
self.auto_position = auto_position
221225
self.skip_to_timestamp = skip_to_timestamp
222226
self.is_mariadb = is_mariadb
227+
self.__annotate_rows_event = annotate_rows_event
223228

224229
if end_log_pos:
225230
self.is_past_end_log_pos = False
@@ -302,7 +307,7 @@ def __connect_to_stream(self):
302307

303308
if self.slave_uuid:
304309
cur = self._stream_connection.cursor()
305-
cur.execute("set @slave_uuid= '%s'" % self.slave_uuid)
310+
cur.execute("SET @slave_uuid = %s, @replica_uuid = %s", (self.slave_uuid, self.slave_uuid))
306311
cur.close()
307312

308313
if self.slave_heartbeat:
@@ -332,67 +337,39 @@ def __connect_to_stream(self):
332337
self._register_slave()
333338

334339
if not self.auto_position:
335-
# only when log_file and log_pos both provided, the position info is
336-
# valid, if not, get the current position from master
337-
if self.log_file is None or self.log_pos is None:
338-
cur = self._stream_connection.cursor()
339-
cur.execute("SHOW MASTER STATUS")
340-
master_status = cur.fetchone()
341-
if master_status is None:
342-
raise BinLogNotEnabled()
343-
self.log_file, self.log_pos = master_status[:2]
344-
cur.close()
345-
346-
prelude = struct.pack('<i', len(self.log_file) + 11) \
347-
+ bytes(bytearray([COM_BINLOG_DUMP]))
348-
349-
if self.__resume_stream:
350-
prelude += struct.pack('<I', self.log_pos)
351-
else:
352-
prelude += struct.pack('<I', 4)
353-
354-
flags = 0
355-
if not self.__blocking:
356-
flags |= 0x01 # BINLOG_DUMP_NON_BLOCK
357-
prelude += struct.pack('<H', flags)
358-
359-
prelude += struct.pack('<I', self.__server_id)
360-
prelude += self.log_file.encode()
361-
else:
362340
if self.is_mariadb:
363-
# https://mariadb.com/kb/en/5-slave-registration/
364-
cur = self._stream_connection.cursor()
365-
cur.execute("SET @slave_connect_state='%s'" % self.auto_position)
366-
cur.execute("SET @slave_gtid_strict_mode=1")
367-
cur.execute("SET @slave_gtid_ignore_duplicates=0")
368-
cur.close()
369-
370-
# https://mariadb.com/kb/en/com_binlog_dump/
371-
header_size = (
372-
4 + # binlog pos
373-
2 + # binlog flags
374-
4 + # slave server_id,
375-
4 # requested binlog file name , set it to empty
376-
)
377-
378-
prelude = struct.pack('<i', header_size) + bytes(bytearray([COM_BINLOG_DUMP]))
379-
380-
# binlog pos
381-
prelude += struct.pack('<i', 4)
341+
prelude = self.__set_mariadb_settings()
342+
else:
343+
# only when log_file and log_pos both provided, the position info is
344+
# valid, if not, get the current position from master
345+
if self.log_file is None or self.log_pos is None:
346+
cur = self._stream_connection.cursor()
347+
cur.execute("SHOW MASTER STATUS")
348+
master_status = cur.fetchone()
349+
if master_status is None:
350+
raise BinLogNotEnabled()
351+
self.log_file, self.log_pos = master_status[:2]
352+
cur.close()
353+
354+
prelude = struct.pack('<i', len(self.log_file) + 11) \
355+
+ bytes(bytearray([COM_BINLOG_DUMP]))
356+
357+
if self.__resume_stream:
358+
prelude += struct.pack('<I', self.log_pos)
359+
else:
360+
prelude += struct.pack('<I', 4)
382361

383362
flags = 0
363+
384364
if not self.__blocking:
385365
flags |= 0x01 # BINLOG_DUMP_NON_BLOCK
386-
387-
# binlog flags
388366
prelude += struct.pack('<H', flags)
389367

390-
# server id (4 bytes)
391368
prelude += struct.pack('<I', self.__server_id)
392-
393-
# empty_binlog_name (4 bytes)
394-
prelude += b'\0\0\0\0'
395-
369+
prelude += self.log_file.encode()
370+
else:
371+
if self.is_mariadb:
372+
prelude = self.__set_mariadb_settings()
396373
else:
397374
# Format for mysql packet master_auto_position
398375
#
@@ -474,6 +451,48 @@ def __connect_to_stream(self):
474451
self._stream_connection._next_seq_id = 1
475452
self.__connected_stream = True
476453

454+
def __set_mariadb_settings(self):
455+
# https://mariadb.com/kb/en/5-slave-registration/
456+
cur = self._stream_connection.cursor()
457+
if self.auto_position != None :
458+
cur.execute("SET @slave_connect_state='%s'" % self.auto_position)
459+
cur.execute("SET @slave_gtid_strict_mode=1")
460+
cur.execute("SET @slave_gtid_ignore_duplicates=0")
461+
cur.close()
462+
463+
# https://mariadb.com/kb/en/com_binlog_dump/
464+
header_size = (
465+
4 + # binlog pos
466+
2 + # binlog flags
467+
4 + # slave server_id,
468+
4 # requested binlog file name , set it to empty
469+
)
470+
471+
prelude = struct.pack('<i', header_size) + bytes(bytearray([COM_BINLOG_DUMP]))
472+
473+
# binlog pos
474+
prelude += struct.pack('<i', 4)
475+
476+
flags = 0
477+
478+
# Enable annotate rows event
479+
if self.__annotate_rows_event:
480+
flags |= 0x02 # BINLOG_SEND_ANNOTATE_ROWS_EVENT
481+
482+
if not self.__blocking:
483+
flags |= 0x01 # BINLOG_DUMP_NON_BLOCK
484+
485+
# binlog flags
486+
prelude += struct.pack('<H', flags)
487+
488+
# server id (4 bytes)
489+
prelude += struct.pack('<I', self.__server_id)
490+
491+
# empty_binlog_name (4 bytes)
492+
prelude += b'\0\0\0\0'
493+
494+
return prelude
495+
477496
def fetchone(self):
478497
while True:
479498
if self.end_log_pos and self.is_past_end_log_pos:
@@ -602,6 +621,7 @@ def _allowed_event_list(self, only_events, ignored_events,
602621
HeartbeatLogEvent,
603622
NotImplementedEvent,
604623
MariadbGtidEvent,
624+
MariadbAnnotateRowsEvent,
605625
RandEvent
606626
))
607627
if ignored_events is not None:

pymysqlreplication/bitmap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
2020
]
2121

22-
# Calculate totol bit counts in a bitmap
22+
# Calculate total bit counts in a bitmap
2323
def BitCount(bitmap):
2424
n = 0
2525
for i in range(0, len(bitmap)):

0 commit comments

Comments
 (0)