├── LICENSE
├── README.md
├── README.zh-CN.md
├── api
└── api.go
├── app
└── server.go
├── build.sh
├── cmd
└── qin_cdc.go
├── config
├── config.go
└── plugin_config.go
├── core
├── input.go
├── meta.go
├── msg.go
├── output.go
├── position.go
└── transform.go
├── docs
├── mysql-to-doris-sample.toml
├── mysql-to-kafka-sample.toml
├── mysql-to-mysql-sample.toml
└── mysql-to-starrocks-sample.toml
├── go.mod
├── go.sum
├── inputs
├── init.go
└── mysql
│ ├── msg.go
│ ├── mysql.go
│ ├── mysql_meta.go
│ ├── mysql_position.go
│ ├── mysql_replication.go
│ └── mysql_utils.go
├── metas
├── mysql_ddl_parse.go
├── routers.go
└── table.go
├── metrics
└── metrics.go
├── outputs
├── doris
│ ├── doris.go
│ ├── doris_meta.go
│ └── doris_utils.go
├── init.go
├── kafka
│ ├── kafka.go
│ ├── kafka_meta.go
│ └── kafka_utils.go
├── mysql
│ ├── mysql.go
│ ├── mysql_meta.go
│ └── mysql_utils.go
└── starrocks
│ ├── starrocks.go
│ ├── starrocks_meta.go
│ └── starrocks_utils.go
├── registry
└── registry.go
├── transforms
├── trans_delete_column.go
├── trans_rename_column.go
├── transforms.go
└── utils.go
└── utils
├── daemon.go
├── file_path.go
├── help.go
├── http.go
├── input_param.go
└── type_cast.go
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## qin-cdc data sync
2 | Simple, efficient, real-time, stable, scalable, highly available, AI, open source
3 |
4 | 
5 | 
6 | [](https://github.com/sqlpub/qin-cdc/releases)
7 |
8 | English | [简体中文](README.zh-CN.md)
9 |
10 | ### Support sync database
11 | #### source
12 | 1. mysql
13 | 2. TODO sqlserver
14 | 3. TODO mongo
15 | 4. TODO oracle
16 |
17 | #### target
18 |
19 | 1. mysql
20 | 2. starrocks
21 | 3. doris
22 | 4. kafka json
23 | 5. TODO kafka canal
24 | 6. kafka aliyun_dts_canal
25 |
26 | ### Quick start
27 | #### 1. Install
28 | [Download](https://github.com/sqlpub/qin-cdc/releases/latest) the latest release and extract it.
29 |
30 | #### 2. Create a synchronization account
31 | ```sql
32 | CREATE USER 'qin_cdc'@'%' IDENTIFIED BY 'xxxxxx';
33 | GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'qin_cdc'@'%';
34 | ```
35 | #### 3. Create a configuration file
36 | mysql-to-starrocks.toml
37 | ```toml
38 | # name is required and must be globally unique when multiple instances are running
39 | name = "mysql2starrocks"
40 |
41 | [input]
42 | type = "mysql"
43 |
44 | [input.config.source]
45 | host = "127.0.0.1"
46 | port = 3306
47 | username = "root"
48 | password = "root"
49 |
50 | [input.config.source.options]
51 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
52 | #server-id = 1001
53 |
54 | [[transforms]]
55 | type = "rename-column"
56 | [transforms.config]
57 | match-schema = "mysql_test"
58 | match-table = "tb1"
59 | columns = ["col_1", "col_2"]
60 | rename-as = ["col_11", "col_22"]
61 |
62 | [[transforms]]
63 | type = "delete-column"
64 | [transforms.config]
65 | match-schema = "mysql_test"
66 | match-table = "tb1"
67 | columns = ["phone"]
68 |
69 | [output]
70 | type = "starrocks"
71 |
72 | [output.config.target]
73 | host = "127.0.0.1"
74 | port = 9030
75 | load-port = 8040 # support fe httpPort:8030 or be httpPort:8040
76 | username = "root"
77 | password = ""
78 |
79 | [input.config.target.options]
80 | batch-size = 1000
81 | batch-interval-ms = 1000
82 | parallel-workers = 4
83 |
84 | [[output.config.routers]]
85 | source-schema = "mysql_test"
86 | source-table = "tb1"
87 | target-schema = "sr_test"
88 | target-table = "ods_tb1"
89 |
90 | [[output.config.routers]]
91 | source-schema = "mysql_test"
92 | source-table = "tb2"
93 | target-schema = "sr_test"
94 | target-table = "ods_tb2"
95 | # mapper column, optional, if empty, same name mapping
96 | # [output.config.routers.columns-mapper]
97 | # source-columns = []
98 | # target-columns = []
99 | ```
100 |
101 | #### 4. View Help
102 | ```shell
103 | [sr@ ~]$ ./qin-cdc-linux-xxxxxx -h
104 | ```
105 |
106 | #### 5. Startup
107 | ```shell
108 | [sr@ ~]$ ./qin-cdc-linux-xxxxxx -config mysql-to-starrocks.toml -log-file mysql2starrocks.log -level info -daemon
109 | ```
110 |
111 | #### 6. View logs
112 | ```shell
113 | [sr@ ~]$ tail -f mysql2starrocks.log
114 | ```
115 |
116 | #### TODO AI functional points
117 | 1. Intelligent data synchronization and migration
118 | 2. Data security and monitoring
119 | 3. Intelligent operation and maintenance management
120 | 4. User experience optimization
121 | 5. Automated data mapping
122 | 6. Natural language processing
123 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | ## qin-cdc data sync
2 | 简单、高效、实时、稳定、可扩展、高可用、AI、开源
3 |
4 | 
5 | 
6 | 
7 | [](https://github.com/sqlpub/qin-cdc/releases)
8 |
9 | ### 支持同步的数据库
10 | #### 源
11 | 1. mysql
12 | 2. TODO sqlserver
13 | 3. TODO mongo
14 | 4. TODO oracle
15 |
16 | #### 目的
17 |
18 | 1. mysql
19 | 2. starrocks
20 | 3. doris
21 | 4. kafka json
22 | 5. TODO kafka canal
23 | 6. kafka aliyun_dts_canal
24 | 7.
25 | ### Quick start
26 | #### 1. 安装
27 | [Download](https://github.com/sqlpub/qin-cdc/releases/latest) the latest release and extract it.
28 |
29 | #### 2. 创建同步账号
30 | ```sql
31 | CREATE USER 'qin_cdc'@'%' IDENTIFIED BY 'xxxxxx';
32 | GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'qin_cdc'@'%';
33 | ```
34 | #### 3. 创建配置文件
35 | mysql-to-starrocks.toml
36 | ```toml
37 | # name 必填,多实例运行时保证全局唯一
38 | name = "mysql2starrocks"
39 |
40 | [input]
41 | type = "mysql"
42 |
43 | [input.config.source]
44 | host = "127.0.0.1"
45 | port = 3306
46 | username = "root"
47 | password = "root"
48 |
49 | [input.config.source.options]
50 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
51 | #server-id = 1001
52 |
53 | [[transforms]]
54 | type = "rename-column"
55 | [transforms.config]
56 | match-schema = "mysql_test"
57 | match-table = "tb1"
58 | columns = ["col_1", "col_2"]
59 | rename-as = ["col_11", "col_22"]
60 |
61 | [[transforms]]
62 | type = "delete-column"
63 | [transforms.config]
64 | match-schema = "mysql_test"
65 | match-table = "tb1"
66 | columns = ["phone"]
67 |
68 | [[transforms]]
69 | type = "mapper-column"
70 | [transforms.config]
71 | match-schema = "mysql_test"
72 | match-table = "tb1"
73 | [transforms.config.mapper]
74 | id = "user_id"
75 | name = "nick_name"
76 |
77 | [output]
78 | type = "starrocks"
79 |
80 | [output.config.target]
81 | host = "127.0.0.1"
82 | port = 9030
83 | load-port = 8040 # support fe httpPort:8030 or be httpPort:8040
84 | username = "root"
85 | password = ""
86 |
87 | [input.config.target.options]
88 | batch-size = 1000
89 | batch-interval-ms = 1000
90 | parallel-workers = 4
91 |
92 | [[output.config.routers]]
93 | source-schema = "sysbenchts"
94 | source-table = "sbtest1"
95 | target-schema = "sr_test"
96 | target-table = "ods_sbtest1"
97 |
98 | [[output.config.routers]]
99 | source-schema = "sysbenchts"
100 | source-table = "sbtest2"
101 | target-schema = "sr_test"
102 | target-table = "ods_sbtest2"
103 | [output.config.routers.columns-mapper]
104 | source-columns = []
105 | target-columns = []
106 | ```
107 |
108 | #### 4. 查看帮助
109 | ```shell
110 | [sr@ ~]$ ./qin-cdc-linux-xxxxxx -h
111 | ```
112 |
113 | #### 5. 启动
114 | ```shell
115 | [sr@ ~]$ ./qin-cdc-linux-xxxxxx -config mysql-to-starrocks.toml -log-file mysql2starrocks.log -level info -daemon
116 | ```
117 |
118 | #### 6. 查看日志
119 | ```shell
120 | [sr@ ~]$ tail -f mysql2starrocks.log
121 | ```
122 |
123 | #### TODO AI功能点
124 | 1. 智能数据同步和迁移
125 | 2. 数据安全与监控
126 | 3. 智能化运维管理
127 | 4. 用户体验优化
128 | 5. 自动化数据映射
129 | 6. 自然语言处理
--------------------------------------------------------------------------------
/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func AddRouter() func(http.ResponseWriter, *http.Request) {
8 | return func(w http.ResponseWriter, r *http.Request) {
9 | return
10 | }
11 | }
12 |
13 | func DelRouter() func(http.ResponseWriter, *http.Request) {
14 | return func(w http.ResponseWriter, r *http.Request) {
15 | return
16 | }
17 | }
18 |
19 | func GetRouter() func(http.ResponseWriter, *http.Request) {
20 | return func(w http.ResponseWriter, r *http.Request) {
21 | return
22 | }
23 | }
24 |
25 | func PauseRouter() func(http.ResponseWriter, *http.Request) {
26 | return func(w http.ResponseWriter, r *http.Request) {
27 | return
28 | }
29 | }
30 |
31 | func ResumeRouter() func(http.ResponseWriter, *http.Request) {
32 | return func(w http.ResponseWriter, r *http.Request) {
33 | return
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/server.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/juju/errors"
5 | "github.com/sqlpub/qin-cdc/config"
6 | "github.com/sqlpub/qin-cdc/core"
7 | _ "github.com/sqlpub/qin-cdc/inputs"
8 | "github.com/sqlpub/qin-cdc/metas"
9 | _ "github.com/sqlpub/qin-cdc/outputs"
10 | "github.com/sqlpub/qin-cdc/registry"
11 | "github.com/sqlpub/qin-cdc/transforms"
12 | "sync"
13 | )
14 |
15 | type Server struct {
16 | Input core.Input
17 | Output core.Output
18 | Metas *core.Metas
19 | Position core.Position
20 | Transforms transforms.MatcherTransforms
21 | InboundChan chan *core.Msg
22 | OutboundChan chan *core.Msg
23 | sync.Mutex
24 | }
25 |
26 | func NewServer(conf *config.Config) (server *Server, err error) {
27 | server = &Server{}
28 |
29 | // input
30 | plugin, err := registry.GetPlugin(registry.InputPlugin, conf.InputConfig.Type)
31 | if err != nil {
32 | return nil, err
33 | }
34 | input, ok := plugin.(core.Input)
35 | if !ok {
36 | return nil, errors.Errorf("not a valid input type")
37 | }
38 | server.Input = input
39 | err = plugin.Configure(conf.InputConfig.Config)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | // output
45 | plugin, err = registry.GetPlugin(registry.OutputPlugin, conf.OutputConfig.Type)
46 | if err != nil {
47 | return nil, err
48 | }
49 | output, ok := plugin.(core.Output)
50 | if !ok {
51 | return nil, errors.Errorf("not a valid output type")
52 | }
53 | server.Output = output
54 | err = plugin.Configure(conf.OutputConfig.Config)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | // meta
60 | err = server.initMeta(conf)
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | // position
66 | plugin, err = registry.GetPlugin(registry.PositionPlugin, conf.InputConfig.Type)
67 | if err != nil {
68 | return nil, err
69 | }
70 | position, ok := plugin.(core.Position)
71 | if !ok {
72 | return nil, errors.Errorf("not a valid position type")
73 | }
74 | server.Position = position
75 | err = plugin.Configure(conf.InputConfig.Config)
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | // sync chan
81 | server.InboundChan = make(chan *core.Msg, 10240)
82 | server.OutboundChan = make(chan *core.Msg, 10240)
83 |
84 | // new position
85 | server.Position.LoadPosition(conf.Name)
86 | // new output
87 | server.Output.NewOutput(server.Metas)
88 | // new input
89 | server.Input.NewInput(server.Metas)
90 |
91 | return server, nil
92 | }
93 |
94 | func (s *Server) initMeta(conf *config.Config) (err error) {
95 | // Routers
96 | s.Metas = &core.Metas{
97 | Routers: &metas.Routers{},
98 | }
99 | err = s.Metas.Routers.InitRouters(conf.OutputConfig.Config)
100 | if err != nil {
101 | return err
102 | }
103 |
104 | // input meta
105 | plugin, err := registry.GetPlugin(registry.MetaPlugin, string(registry.InputPlugin)+conf.InputConfig.Type)
106 | if err != nil {
107 | return err
108 | }
109 | meta, ok := plugin.(core.InputMeta)
110 | if !ok {
111 | return errors.Errorf("not a valid meta type")
112 | }
113 |
114 | s.Metas.Input = meta
115 | err = plugin.Configure(conf.InputConfig.Config)
116 | if err != nil {
117 | return err
118 | }
119 | err = s.Metas.Input.LoadMeta(s.Metas.Routers.Raws)
120 | if err != nil {
121 | return err
122 | }
123 |
124 | // output meta
125 | plugin, err = registry.GetPlugin(registry.MetaPlugin, string(registry.OutputPlugin)+conf.OutputConfig.Type)
126 | if err != nil {
127 | return err
128 | }
129 | outputMeta, ok := plugin.(core.OutputMeta)
130 | if !ok {
131 | return errors.Errorf("not a valid meta type")
132 | }
133 |
134 | s.Metas.Output = outputMeta
135 | err = plugin.Configure(conf.OutputConfig.Config)
136 | if err != nil {
137 | return err
138 | }
139 | err = s.Metas.Output.LoadMeta(s.Metas.Routers.Raws)
140 | if err != nil {
141 | return err
142 | }
143 |
144 | // router column mapper
145 | err = s.Metas.InitRouterColumnsMapper()
146 | if err != nil {
147 | return err
148 | }
149 |
150 | // trans
151 | s.Transforms = transforms.NewMatcherTransforms(conf.TransformsConfig, s.Metas.Routers)
152 |
153 | // router column mapper MapMapper
154 | s.Metas.InitRouterColumnsMapperMapMapper()
155 | return nil
156 | }
157 |
158 | func (s *Server) Start() {
159 | s.Lock()
160 | defer s.Unlock()
161 |
162 | s.Input.Start(s.Position, s.InboundChan)
163 | s.Position.Start()
164 | s.Transforms.Start(s.InboundChan, s.OutboundChan)
165 | s.Output.Start(s.OutboundChan, s.Position)
166 | }
167 |
168 | func (s *Server) Close() {
169 | s.Lock()
170 | defer s.Unlock()
171 |
172 | s.Input.Close()
173 | s.Output.Close()
174 | s.Position.Close()
175 | s.Metas.Input.Close()
176 | s.Metas.Output.Close()
177 | }
178 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | version="v0.3.0"
4 | currentDir=$(cd $(dirname "$0") || exit; pwd)
5 |
6 | path="github.com/go-demo/version"
7 | buildTime=$(date +"%Y-%m-%d %H:%M:%S")
8 | buildTimeFormat=$(date +"%Y%m%d%H%M%S")
9 | newDir="../bin/qin-cdc-$version"
10 | # flagsMac="-X $path.Version=$version -X '$path.GoVersion=$(go version)' -X '$path.BuildTime=$buildTime' -X $path.GitCommit=$(git rev-parse HEAD)"
11 | flagsLinux="-X $path.Version=$version -X '$path.GoVersion=$(go version)' -X '$path.BuildTime=$buildTime' -X $path.GitCommit=$(git rev-parse HEAD)"
12 |
13 | mkdir -p "$newDir"
14 | echo start buid qin-cdc
15 | cd "$currentDir"/cmd || exit
16 | # go build -ldflags "$flagsMac" -o "$newDir"/go-"$dbType"-starrocks-mac-"$buildTimeFormat"
17 | CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ \
18 | go build -tags musl -ldflags "-extldflags -static $flagsLinux" -o "$newDir"/qin-cdc-$version-"$buildTimeFormat"
19 | echo end buid qin-cdc
--------------------------------------------------------------------------------
/cmd/qin_cdc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/siddontang/go-log/log"
5 | "github.com/sqlpub/qin-cdc/app"
6 | "github.com/sqlpub/qin-cdc/config"
7 | "github.com/sqlpub/qin-cdc/utils"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | )
12 |
13 | func main() {
14 | // help handle
15 | inputParam := utils.InitHelp()
16 | // input param handle
17 | utils.InputParamHandle(inputParam)
18 | // init log level
19 | log.SetLevelByName(*inputParam.LogLevel)
20 | // daemon mode handle
21 | utils.Daemon(inputParam)
22 |
23 | // 进程信号处理
24 | sc := make(chan os.Signal, 1)
25 | signal.Notify(sc,
26 | os.Kill,
27 | os.Interrupt,
28 | syscall.SIGHUP,
29 | syscall.SIGINT,
30 | syscall.SIGTERM,
31 | syscall.SIGQUIT)
32 |
33 | utils.StartHttp(inputParam)
34 |
35 | // config file handle
36 | conf := config.NewConfig(inputParam.ConfigFile)
37 | s, err := app.NewServer(conf)
38 | if err != nil {
39 | log.Fatal(err)
40 | }
41 | s.Start()
42 |
43 | utils.InitHttpApi()
44 |
45 | select {
46 | case n := <-sc:
47 | log.Infof("receive signal %v, closing", n)
48 | s.Close()
49 | log.Infof("qin-cdc is stopped.")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/BurntSushi/toml"
5 | "github.com/juju/errors"
6 | "github.com/siddontang/go-log/log"
7 | "path/filepath"
8 | )
9 |
10 | type Config struct {
11 | Name string
12 | InputConfig InputConfig `toml:"input"`
13 | OutputConfig OutputConfig `toml:"output"`
14 | TransformsConfig []TransformConfig `toml:"transforms"`
15 | FileName *string
16 | }
17 |
18 | type InputConfig struct {
19 | Type string `toml:"type"`
20 | Config map[string]interface{} `toml:"config"`
21 | }
22 |
23 | type OutputConfig struct {
24 | Type string `toml:"type"`
25 | Config map[string]interface{} `toml:"config"`
26 | }
27 |
28 | type TransformConfig struct {
29 | Type string `toml:"type"`
30 | Config map[string]interface{} `toml:"config"`
31 | }
32 |
33 | func NewConfig(fileName *string) (c *Config) {
34 | c = &Config{}
35 | fileNamePath, err := filepath.Abs(*fileName)
36 | if err != nil {
37 | log.Fatal(err)
38 | }
39 | c.FileName = &fileNamePath
40 | err = c.ReadConfig()
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 | return c
45 | }
46 |
47 | func (c *Config) ReadConfig() error {
48 | var err error
49 | if _, err = toml.DecodeFile(*c.FileName, c); err != nil {
50 | return errors.Trace(err)
51 | }
52 | return err
53 | }
54 |
--------------------------------------------------------------------------------
/config/plugin_config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type MysqlConfig struct {
4 | Host string
5 | Port int
6 | UserName string
7 | Password string
8 | Options struct {
9 | StartGtid string `toml:"start-gtid"`
10 | ServerId int `toml:"server-id"`
11 | BatchSize int `toml:"batch-size"`
12 | BatchIntervalMs int `toml:"batch-interval-ms"`
13 | }
14 | }
15 |
16 | type StarrocksConfig struct {
17 | Host string
18 | Port int
19 | LoadPort int `mapstructure:"load-port"`
20 | UserName string
21 | Password string
22 | Options struct {
23 | BatchSize int `toml:"batch-size" mapstructure:"batch-size"`
24 | BatchIntervalMs int `toml:"batch-interval-ms" mapstructure:"batch-interval-ms"`
25 | }
26 | }
27 |
28 | type DorisConfig struct {
29 | Host string
30 | Port int
31 | LoadPort int `mapstructure:"load-port"`
32 | UserName string
33 | Password string
34 | Options struct {
35 | BatchSize int `toml:"batch-size" mapstructure:"batch-size"`
36 | BatchIntervalMs int `toml:"batch-interval-ms" mapstructure:"batch-interval-ms"`
37 | }
38 | }
39 |
40 | type KafkaConfig struct {
41 | Brokers []string `toml:"brokers"`
42 | PartitionNum int `toml:"partition-num" mapstructure:"partition-num"`
43 | Options struct {
44 | BatchSize int `toml:"batch-size" mapstructure:"batch-size"`
45 | BatchIntervalMs int `toml:"batch-interval-ms" mapstructure:"batch-interval-ms"`
46 | OutputFormat string `toml:"output-format" mapstructure:"output-format"`
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/input.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type Input interface {
4 | NewInput(metas *Metas)
5 | Start(pos Position, in chan *Msg)
6 | Close()
7 | }
8 |
--------------------------------------------------------------------------------
/core/meta.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/juju/errors"
5 | "github.com/sqlpub/qin-cdc/metas"
6 | )
7 |
8 | type InputMeta interface {
9 | LoadMeta(routers []*metas.Router) error
10 | GetMeta(*metas.Router) (*metas.Table, error)
11 | // GetAll() map[string]*metas.Table
12 | GetVersion(schema string, tableName string, version uint) (*metas.Table, error)
13 | // Add(*metas.Table) error
14 | // Update(newTable *metas.Table) error
15 | // Delete(string, string) error
16 | Save() error
17 | Close()
18 | }
19 |
20 | type OutputMeta interface {
21 | LoadMeta(routers []*metas.Router) error
22 | GetMeta(*metas.Router) (interface{}, error)
23 | // GetAll() map[string]*metas.Table
24 | // GetVersion(schema string, tableName string, version uint) (*metas.Table, error)
25 | // Add(*metas.Table) error
26 | // Update(newTable *metas.Table) error
27 | // Delete(string, string) error
28 | Save() error
29 | Close()
30 | }
31 |
32 | type Metas struct {
33 | Input InputMeta
34 | Output OutputMeta
35 | Routers *metas.Routers
36 | }
37 |
38 | func (m *Metas) InitRouterColumnsMapper() error {
39 | // router column mapper
40 | for _, router := range m.Routers.Raws {
41 | inputTable, err := m.Input.GetMeta(router)
42 | if err != nil {
43 | return err
44 | }
45 | if inputTable == nil {
46 | return errors.Errorf("get input meta failed, err: %s.%s not found", router.SourceSchema, router.SourceTable)
47 | }
48 | for _, column := range inputTable.Columns {
49 | router.ColumnsMapper.SourceColumns = append(router.ColumnsMapper.SourceColumns, column.Name)
50 | if column.IsPrimaryKey {
51 | router.ColumnsMapper.PrimaryKeys = append(router.ColumnsMapper.PrimaryKeys, column.Name)
52 | }
53 | }
54 | metaObj, err := m.Output.GetMeta(router)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | outputTable, ok := metaObj.(*metas.Table)
60 | if ok {
61 | if outputTable == nil {
62 | return errors.Errorf("get output meta failed, err: %s.%s not found", router.TargetSchema, router.TargetTable)
63 | }
64 | for _, column := range outputTable.Columns {
65 | router.ColumnsMapper.TargetColumns = append(router.ColumnsMapper.TargetColumns, column.Name)
66 | }
67 | } else {
68 | // target == source
69 | for _, column := range inputTable.Columns {
70 | router.ColumnsMapper.TargetColumns = append(router.ColumnsMapper.TargetColumns, column.Name)
71 | }
72 | }
73 |
74 | }
75 | return nil
76 | }
77 |
78 | func (m *Metas) InitRouterColumnsMapperMapMapper() {
79 | // router column mapper MapMapper
80 | for _, router := range m.Routers.Raws {
81 | mapMapper := make(map[string]string)
82 | mapMapperOrder := make([]string, 0)
83 | // user config output.config.routers.columns-mapper.map-mapper
84 | if len(router.ColumnsMapper.MapMapper) > 0 {
85 | for i, column := range router.ColumnsMapper.SourceColumns {
86 | mapMapper[column] = router.ColumnsMapper.TargetColumns[i]
87 | mapMapperOrder = append(mapMapperOrder, column)
88 | }
89 | } else {
90 | for _, column := range router.ColumnsMapper.SourceColumns {
91 | // same name mapping
92 | for _, targetColumn := range router.ColumnsMapper.TargetColumns {
93 | if column == targetColumn {
94 | mapMapper[column] = targetColumn
95 | mapMapperOrder = append(mapMapperOrder, column)
96 | break
97 | }
98 | }
99 | }
100 | }
101 | router.ColumnsMapper.MapMapper = mapMapper
102 | router.ColumnsMapper.MapMapperOrder = mapMapperOrder
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/core/msg.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "github.com/goccy/go-json"
6 | "github.com/sqlpub/qin-cdc/metas"
7 | "time"
8 | )
9 |
10 | type MsgType string
11 | type ActionType string
12 | type DDLActionType string
13 |
14 | const (
15 | MsgDML MsgType = "dml"
16 | MsgDDL MsgType = "ddl"
17 | MsgCtl MsgType = "ctl"
18 |
19 | InsertAction ActionType = "insert"
20 | UpdateAction ActionType = "update"
21 | DeleteAction ActionType = "delete"
22 | ReplaceAction ActionType = "replace"
23 |
24 | CreateAction DDLActionType = "create"
25 | AlterAction DDLActionType = "alter"
26 | RenameAction DDLActionType = "rename"
27 | DropAction DDLActionType = "drop"
28 | TruncateAction DDLActionType = "Truncate"
29 | )
30 |
31 | type Msg struct {
32 | Database string
33 | Table string
34 | Type MsgType
35 | DmlMsg *DMLMsg
36 | Timestamp time.Time
37 | InputContext struct {
38 | Pos string
39 | }
40 | }
41 |
42 | type DMLMsg struct {
43 | Action ActionType
44 | Data map[string]interface{}
45 | Old map[string]interface{}
46 | TableVersion uint
47 | }
48 |
49 | type DDLMsg struct {
50 | Action DDLActionType
51 | NewTable metas.Table
52 | DdlStatement metas.DdlStatement
53 | }
54 |
55 | func (m *Msg) ToString() string {
56 | switch m.Type {
57 | case MsgDML:
58 | marshal, _ := json.Marshal(m.DmlMsg)
59 | return fmt.Sprintf("msg event: %s %s.%s %v", m.DmlMsg.Action, m.Database, m.Table, string(marshal))
60 | case MsgDDL:
61 | case MsgCtl:
62 | marshal, _ := json.Marshal(m.InputContext)
63 | return fmt.Sprintf("msg event: %s %v", m.Type, string(marshal))
64 | default:
65 | return fmt.Sprintf("msg event: %s %v", m.DmlMsg.Action, m)
66 | }
67 | return ""
68 | }
69 |
--------------------------------------------------------------------------------
/core/output.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type Output interface {
4 | NewOutput(metas *Metas)
5 | Start(out chan *Msg, pos Position)
6 | Close()
7 | }
8 |
--------------------------------------------------------------------------------
/core/position.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type Position interface {
4 | LoadPosition(name string) string
5 | Start()
6 | Update(v string) error
7 | Save() error
8 | Get() string
9 | Close()
10 | }
11 |
--------------------------------------------------------------------------------
/core/transform.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type Transform interface {
4 | NewTransform(config map[string]interface{}) error
5 | Transform(msg *Msg) bool
6 | }
7 |
--------------------------------------------------------------------------------
/docs/mysql-to-doris-sample.toml:
--------------------------------------------------------------------------------
1 | # name 必填,多实例运行时保证全局唯一
2 | name = "mysql2doris"
3 |
4 | [input]
5 | type = "mysql"
6 |
7 | [input.config.source]
8 | host = "127.0.0.1"
9 | port = 3306
10 | username = "root"
11 | password = "root"
12 |
13 | [input.config.source.options]
14 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
15 | #server-id = 1001
16 |
17 | [[transforms]]
18 | type = "rename-column"
19 | [transforms.config]
20 | match-schema = "sysbenchts"
21 | match-table = "sbtest1"
22 | columns = ["k", "c"]
23 | rename-as = ["k_1", "c_1"]
24 |
25 | [[transforms]]
26 | type = "delete-column"
27 | [transforms.config]
28 | match-schema = "sysbenchts"
29 | match-table = "sbtest1"
30 | columns = ["c_1"]
31 |
32 | [output]
33 | type = "doris"
34 |
35 | [output.config.target]
36 | host = "127.0.0.1"
37 | port = 9030
38 | load-port = 8030 # support fe httpPort:8030 or be httpPort:8040
39 | username = "root"
40 | password = "root"
41 |
42 | [input.config.target.options]
43 | batch-size = 1000
44 | batch-interval-ms = 1000
45 | parallel-workers = 4
46 |
47 | [[output.config.routers]]
48 | source-schema = "sysbenchts"
49 | source-table = "sbtest1"
50 | target-schema = "doris_test"
51 | target-table = "ods_sbtest1"
52 |
53 | [[output.config.routers]]
54 | source-schema = "sysbenchts"
55 | source-table = "sbtest2"
56 | target-schema = "doris_test"
57 | target-table = "ods_sbtest2"
58 | [output.config.routers.columns-mapper]
59 | source-columns = []
60 | target-columns = []
--------------------------------------------------------------------------------
/docs/mysql-to-kafka-sample.toml:
--------------------------------------------------------------------------------
1 | # name 必填,多实例运行时保证全局唯一
2 | name = "mysql2kafka"
3 |
4 | [input]
5 | type = "mysql"
6 |
7 | [input.config.source]
8 | host = "127.0.0.1"
9 | port = 3306
10 | username = "root"
11 | password = "root"
12 |
13 | [input.config.source.options]
14 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
15 | #server-id = 1001
16 |
17 | [[transforms]]
18 | type = "rename-column"
19 | [transforms.config]
20 | match-schema = "sysbenchts"
21 | match-table = "sbtest1"
22 | columns = ["k", "c"]
23 | rename-as = ["k_1", "c_1"]
24 |
25 | [[transforms]]
26 | type = "delete-column"
27 | [transforms.config]
28 | match-schema = "sysbenchts"
29 | match-table = "sbtest1"
30 | columns = ["c_1"]
31 |
32 | [output]
33 | type = "kafka"
34 |
35 | [output.config.target]
36 | brokers = ["127.0.0.1:9092"]
37 | partition-num = 1
38 |
39 | [output.config.target.options]
40 | batch-size = 1000
41 | batch-interval-ms = 1000
42 | parallel-workers = 4
43 | output-format = "json" # or aliyun_dts_canal
44 |
45 | [[output.config.routers]]
46 | source-schema = "sysbenchts"
47 | source-table = "sbtest1"
48 | dml-topic = "mysql-binlog"
49 | [output.config.routers.columns-mapper]
50 | source-columns = []
51 | target-columns = []
52 |
53 | [[output.config.routers]]
54 | source-schema = "sysbenchts"
55 | source-table = "sbtest2"
56 | dml-topic = "mysql-binlog"
57 | [output.config.routers.columns-mapper]
58 | source-columns = []
59 | target-columns = []
--------------------------------------------------------------------------------
/docs/mysql-to-mysql-sample.toml:
--------------------------------------------------------------------------------
1 | # name 必填,多实例运行时保证全局唯一
2 | name = "mysql2mysql"
3 |
4 | [input]
5 | type = "mysql"
6 |
7 | [input.config.source]
8 | host = "127.0.0.1"
9 | port = 3306
10 | username = "root"
11 | password = "root"
12 |
13 | [input.config.source.options]
14 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
15 | #server-id = 1001
16 |
17 | [[transforms]]
18 | type = "rename-column"
19 | [transforms.config]
20 | match-schema = "sysbenchts"
21 | match-table = "sbtest1"
22 | columns = ["c"]
23 | rename-as = ["c_1"]
24 |
25 | [[transforms]]
26 | type = "delete-column"
27 | [transforms.config]
28 | match-schema = "sysbenchts"
29 | match-table = "sbtest1"
30 | columns = ["c_1"]
31 |
32 | [output]
33 | type = "mysql"
34 |
35 | [output.config.target]
36 | host = "127.0.0.1"
37 | port = 3307
38 | username = "root"
39 | password = "root"
40 |
41 | [input.config.target.options]
42 | batch-size = 1000
43 | batch-interval-ms = 500
44 | parallel-workers = 4
45 |
46 | [[output.config.routers]]
47 | source-schema = "sysbenchts"
48 | source-table = "sbtest1"
49 | target-schema = "sysbenchts"
50 | target-table = "sbtest1"
51 |
52 | [[output.config.routers]]
53 | source-schema = "sysbenchts"
54 | source-table = "sbtest2"
55 | target-schema = "sysbenchts"
56 | target-table = "sbtest2"
57 | [output.config.routers.columns-mapper]
58 | source-columns = []
59 | target-columns = []
--------------------------------------------------------------------------------
/docs/mysql-to-starrocks-sample.toml:
--------------------------------------------------------------------------------
1 | # name 必填,多实例运行时保证全局唯一
2 | name = "mysql2starrocks"
3 |
4 | [input]
5 | type = "mysql"
6 |
7 | [input.config.source]
8 | host = "127.0.0.1"
9 | port = 3306
10 | username = "root"
11 | password = "root"
12 |
13 | [input.config.source.options]
14 | #start-gtid = "3ba13781-44eb-2157-88a5-0dc879ec2221:1-123456"
15 | #server-id = 1001
16 |
17 | [[transforms]]
18 | type = "rename-column"
19 | [transforms.config]
20 | match-schema = "sysbenchts"
21 | match-table = "sbtest1"
22 | columns = ["k", "c"]
23 | rename-as = ["k_1", "c_1"]
24 |
25 | [[transforms]]
26 | type = "delete-column"
27 | [transforms.config]
28 | match-schema = "sysbenchts"
29 | match-table = "sbtest1"
30 | columns = ["c_1"]
31 |
32 | [output]
33 | type = "starrocks"
34 |
35 | [output.config.target]
36 | host = "127.0.0.1"
37 | port = 9030
38 | load-port = 8040 # support fe httpPort:8030 or be httpPort:8040
39 | username = "root"
40 | password = ""
41 |
42 | [input.config.target.options]
43 | batch-size = 1000
44 | batch-interval-ms = 1000
45 | parallel-workers = 4
46 |
47 | [[output.config.routers]]
48 | source-schema = "sysbenchts"
49 | source-table = "sbtest1"
50 | target-schema = "sr_test"
51 | target-table = "ods_sbtest1"
52 |
53 | [[output.config.routers]]
54 | source-schema = "sysbenchts"
55 | source-table = "sbtest2"
56 | target-schema = "sr_test"
57 | target-table = "ods_sbtest2"
58 | [output.config.routers.columns-mapper]
59 | source-columns = []
60 | target-columns = []
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sqlpub/qin-cdc
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/BurntSushi/toml v1.4.0
7 | github.com/confluentinc/confluent-kafka-go/v2 v2.4.0
8 | github.com/go-demo/version v0.0.0-20200109120206-2cde9473fd92
9 | github.com/go-mysql-org/go-mysql v1.8.0
10 | github.com/go-sql-driver/mysql v1.8.1
11 | github.com/goccy/go-json v0.10.3
12 | github.com/google/uuid v1.6.0
13 | github.com/juju/errors v1.0.0
14 | github.com/mitchellh/hashstructure/v2 v2.0.2
15 | github.com/mitchellh/mapstructure v1.5.0
16 | github.com/pingcap/tidb/pkg/parser v0.0.0-20240516062813-cc127c14b8cc
17 | github.com/prometheus/client_golang v1.19.1
18 | github.com/sevlyar/go-daemon v0.1.6
19 | github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
20 | go.etcd.io/bbolt v1.3.10
21 | )
22 |
23 | require (
24 | filippo.io/edwards25519 v1.1.0 // indirect
25 | github.com/Masterminds/semver v1.5.0 // indirect
26 | github.com/beorn7/perks v1.0.1 // indirect
27 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
28 | github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
29 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
30 | github.com/klauspost/compress v1.17.4 // indirect
31 | github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
32 | github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c // indirect
33 | github.com/pingcap/log v1.1.1-0.20230317032135-a0d097d16e22 // indirect
34 | github.com/prometheus/client_model v0.5.0 // indirect
35 | github.com/prometheus/common v0.48.0 // indirect
36 | github.com/prometheus/procfs v0.12.0 // indirect
37 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
38 | github.com/shopspring/decimal v1.2.0 // indirect
39 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
40 | go.uber.org/atomic v1.11.0 // indirect
41 | go.uber.org/multierr v1.11.0 // indirect
42 | go.uber.org/zap v1.26.0 // indirect
43 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
44 | golang.org/x/sys v0.18.0 // indirect
45 | golang.org/x/text v0.14.0 // indirect
46 | google.golang.org/protobuf v1.33.0 // indirect
47 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
48 | )
49 |
--------------------------------------------------------------------------------
/inputs/init.go:
--------------------------------------------------------------------------------
1 | package inputs
2 |
3 | import (
4 | "github.com/sqlpub/qin-cdc/inputs/mysql"
5 | "github.com/sqlpub/qin-cdc/registry"
6 | )
7 |
8 | func init() {
9 | // input mysql plugins
10 | registry.RegisterPlugin(registry.InputPlugin, mysql.PluginName, &mysql.InputPlugin{})
11 | registry.RegisterPlugin(registry.MetaPlugin, string(registry.InputPlugin+mysql.PluginName), &mysql.MetaPlugin{})
12 | registry.RegisterPlugin(registry.PositionPlugin, mysql.PluginName, &mysql.PositionPlugin{})
13 | }
14 |
--------------------------------------------------------------------------------
/inputs/mysql/msg.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/go-mysql-org/go-mysql/replication"
5 | "github.com/sqlpub/qin-cdc/core"
6 | "github.com/sqlpub/qin-cdc/metas"
7 | "time"
8 | )
9 |
10 | func (i *InputPlugin) NewInsertMsgs(schemaName string, tableName string, tableMeta *metas.Table, evs *replication.RowsEvent, header *replication.EventHeader) (msgs []*core.Msg, err error) {
11 | // new insert msgs
12 | msgs = make([]*core.Msg, len(evs.Rows))
13 | for index, row := range evs.Rows {
14 | data := make(map[string]interface{})
15 | for columnIndex, value := range row {
16 | data[tableMeta.Columns[columnIndex].Name] = deserialize(value, tableMeta.Columns[columnIndex])
17 | }
18 | msg := &core.Msg{
19 | Database: schemaName,
20 | Table: tableName,
21 | Type: core.MsgDML,
22 | DmlMsg: &core.DMLMsg{Action: core.InsertAction, Data: data, TableVersion: tableMeta.Version},
23 | Timestamp: time.Unix(int64(header.Timestamp), 0),
24 | }
25 | msgs[index] = msg
26 | }
27 | return msgs, nil
28 | }
29 |
30 | func (i *InputPlugin) NewUpdateMsgs(schemaName string, tableName string, tableMeta *metas.Table, evs *replication.RowsEvent, header *replication.EventHeader) (msgs []*core.Msg, err error) {
31 | // new update msgs
32 | for index := 0; index < len(evs.Rows); index += 2 {
33 | oldDataRow := evs.Rows[index]
34 | newDataRow := evs.Rows[index+1]
35 |
36 | data := make(map[string]interface{})
37 | old := make(map[string]interface{})
38 |
39 | for columnIndex := 0; columnIndex < len(newDataRow); columnIndex++ {
40 | data[tableMeta.Columns[columnIndex].Name] = deserialize(newDataRow[columnIndex], tableMeta.Columns[columnIndex])
41 | old[tableMeta.Columns[columnIndex].Name] = deserialize(oldDataRow[columnIndex], tableMeta.Columns[columnIndex])
42 | }
43 | msg := &core.Msg{
44 | Database: schemaName,
45 | Table: tableName,
46 | Type: core.MsgDML,
47 | DmlMsg: &core.DMLMsg{Action: core.UpdateAction, Data: data, Old: old, TableVersion: tableMeta.Version},
48 | Timestamp: time.Unix(int64(header.Timestamp), 0),
49 | }
50 | msgs = append(msgs, msg)
51 |
52 | }
53 | return msgs, nil
54 | }
55 |
56 | func (i *InputPlugin) NewDeleteMsgs(schemaName string, tableName string, tableMeta *metas.Table, evs *replication.RowsEvent, header *replication.EventHeader) (msgs []*core.Msg, err error) {
57 | // new delete msgs
58 | msgs = make([]*core.Msg, len(evs.Rows))
59 | for index, row := range evs.Rows {
60 | data := make(map[string]interface{})
61 | for columnIndex, value := range row {
62 | data[tableMeta.Columns[columnIndex].Name] = deserialize(value, tableMeta.Columns[columnIndex])
63 | }
64 | msg := &core.Msg{
65 | Database: schemaName,
66 | Table: tableName,
67 | Type: core.MsgDML,
68 | DmlMsg: &core.DMLMsg{Action: core.DeleteAction, Data: data, TableVersion: tableMeta.Version},
69 | Timestamp: time.Unix(int64(header.Timestamp), 0),
70 | }
71 | msgs[index] = msg
72 | }
73 | return msgs, nil
74 | }
75 |
76 | func (i *InputPlugin) NewXIDMsg(ev *replication.XIDEvent, header *replication.EventHeader) (msg *core.Msg, err error) {
77 | // new xid msg
78 | msg = &core.Msg{
79 | Type: core.MsgCtl,
80 | Timestamp: time.Unix(int64(header.Timestamp), 0),
81 | }
82 | msg.InputContext.Pos = ev.GSet.String()
83 | return msg, nil
84 | }
85 |
86 | func (i *InputPlugin) SendMsgs(msgs []*core.Msg) {
87 | for _, msg := range msgs {
88 | i.SendMsg(msg)
89 | }
90 | }
91 |
92 | func (i *InputPlugin) SendMsg(msg *core.Msg) {
93 | i.in <- msg
94 | }
95 |
--------------------------------------------------------------------------------
/inputs/mysql/mysql.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/mitchellh/mapstructure"
5 | "github.com/siddontang/go-log/log"
6 | "github.com/sqlpub/qin-cdc/config"
7 | "github.com/sqlpub/qin-cdc/core"
8 | )
9 |
10 | type InputPlugin struct {
11 | *config.MysqlConfig
12 | in chan *core.Msg
13 | metas *core.Metas
14 | metaPlugin *MetaPlugin
15 | binlogTailer *BinlogTailer
16 | }
17 |
18 | func (i *InputPlugin) Configure(conf map[string]interface{}) error {
19 | i.MysqlConfig = &config.MysqlConfig{}
20 | var source = conf["source"]
21 | if err := mapstructure.Decode(source, i.MysqlConfig); err != nil {
22 | return err
23 | }
24 | return nil
25 | }
26 |
27 | func (i *InputPlugin) NewInput(metas *core.Metas) {
28 | i.metas = metas
29 | i.metaPlugin = metas.Input.(*MetaPlugin)
30 | }
31 |
32 | func (i *InputPlugin) Start(pos core.Position, in chan *core.Msg) {
33 | i.in = in
34 | var positionPlugin = &PositionPlugin{}
35 | if err := mapstructure.Decode(pos, positionPlugin); err != nil {
36 | log.Fatalf("mysql position parsing failed. err: %s", err.Error())
37 | }
38 |
39 | i.binlogTailer = &BinlogTailer{}
40 | i.binlogTailer.New(i)
41 | go i.binlogTailer.Start(positionPlugin.pos)
42 | }
43 |
44 | func (i *InputPlugin) Close() {
45 | i.binlogTailer.Close()
46 | close(i.in)
47 | }
48 |
--------------------------------------------------------------------------------
/inputs/mysql/mysql_meta.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | _ "github.com/go-sql-driver/mysql"
7 | "github.com/mitchellh/mapstructure"
8 | "github.com/sqlpub/qin-cdc/config"
9 | "github.com/sqlpub/qin-cdc/metas"
10 | "strings"
11 | "sync"
12 | "time"
13 | )
14 |
15 | type MetaPlugin struct {
16 | *config.MysqlConfig
17 | tables map[string]*metas.Table
18 | tablesVersion map[string]*metas.Table
19 | db *sql.DB
20 | mu sync.Mutex
21 | }
22 |
23 | func (m *MetaPlugin) Configure(conf map[string]interface{}) error {
24 | m.MysqlConfig = &config.MysqlConfig{}
25 | var source = conf["source"]
26 | if err := mapstructure.Decode(source, m.MysqlConfig); err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
32 | func (m *MetaPlugin) LoadMeta(routers []*metas.Router) (err error) {
33 | m.tables = make(map[string]*metas.Table)
34 | m.tablesVersion = make(map[string]*metas.Table)
35 | dsn := fmt.Sprintf(
36 | "%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4&timeout=3s",
37 | m.MysqlConfig.UserName, m.MysqlConfig.Password,
38 | m.MysqlConfig.Host, m.MysqlConfig.Port)
39 | m.db, err = sql.Open("mysql", dsn)
40 | if err != nil {
41 | return err
42 | }
43 | m.db.SetConnMaxLifetime(time.Minute * 3)
44 | m.db.SetMaxOpenConns(2)
45 | m.db.SetMaxIdleConns(2)
46 | for _, router := range routers {
47 | row := m.db.QueryRow(fmt.Sprintf("show create table `%s`.`%s`", router.SourceSchema, router.SourceTable))
48 | if row.Err() != nil {
49 | return err
50 | }
51 | var tableName string
52 | var createTableDdlStr string
53 | err = row.Scan(&tableName, &createTableDdlStr)
54 | if err != nil {
55 | return err
56 | }
57 | createTableDdlStr = strings.Replace(createTableDdlStr, "CREATE TABLE ", fmt.Sprintf("CREATE TABLE `%s`.", router.SourceSchema), 1)
58 | table := &metas.Table{}
59 | table, err = metas.NewTable(createTableDdlStr)
60 | if err != nil {
61 | return err
62 | }
63 | err = m.Add(table)
64 | if err != nil {
65 | return err
66 | }
67 | }
68 | return nil
69 | }
70 |
71 | func (m *MetaPlugin) GetMeta(router *metas.Router) (table *metas.Table, err error) {
72 | return m.Get(router.SourceSchema, router.SourceTable)
73 | }
74 |
75 | func (m *MetaPlugin) Get(schema string, tableName string) (table *metas.Table, err error) {
76 | key := metas.GenerateMapRouterKey(schema, tableName)
77 | m.mu.Lock()
78 | defer m.mu.Unlock()
79 | return m.tables[key], err
80 | }
81 |
82 | func (m *MetaPlugin) GetAll() map[string]*metas.Table {
83 | m.mu.Lock()
84 | defer m.mu.Unlock()
85 | return m.tables
86 | }
87 |
88 | func (m *MetaPlugin) GetVersion(schema string, tableName string, version uint) (table *metas.Table, err error) {
89 | key := metas.GenerateMapRouterVersionKey(schema, tableName, version)
90 | m.mu.Lock()
91 | defer m.mu.Unlock()
92 | return m.tablesVersion[key], err
93 | }
94 |
95 | func (m *MetaPlugin) GetVersions(schema string, tableName string) []*metas.Table {
96 | m.mu.Lock()
97 | defer m.mu.Unlock()
98 | tables := make([]*metas.Table, 0)
99 | for k, table := range m.tablesVersion {
100 | s, t, _ := metas.SplitMapRouterVersionKey(k)
101 | if schema == s && tableName == t {
102 | tables = append(tables, table)
103 | }
104 | }
105 | return tables
106 | }
107 |
108 | func (m *MetaPlugin) Add(newTable *metas.Table) error {
109 | m.mu.Lock()
110 | defer m.mu.Unlock()
111 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
112 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
113 | return nil
114 | }
115 |
116 | func (m *MetaPlugin) Update(newTable *metas.Table) error {
117 | m.mu.Lock()
118 | defer m.mu.Unlock()
119 | newTable.Version += 1
120 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
121 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
122 | return nil
123 | }
124 |
125 | func (m *MetaPlugin) Delete(schema string, name string) error {
126 | m.mu.Lock()
127 | defer m.mu.Unlock()
128 | delete(m.tables, metas.GenerateMapRouterKey(schema, name))
129 | for _, table := range m.GetVersions(schema, name) {
130 | delete(m.tablesVersion, metas.GenerateMapRouterVersionKey(schema, name, table.Version))
131 | }
132 | return nil
133 | }
134 |
135 | func (m *MetaPlugin) Save() error {
136 | return nil
137 | }
138 |
139 | func (m *MetaPlugin) Close() {
140 | if m.db != nil {
141 | _ = m.db.Close()
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/inputs/mysql/mysql_position.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/mitchellh/mapstructure"
7 | "github.com/siddontang/go-log/log"
8 | "github.com/sqlpub/qin-cdc/config"
9 | bolt "go.etcd.io/bbolt"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type PositionPlugin struct {
15 | *config.MysqlConfig
16 | name string
17 | metaDb *bolt.DB
18 | bucketName string
19 | pos string
20 | stop chan struct{}
21 | done chan struct{}
22 | mu sync.Mutex
23 | }
24 |
25 | func (p *PositionPlugin) Configure(conf map[string]interface{}) error {
26 | p.MysqlConfig = &config.MysqlConfig{}
27 | var source = conf["source"]
28 | if err := mapstructure.Decode(source, p.MysqlConfig); err != nil {
29 | return err
30 | }
31 | p.bucketName = "position"
32 | p.stop = make(chan struct{})
33 | p.done = make(chan struct{})
34 | return nil
35 | }
36 |
37 | func (p *PositionPlugin) LoadPosition(name string) string {
38 | p.name = name
39 | var err error
40 | p.metaDb, err = bolt.Open("meta.db", 0600, &bolt.Options{Timeout: 1 * time.Second})
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 | p.pos = p.getPosFromMetaDb() // meta.db
45 | if p.pos != "" {
46 | return p.pos
47 | }
48 | p.pos = p.getPosFromConfig() // config file
49 | if p.pos != "" {
50 | return p.pos
51 | }
52 | p.pos = p.getPosFromSource() // from db get now position
53 | return p.pos
54 | }
55 |
56 | func (p *PositionPlugin) Start() {
57 | go p.timerSave()
58 | }
59 |
60 | func (p *PositionPlugin) Update(v string) error {
61 | // only updating memory variables is not persistent
62 | // select save func persistent
63 | p.mu.Lock()
64 | defer p.mu.Unlock()
65 | if v == "" {
66 | return errors.Errorf("empty value")
67 | }
68 | p.pos = v
69 | return nil
70 | }
71 |
72 | func (p *PositionPlugin) Save() error {
73 | // persistent save pos
74 | p.mu.Lock()
75 | defer p.mu.Unlock()
76 | err := p.metaDb.Update(func(tx *bolt.Tx) error {
77 | b := tx.Bucket([]byte(p.bucketName))
78 | if b == nil {
79 | return fmt.Errorf("bucket:%s does not exist", p.bucketName)
80 | }
81 | err := b.Put([]byte(p.name), []byte(p.pos))
82 | return err
83 | })
84 | return err
85 | }
86 |
87 | func (p *PositionPlugin) Get() string {
88 | p.mu.Lock()
89 | defer p.mu.Unlock()
90 | return p.pos
91 | }
92 |
93 | func (p *PositionPlugin) Close() {
94 | close(p.stop)
95 | <-p.done
96 | if p.metaDb != nil {
97 | err := p.metaDb.Close()
98 | if err != nil {
99 | log.Errorf("close metaDb conn failed: %s", err.Error())
100 | }
101 | }
102 | }
103 |
104 | func (p *PositionPlugin) getPosFromMetaDb() string {
105 | var pos []byte
106 | err := p.metaDb.Update(func(tx *bolt.Tx) error {
107 | b, err := tx.CreateBucketIfNotExists([]byte(p.bucketName))
108 | if err != nil {
109 | return err
110 | }
111 | pos = b.Get([]byte(p.name))
112 | return nil
113 | })
114 | if err != nil {
115 | log.Fatalf("from metaDb get position: %s", err.Error())
116 | }
117 | return string(pos)
118 | }
119 |
120 | func (p *PositionPlugin) getPosFromConfig() string {
121 | if pos := p.MysqlConfig.Options.StartGtid; pos != "" {
122 | return fmt.Sprintf("%v", pos)
123 | }
124 | return ""
125 | }
126 |
127 | func (p *PositionPlugin) getPosFromSource() string {
128 | db, err := getConn(p.MysqlConfig)
129 | if err != nil {
130 | log.Fatalf("conn db failed, %s", err.Error())
131 | return ""
132 | }
133 | defer closeConn(db)
134 | var gtidMode string
135 | err = db.QueryRow(`select @@GLOBAL.gtid_mode`).Scan(>idMode)
136 | if err != nil {
137 | log.Fatalf("query gtid_mode failed, %s", err.Error())
138 | }
139 | if gtidMode != "ON" {
140 | log.Fatalf("gtid_mode is not enabled")
141 | }
142 | var nowGtid string
143 | err = db.QueryRow(`SELECT @@GLOBAL.gtid_executed`).Scan(&nowGtid)
144 | if err != nil {
145 | log.Fatalf("query now gitd value failed, %s", err.Error())
146 | }
147 | return nowGtid
148 | }
149 |
150 | func (p *PositionPlugin) timerSave() {
151 | ticker := time.NewTicker(3 * time.Second)
152 | defer ticker.Stop()
153 | for {
154 | select {
155 | case <-ticker.C:
156 | if err := p.Save(); err != nil {
157 | log.Fatalf("timer save position failed: %s", err.Error())
158 | }
159 | // log.Debugf("timer save position: %s", p.pos)
160 | case <-p.stop:
161 | if err := p.Save(); err != nil {
162 | log.Fatalf("timer save position failed: %s", err.Error())
163 | }
164 | log.Infof("last save position: %v", p.pos)
165 | p.done <- struct{}{}
166 | return
167 |
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/inputs/mysql/mysql_replication.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/go-mysql-org/go-mysql/mysql"
7 | "github.com/google/uuid"
8 | "github.com/siddontang/go-log/log"
9 | "github.com/sqlpub/qin-cdc/config"
10 | "github.com/sqlpub/qin-cdc/core"
11 | "github.com/sqlpub/qin-cdc/metas"
12 | "regexp"
13 | )
14 | import "github.com/go-mysql-org/go-mysql/replication"
15 |
16 | type BinlogTailer struct {
17 | *config.MysqlConfig
18 | syncer *replication.BinlogSyncer
19 | inputPlugin *InputPlugin
20 | Pos mysql.Position
21 | GSet mysql.GTIDSet
22 | }
23 |
24 | func (b *BinlogTailer) New(inputPlugin *InputPlugin) {
25 | cfg := replication.BinlogSyncerConfig{
26 | ServerID: getServerId(inputPlugin.MysqlConfig),
27 | Flavor: PluginName,
28 | Host: inputPlugin.Host,
29 | Port: uint16(inputPlugin.Port),
30 | User: inputPlugin.UserName,
31 | Password: inputPlugin.Password,
32 | Charset: DefaultCharset,
33 | }
34 | b.syncer = replication.NewBinlogSyncer(cfg)
35 | b.inputPlugin = inputPlugin
36 | }
37 |
38 | func (b *BinlogTailer) Start(gtid string) {
39 | gtidSet, err := mysql.ParseGTIDSet(PluginName, gtid)
40 | if err != nil {
41 | log.Fatalf("parse gtid %s with flavor %s failed, error: %v", gtid, PluginName, err.Error())
42 | }
43 | streamer, _ := b.syncer.StartSyncGTID(gtidSet)
44 | for {
45 | ev, err := streamer.GetEvent(context.Background())
46 | if err == replication.ErrSyncClosed {
47 | return
48 | }
49 | // ev.Dump(os.Stdout)
50 | switch e := ev.Event.(type) {
51 | case *replication.RotateEvent:
52 | b.handleRotateEvent(e)
53 | case *replication.RowsEvent:
54 | b.handleRowsEvent(ev)
55 | case *replication.XIDEvent:
56 | b.handleXIDEvent(ev)
57 | case *replication.GTIDEvent:
58 | b.handleGTIDEvent(e)
59 | case *replication.QueryEvent:
60 | b.handleDDLEvent(ev)
61 | default:
62 | continue
63 | }
64 | }
65 | }
66 |
67 | func (b *BinlogTailer) Close() {
68 | b.syncer.Close()
69 | }
70 |
71 | func (b *BinlogTailer) columnCountEqual(e *replication.RowsEvent, tableMeta *metas.Table) bool {
72 | return int(e.ColumnCount) == len(tableMeta.Columns)
73 | }
74 |
75 | func (b *BinlogTailer) handleRotateEvent(e *replication.RotateEvent) {
76 | b.Pos.Name = string(e.NextLogName)
77 | b.Pos.Pos = uint32(e.Position)
78 | }
79 |
80 | func (b *BinlogTailer) handleRowsEvent(ev *replication.BinlogEvent) {
81 | e := ev.Event.(*replication.RowsEvent)
82 | schemaName, tableName := string(e.Table.Schema), string(e.Table.Table)
83 | tableMeta, _ := b.inputPlugin.metaPlugin.Get(schemaName, tableName)
84 | if tableMeta == nil {
85 | return
86 | }
87 | // column count equal check
88 | if !b.columnCountEqual(e, tableMeta) {
89 | log.Fatalf("binlog event row values length: %d != table meta columns length: %d", e.ColumnCount, len(tableMeta.Columns))
90 | }
91 |
92 | var msgs []*core.Msg
93 | var err error
94 | var actionType core.ActionType
95 | switch ev.Header.EventType {
96 | case replication.WRITE_ROWS_EVENTv0, replication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2:
97 | actionType = core.InsertAction
98 | msgs, err = b.inputPlugin.NewInsertMsgs(schemaName, tableName, tableMeta, e, ev.Header)
99 | case replication.UPDATE_ROWS_EVENTv0, replication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:
100 | actionType = core.UpdateAction
101 | msgs, err = b.inputPlugin.NewUpdateMsgs(schemaName, tableName, tableMeta, e, ev.Header)
102 | case replication.DELETE_ROWS_EVENTv0, replication.DELETE_ROWS_EVENTv1, replication.DELETE_ROWS_EVENTv2:
103 | actionType = core.DeleteAction
104 | msgs, err = b.inputPlugin.NewDeleteMsgs(schemaName, tableName, tableMeta, e, ev.Header)
105 | default:
106 | log.Fatalf("unsupported event type: %s", ev.Header.EventType)
107 | }
108 | if err != nil {
109 | log.Fatalf("%v event handle failed: %s", actionType, err.Error())
110 | }
111 | b.inputPlugin.SendMsgs(msgs)
112 | }
113 |
114 | func (b *BinlogTailer) handleXIDEvent(ev *replication.BinlogEvent) {
115 | e := ev.Event.(*replication.XIDEvent)
116 | msg, err := b.inputPlugin.NewXIDMsg(e, ev.Header)
117 | if err != nil {
118 | log.Fatalf("xid event handle failed: %s", err.Error())
119 | }
120 | b.inputPlugin.SendMsg(msg)
121 | }
122 |
123 | func (b *BinlogTailer) handleGTIDEvent(e *replication.GTIDEvent) {
124 | var err error
125 | u, _ := uuid.FromBytes(e.SID)
126 | b.GSet, err = mysql.ParseMysqlGTIDSet(fmt.Sprintf("%s:%d", u.String(), e.GNO))
127 | if err != nil {
128 | log.Fatalf("gtid event handle failed: %v", err)
129 | }
130 | }
131 |
132 | func (b *BinlogTailer) handleDDLEvent(ev *replication.BinlogEvent) {
133 | e := ev.Event.(*replication.QueryEvent)
134 | schemaName := string(e.Schema)
135 | ddlSql := string(e.Query)
136 | if ddlSql == "BEGIN" {
137 | return
138 | }
139 | ddlStatements, err := metas.TableDdlParser(ddlSql, schemaName)
140 | if err != nil {
141 | log.Fatalf("ddl event handle failed: %s", err.Error())
142 | }
143 | for _, ddlStatement := range ddlStatements {
144 | schema := ddlStatement.Schema
145 | name := ddlStatement.Name
146 | var table *metas.Table
147 | table, err = b.inputPlugin.metaPlugin.Get(schema, name)
148 | if err != nil {
149 | log.Fatalf("ddl event handle get table meta failed: %s", err.Error())
150 | }
151 |
152 | isSyncTable := false
153 | isOnlineDdlTable := false
154 | for _, v := range b.inputPlugin.metaPlugin.GetAll() {
155 | if schema == v.Schema && name == v.Name {
156 | isSyncTable = true
157 | break
158 | }
159 |
160 | // aliyun dms online ddl reg
161 | aliyunDMSOnlineDdlRegStr := fmt.Sprintf("^tp_\\d+_(ogt|del|ogl)_%s$", v.Name)
162 | aliyunDMSOnlineDdlReg, err := regexp.Compile(aliyunDMSOnlineDdlRegStr)
163 | if err != nil {
164 | log.Fatalf("parse aliyun dms online ddl regexp err %v", err.Error())
165 | }
166 | // aliyun dms online ddl reg2
167 | aliyunDMSOnlineDdlReg2Str := fmt.Sprintf("^tpa_[a-z0-9]+_%v$", v.Name)
168 | aliyunDMSOnlineDdlReg2, err := regexp.Compile(aliyunDMSOnlineDdlReg2Str)
169 | if err != nil {
170 | log.Fatalf("parse aliyun dms online ddl regexp err %v", err.Error())
171 | }
172 | // gh-ost online ddl reg
173 | ghostOnlineDdlRegStr := fmt.Sprintf("^_%s_(gho|ghc|del)$", v.Name)
174 | ghostOnlineDdlReg, err := regexp.Compile(ghostOnlineDdlRegStr)
175 | if err != nil {
176 | log.Fatalf("parse gh-ost online ddl regexp err %v", err.Error())
177 | }
178 | if schema == v.Schema &&
179 | (aliyunDMSOnlineDdlReg.MatchString(name) ||
180 | aliyunDMSOnlineDdlReg2.MatchString(name) ||
181 | ghostOnlineDdlReg.MatchString(name)) {
182 | isOnlineDdlTable = true
183 | break
184 | }
185 | }
186 | if isSyncTable || isOnlineDdlTable {
187 | deepCopy, err := table.DeepCopy()
188 | if err != nil {
189 | log.Fatalf("table deep copy failed: %s", err.Error())
190 | }
191 | if ddlStatement.IsAlterTable { // alter table
192 | err = metas.TableDdlHandle(deepCopy, ddlStatement.RawSql)
193 | if err != nil {
194 | log.Fatalf("ddl event handle failed: %s", err.Error())
195 | }
196 | err = b.inputPlugin.metaPlugin.Update(deepCopy)
197 | if err != nil {
198 | log.Fatalf("ddl event handle failed: %s", err.Error())
199 | }
200 | } else if ddlStatement.IsCreateTable {
201 | err = metas.TableDdlHandle(deepCopy, ddlStatement.RawSql)
202 | if err != nil {
203 | log.Fatalf("ddl event handle failed: %s", err.Error())
204 | }
205 | err = b.inputPlugin.metaPlugin.Add(deepCopy)
206 | if err != nil {
207 | log.Fatalf("ddl event handle failed: %s", err.Error())
208 | }
209 | } else if ddlStatement.IsDropTable { // drop table
210 | err = b.inputPlugin.metaPlugin.Delete(schema, name)
211 | if err != nil {
212 | log.Fatalf("ddl event handle failed: %s", err.Error())
213 | }
214 | } else if ddlStatement.IsRenameTable { // rename table
215 | err = metas.TableDdlHandle(deepCopy, ddlStatement.RawSql)
216 | if err != nil {
217 | log.Fatalf("ddl event handle failed: %s", err.Error())
218 | }
219 | err = b.inputPlugin.metaPlugin.Update(deepCopy)
220 | if err != nil {
221 | log.Fatalf("ddl event handle failed: %s", err.Error())
222 | }
223 | } else if ddlStatement.IsTruncateTable { // truncate table
224 | return
225 | }
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/inputs/mysql/mysql_utils.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/siddontang/go-log/log"
7 | "github.com/sqlpub/qin-cdc/config"
8 | "github.com/sqlpub/qin-cdc/metas"
9 | "math/rand"
10 | "time"
11 | )
12 |
13 | const (
14 | PluginName = "mysql"
15 | DefaultCharset string = "utf8"
16 | )
17 |
18 | func getConn(conf *config.MysqlConfig) (db *sql.DB, err error) {
19 | dsn := fmt.Sprintf(
20 | "%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4&timeout=3s",
21 | conf.UserName, conf.Password,
22 | conf.Host, conf.Port)
23 | db, err = sql.Open("mysql", dsn)
24 | if err != nil {
25 | return db, err
26 | }
27 | db.SetConnMaxLifetime(time.Minute * 3)
28 | db.SetMaxOpenConns(2)
29 | db.SetMaxIdleConns(2)
30 | return db, err
31 | }
32 |
33 | func closeConn(db *sql.DB) {
34 | if db != nil {
35 | _ = db.Close()
36 | }
37 | }
38 |
39 | func getServerId(conf *config.MysqlConfig) uint32 {
40 | serverId := conf.Options.ServerId
41 | if serverId > 0 {
42 | return uint32(serverId)
43 | } else if serverId < 0 {
44 | log.Fatalf("options server-id: %v is invalid, must be greater than 0", serverId)
45 | }
46 | // default generate rand value [1001, 2000]
47 | return uint32(rand.New(rand.NewSource(time.Now().Unix())).Intn(1000)) + 1001
48 | }
49 |
50 | func deserialize(raw interface{}, column metas.Column) interface{} {
51 | if raw == nil {
52 | return nil
53 | }
54 |
55 | ret := raw
56 | if column.RawType == "text" || column.RawType == "json" {
57 | _, ok := raw.([]uint8)
58 | if ok {
59 | ret = string(raw.([]uint8))
60 | }
61 | }
62 | return ret
63 | }
64 |
--------------------------------------------------------------------------------
/metas/mysql_ddl_parse.go:
--------------------------------------------------------------------------------
1 | package metas
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "github.com/pingcap/tidb/pkg/parser"
8 | "github.com/pingcap/tidb/pkg/parser/ast"
9 | "github.com/pingcap/tidb/pkg/parser/format"
10 | "github.com/pingcap/tidb/pkg/parser/mysql"
11 | "github.com/pingcap/tidb/pkg/parser/test_driver"
12 | )
13 |
14 | var p *parser.Parser
15 |
16 | func init() {
17 | p = parser.New()
18 | }
19 |
20 | func parse(sql string) (*ast.StmtNode, error) {
21 | stmtNodes, _, err := p.ParseSQL(sql)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | return &stmtNodes[0], nil
27 | }
28 |
29 | func columnDefParse(columnDef *ast.ColumnDef) Column {
30 | tableColumn := Column{}
31 | tableColumn.Name = columnDef.Name.String()
32 | tableColumn.RawType = columnDef.Tp.String()
33 | switch columnDef.Tp.GetType() {
34 | case mysql.TypeEnum:
35 | tableColumn.Type = TypeEnum
36 | case mysql.TypeSet:
37 | tableColumn.Type = TypeSet
38 | case mysql.TypeTimestamp:
39 | tableColumn.Type = TypeTimestamp
40 | case mysql.TypeDatetime:
41 | tableColumn.Type = TypeDatetime
42 | case mysql.TypeDuration:
43 | tableColumn.Type = TypeTime
44 | case mysql.TypeDouble, mysql.TypeFloat:
45 | tableColumn.Type = TypeFloat
46 | case mysql.TypeNewDecimal:
47 | tableColumn.Type = TypeDecimal
48 | case mysql.TypeBit:
49 | tableColumn.Type = TypeBit
50 | case mysql.TypeVarchar, mysql.TypeString, mysql.TypeVarString:
51 | tableColumn.Type = TypeString
52 | case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong, mysql.TypeLonglong, mysql.TypeYear:
53 | tableColumn.Type = TypeNumber
54 | case mysql.TypeDate:
55 | tableColumn.Type = TypeDate
56 | case mysql.TypeJSON:
57 | tableColumn.Type = TypeJson
58 | case mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeBlob, mysql.TypeLongBlob:
59 | tableColumn.Type = TypeBinary
60 | }
61 | for _, columnOption := range columnDef.Options {
62 | switch columnOption.Tp {
63 | case ast.ColumnOptionNoOption:
64 | case ast.ColumnOptionPrimaryKey:
65 | tableColumn.IsPrimaryKey = true
66 | case ast.ColumnOptionNotNull:
67 | case ast.ColumnOptionAutoIncrement:
68 | case ast.ColumnOptionDefaultValue:
69 | case ast.ColumnOptionUniqKey:
70 | case ast.ColumnOptionNull:
71 | case ast.ColumnOptionOnUpdate: // For Timestamp and Datetime only.
72 | case ast.ColumnOptionFulltext:
73 | case ast.ColumnOptionComment:
74 | switch exp := columnOption.Expr.(type) {
75 | case *test_driver.ValueExpr:
76 | tableColumn.Comment = exp.Datum.GetString()
77 | }
78 | case ast.ColumnOptionGenerated:
79 | case ast.ColumnOptionReference:
80 | case ast.ColumnOptionCollate:
81 | case ast.ColumnOptionCheck:
82 | case ast.ColumnOptionColumnFormat:
83 | case ast.ColumnOptionStorage:
84 | case ast.ColumnOptionAutoRandom:
85 | }
86 | }
87 | return tableColumn
88 | }
89 |
90 | func NewTable(createDdlSql string) (*Table, error) {
91 | tab := &Table{}
92 | err := TableDdlHandle(tab, createDdlSql)
93 | if err != nil {
94 | return nil, err
95 | }
96 | return tab, nil
97 | }
98 |
99 | func TableDdlHandle(tab *Table, sql string) error {
100 | astNode, err := parse(sql)
101 | if err != nil {
102 | return errors.New(fmt.Sprintf("parse error: %v\n", err.Error()))
103 | }
104 | switch t := (*astNode).(type) {
105 | case *ast.AlterTableStmt:
106 | if t.Table.Schema.String() != tab.Schema || t.Table.Name.String() != tab.Name {
107 | return errors.New(fmt.Sprintf("operation object do not match error: table: %s.%s and sql: %s", tab.Schema, tab.Name, sql))
108 | }
109 | for _, alterTableSpec := range t.Specs {
110 | switch alterTableSpec.Tp {
111 | case ast.AlterTableOption:
112 | case ast.AlterTableAddColumns:
113 | relativeColumn := ""
114 | isFirst := false
115 | switch alterTableSpec.Position.Tp {
116 | case ast.ColumnPositionNone:
117 | case ast.ColumnPositionFirst:
118 | isFirst = true
119 | case ast.ColumnPositionAfter:
120 | relativeColumn = alterTableSpec.Position.RelativeColumn.Name.String()
121 | }
122 | for _, column := range alterTableSpec.NewColumns {
123 | tableColumn := columnDefParse(column)
124 | if tableColumn.IsPrimaryKey {
125 | tab.PrimaryKeyColumns = append(tab.PrimaryKeyColumns, tableColumn)
126 | }
127 | if relativeColumn != "" {
128 | for i, column2 := range tab.Columns {
129 | // add new column to relative column after
130 | if column2.Name == relativeColumn {
131 | tab.Columns = append(tab.Columns[:i+1], append([]Column{tableColumn}, tab.Columns[i+1:]...)...)
132 | }
133 | }
134 | } else if isFirst {
135 | // add new column to first
136 | tab.Columns = append([]Column{tableColumn}, tab.Columns...)
137 | } else {
138 | tab.Columns = append(tab.Columns, tableColumn)
139 | }
140 | }
141 | case ast.AlterTableAddConstraint:
142 | case ast.AlterTableDropColumn:
143 | oldColumnName := alterTableSpec.OldColumnName.Name.String()
144 | for i, column := range tab.Columns {
145 | if column.Name == oldColumnName {
146 | tab.Columns = append(tab.Columns[:i], tab.Columns[i+1:]...)
147 | }
148 | }
149 | case ast.AlterTableDropPrimaryKey:
150 | case ast.AlterTableDropIndex:
151 | case ast.AlterTableDropForeignKey:
152 | case ast.AlterTableModifyColumn:
153 | relativeColumn := ""
154 | isFirst := false
155 | switch alterTableSpec.Position.Tp {
156 | case ast.ColumnPositionNone:
157 | case ast.ColumnPositionFirst:
158 | isFirst = true
159 | case ast.ColumnPositionAfter:
160 | relativeColumn = alterTableSpec.Position.RelativeColumn.Name.String()
161 | }
162 | for _, column := range alterTableSpec.NewColumns {
163 | tableColumn := columnDefParse(column)
164 | if tableColumn.IsPrimaryKey {
165 | tab.PrimaryKeyColumns = append(tab.PrimaryKeyColumns, tableColumn)
166 | }
167 | if relativeColumn != "" {
168 | for i, column2 := range tab.Columns {
169 | // delete old column
170 | if column2.Name == tableColumn.Name {
171 | tab.Columns = append(tab.Columns[:i], tab.Columns[i+1:]...)
172 | }
173 | }
174 | for i, column2 := range tab.Columns {
175 | // add new column to relative column after
176 | if column2.Name == relativeColumn {
177 | tab.Columns = append(tab.Columns[:i+1], append([]Column{tableColumn}, tab.Columns[i+1:]...)...)
178 | }
179 | }
180 | } else if isFirst {
181 | for i, column2 := range tab.Columns {
182 | // delete old column
183 | if column2.Name == tableColumn.Name {
184 | tab.Columns = append(tab.Columns[:i], tab.Columns[i+1:]...)
185 | }
186 | }
187 | // add new column to first
188 | tab.Columns = append([]Column{tableColumn}, tab.Columns...)
189 | } else {
190 | // overwrite column
191 | for i, column2 := range tab.Columns {
192 | if column2.Name == tableColumn.Name {
193 | tab.Columns[i] = tableColumn
194 | break
195 | }
196 | }
197 | }
198 | }
199 | case ast.AlterTableChangeColumn:
200 | oldColumnName := alterTableSpec.OldColumnName.Name.String()
201 | relativeColumn := ""
202 | isFirst := false
203 | switch alterTableSpec.Position.Tp {
204 | case ast.ColumnPositionNone:
205 | case ast.ColumnPositionFirst:
206 | isFirst = true
207 | case ast.ColumnPositionAfter:
208 | relativeColumn = alterTableSpec.Position.RelativeColumn.Name.String()
209 | }
210 | for _, column := range alterTableSpec.NewColumns {
211 | tableColumn := columnDefParse(column)
212 | if tableColumn.IsPrimaryKey {
213 | tab.PrimaryKeyColumns = append(tab.PrimaryKeyColumns, tableColumn)
214 | }
215 | if relativeColumn != "" {
216 | for i, column2 := range tab.Columns {
217 | // delete old column
218 | if column2.Name == oldColumnName {
219 | tab.Columns = append(tab.Columns[:i], tab.Columns[i+1:]...)
220 | }
221 | }
222 | for i, column2 := range tab.Columns {
223 | // add new column to relative column after
224 | if column2.Name == relativeColumn {
225 | tab.Columns = append(tab.Columns[:i+1], append([]Column{tableColumn}, tab.Columns[i+1:]...)...)
226 | }
227 | }
228 | } else if isFirst {
229 | for i, column2 := range tab.Columns {
230 | // delete old column
231 | if column2.Name == oldColumnName {
232 | tab.Columns = append(tab.Columns[:i], tab.Columns[i+1:]...)
233 | }
234 | }
235 | // add new column to first
236 | tab.Columns = append([]Column{tableColumn}, tab.Columns...)
237 | } else {
238 | // overwrite column
239 | for i, column2 := range tab.Columns {
240 | if column2.Name == oldColumnName {
241 | tab.Columns[i] = tableColumn
242 | break
243 | }
244 | }
245 | }
246 | }
247 | case ast.AlterTableRenameColumn:
248 | oldColumnName := alterTableSpec.OldColumnName.Name.String()
249 | newColumnName := alterTableSpec.NewColumnName.Name.String()
250 | for i, column := range tab.Columns {
251 | if column.Name == oldColumnName {
252 | tab.Columns[i].Name = newColumnName
253 | break
254 | }
255 | }
256 | case ast.AlterTableRenameTable:
257 | newTableName := alterTableSpec.NewTable.Name.String()
258 | newSchemaName := alterTableSpec.NewTable.Schema.String()
259 | tab.Name = newTableName
260 | if newSchemaName != "" {
261 | tab.Schema = newSchemaName
262 | }
263 | case ast.AlterTableAlterColumn:
264 | case ast.AlterTableLock:
265 | case ast.AlterTableWriteable:
266 | case ast.AlterTableAlgorithm:
267 | case ast.AlterTableRenameIndex:
268 | case ast.AlterTableForce:
269 | case ast.AlterTableAddPartitions:
270 | case ast.AlterTablePartitionAttributes:
271 | case ast.AlterTablePartitionOptions:
272 | case ast.AlterTableCoalescePartitions:
273 | case ast.AlterTableDropPartition:
274 | case ast.AlterTableTruncatePartition:
275 | case ast.AlterTablePartition:
276 | case ast.AlterTableEnableKeys:
277 | case ast.AlterTableDisableKeys:
278 | case ast.AlterTableRemovePartitioning:
279 | case ast.AlterTableWithValidation:
280 | case ast.AlterTableWithoutValidation:
281 | case ast.AlterTableSecondaryLoad:
282 | case ast.AlterTableSecondaryUnload:
283 | case ast.AlterTableRebuildPartition:
284 | case ast.AlterTableReorganizePartition:
285 | case ast.AlterTableCheckPartitions:
286 | case ast.AlterTableExchangePartition:
287 | case ast.AlterTableOptimizePartition:
288 | case ast.AlterTableRepairPartition:
289 | case ast.AlterTableImportPartitionTablespace:
290 | case ast.AlterTableDiscardPartitionTablespace:
291 | case ast.AlterTableAlterCheck:
292 | case ast.AlterTableDropCheck:
293 | case ast.AlterTableImportTablespace:
294 | case ast.AlterTableDiscardTablespace:
295 | case ast.AlterTableIndexInvisible:
296 | case ast.AlterTableOrderByColumns:
297 | case ast.AlterTableSetTiFlashReplica:
298 | case ast.AlterTableAddStatistics:
299 | case ast.AlterTableDropStatistics:
300 | case ast.AlterTableAttributes:
301 | case ast.AlterTableCache:
302 | case ast.AlterTableNoCache:
303 | case ast.AlterTableStatsOptions:
304 | case ast.AlterTableDropFirstPartition:
305 | case ast.AlterTableAddLastPartition:
306 | case ast.AlterTableReorganizeLastPartition:
307 | case ast.AlterTableReorganizeFirstPartition:
308 | case ast.AlterTableRemoveTTL:
309 | }
310 | }
311 | case *ast.CreateTableStmt:
312 | if tab.Schema != "" || tab.Name != "" {
313 | return errors.New("table object not empty error: create table sql table object must be empty")
314 | }
315 | tab.Schema = t.Table.Schema.String()
316 | tab.Name = t.Table.Name.String()
317 | for _, tableOption := range t.Options {
318 | switch tableOption.Tp {
319 | case ast.TableOptionComment:
320 | tab.Comment = tableOption.StrValue
321 | default:
322 | continue
323 | }
324 | }
325 | tab.Columns = []Column{}
326 | for _, columnDef := range t.Cols {
327 | tableColumn := columnDefParse(columnDef)
328 | if tableColumn.IsPrimaryKey {
329 | tab.PrimaryKeyColumns = append(tab.PrimaryKeyColumns, tableColumn)
330 | }
331 | tab.Columns = append(tab.Columns, tableColumn)
332 | }
333 | for _, constraint := range t.Constraints {
334 | switch constraint.Tp {
335 | case ast.ConstraintPrimaryKey:
336 | for _, key := range constraint.Keys {
337 | keyName := key.Column.Name.String()
338 | for i, column := range tab.Columns {
339 | if keyName == column.Name {
340 | tab.Columns[i].IsPrimaryKey = true
341 | tab.PrimaryKeyColumns = append(tab.PrimaryKeyColumns, tab.Columns[i])
342 | break
343 | }
344 | }
345 | }
346 | case ast.ConstraintKey:
347 | case ast.ConstraintIndex:
348 | case ast.ConstraintUniq:
349 | case ast.ConstraintUniqKey:
350 | case ast.ConstraintUniqIndex:
351 | case ast.ConstraintForeignKey:
352 | case ast.ConstraintFulltext:
353 | case ast.ConstraintCheck:
354 | default:
355 | }
356 | }
357 | case *ast.RenameTableStmt:
358 | for _, tableToTable := range t.TableToTables {
359 | if tableToTable.OldTable.Schema.String() != tab.Schema || tableToTable.OldTable.Name.String() != tab.Name {
360 | return errors.New(fmt.Sprintf("operation object do not match error: table: %s.%s and sql: %s", tab.Schema, tab.Name, sql))
361 | }
362 | newSchema := tableToTable.NewTable.Schema.String()
363 | newTable := tableToTable.NewTable.Name.String()
364 | tab.Schema = newSchema
365 | tab.Name = newTable
366 | break
367 | }
368 | case *ast.DropTableStmt:
369 | return nil
370 | case *ast.TruncateTableStmt:
371 | return nil
372 | default:
373 | return errors.New(fmt.Sprintf("not support table ddl handle, type: %v", t))
374 | }
375 | return nil
376 | }
377 |
378 | func TableDdlParser(sql string, schema string) ([]*DdlStatement, error) {
379 | astNode, err := parse(sql)
380 | if err != nil {
381 | return nil, errors.New(fmt.Sprintf("parse error: %v\n", err.Error()))
382 | }
383 | ddls := make([]*DdlStatement, 0)
384 | switch t := (*astNode).(type) {
385 | case *ast.AlterTableStmt:
386 | ddl := &DdlStatement{}
387 | tableSchema := t.Table.Schema.String()
388 | if tableSchema != "" {
389 | ddl.Schema = tableSchema
390 | } else {
391 | ddl.Schema = schema
392 | t.Table.Schema.L = schema
393 | t.Table.Schema.O = schema
394 | }
395 | ddl.Name = t.Table.Name.String()
396 | ddl.RawSql, err = TableRestore(t)
397 | if err != nil {
398 | return nil, err
399 | }
400 | ddl.IsAlterTable = true
401 | ddls = append(ddls, ddl)
402 | case *ast.CreateTableStmt:
403 | ddl := &DdlStatement{}
404 | tableSchema := t.Table.Schema.String()
405 | if tableSchema != "" {
406 | ddl.Schema = tableSchema
407 | } else {
408 | ddl.Schema = schema
409 | t.Table.Schema.L = schema
410 | t.Table.Schema.O = schema
411 | }
412 | ddl.Name = t.Table.Name.String()
413 | // CREATE TABLE ... LIKE Statement
414 | if t.ReferTable != nil {
415 | referTableSchema := t.ReferTable.Schema.String()
416 | if referTableSchema != "" {
417 | ddl.CreateTable.ReferTable.Schema = referTableSchema
418 | } else {
419 | ddl.CreateTable.ReferTable.Schema = schema
420 | t.ReferTable.Schema.L = schema
421 | t.ReferTable.Schema.O = schema
422 | }
423 | ddl.CreateTable.ReferTable.Name = t.ReferTable.Name.String()
424 | ddl.CreateTable.IsLikeCreateTable = true
425 | }
426 | // CREATE TABLE ... SELECT Statement
427 | if t.Select != nil {
428 | ddl.CreateTable.IsSelectCreateTable = true
429 | ddl.RawSql, err = TableRestore(t.Select)
430 | if err != nil {
431 | return nil, err
432 | }
433 | }
434 | ddl.IsCreateTable = true
435 | ddl.RawSql, err = TableRestore(astNode)
436 | if err != nil {
437 | return nil, err
438 | }
439 | ddls = append(ddls, ddl)
440 | case *ast.DropTableStmt:
441 | for _, tableName := range t.Tables {
442 | ddl := &DdlStatement{}
443 | tableSchema := tableName.Schema.String()
444 | if tableSchema != "" {
445 | ddl.Schema = tableSchema
446 | } else {
447 | ddl.Schema = schema
448 | tableName.Schema.L = schema
449 | tableName.Schema.O = schema
450 | }
451 | ddl.Name = tableName.Name.String()
452 | ddl.RawSql, err = TableRestore(tableName)
453 | if err != nil {
454 | return nil, err
455 | }
456 | ddl.IsDropTable = true
457 | ddls = append(ddls, ddl)
458 | }
459 | case *ast.RenameTableStmt:
460 | for _, tableToTable := range t.TableToTables {
461 | ddl := &DdlStatement{}
462 | oldSchema := tableToTable.OldTable.Schema.String()
463 | if oldSchema != "" {
464 | ddl.Schema = oldSchema
465 | } else {
466 | ddl.Schema = schema
467 | tableToTable.OldTable.Schema.L = schema
468 | tableToTable.OldTable.Schema.O = schema
469 | }
470 | ddl.Name = tableToTable.OldTable.Name.String()
471 |
472 | newSchema := tableToTable.NewTable.Schema.String()
473 | if newSchema == "" {
474 | tableToTable.NewTable.Schema.L = schema
475 | tableToTable.NewTable.Schema.O = schema
476 | }
477 | ddl.RawSql, err = TableRestore(tableToTable)
478 | if err != nil {
479 | return nil, err
480 | }
481 | ddl.IsRenameTable = true
482 | ddls = append(ddls, ddl)
483 | }
484 | case *ast.TruncateTableStmt:
485 | ddl := &DdlStatement{}
486 | tableSchema := t.Table.Schema.String()
487 | if tableSchema != "" {
488 | ddl.Schema = tableSchema
489 | } else {
490 | ddl.Schema = schema
491 | t.Table.Schema.L = schema
492 | t.Table.Schema.O = schema
493 | }
494 | ddl.Name = t.Table.Name.String()
495 | ddl.RawSql, err = TableRestore(t)
496 | if err != nil {
497 | return nil, err
498 | }
499 | ddl.IsTruncateTable = true
500 | ddls = append(ddls, ddl)
501 | default:
502 | return nil, errors.New(fmt.Sprintf("not support table ddl parser, type: %v", t))
503 | }
504 | return ddls, nil
505 | }
506 |
507 | func TableRestore(astNode interface{}) (rawSql string, err error) {
508 | switch t := astNode.(type) {
509 | case *ast.AlterTableStmt:
510 | buf := new(bytes.Buffer)
511 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
512 | err = t.Restore(restoreCtx)
513 | if err != nil {
514 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
515 | }
516 | return buf.String(), nil
517 | case *ast.CreateTableStmt:
518 | buf := new(bytes.Buffer)
519 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
520 | err = t.Restore(restoreCtx)
521 | if err != nil {
522 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
523 | }
524 | return buf.String(), nil
525 | case *ast.TruncateTableStmt:
526 | buf := new(bytes.Buffer)
527 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
528 | err = t.Restore(restoreCtx)
529 | if err != nil {
530 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
531 | }
532 | return buf.String(), nil
533 | case ast.ResultSetNode: // CREATE TABLE ... SELECT Statement
534 | buf := new(bytes.Buffer)
535 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
536 | err = t.Restore(restoreCtx)
537 | if err != nil {
538 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
539 | }
540 | return buf.String(), nil
541 | case *ast.TableName: // DropTableStmt
542 | buf := new(bytes.Buffer)
543 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
544 | restoreCtx.WriteKeyWord("DROP TABLE ")
545 | err = t.Restore(restoreCtx)
546 | if err != nil {
547 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
548 | }
549 | return buf.String(), nil
550 | case *ast.TableToTable: // RenameTableStmt
551 | buf := new(bytes.Buffer)
552 | restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
553 | restoreCtx.WriteKeyWord("RENAME TABLE ")
554 | err = t.Restore(restoreCtx)
555 | if err != nil {
556 | return "", errors.New(fmt.Sprintf("table restore parse error: %v", err.Error()))
557 | }
558 | return buf.String(), nil
559 | default:
560 | return "", errors.New(fmt.Sprintf("not support table restore, type: %v", t))
561 | }
562 | }
563 |
--------------------------------------------------------------------------------
/metas/routers.go:
--------------------------------------------------------------------------------
1 | package metas
2 |
3 | import (
4 | "github.com/mitchellh/mapstructure"
5 | "github.com/siddontang/go-log/log"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type Router struct {
11 | SourceSchema string `mapstructure:"source-schema"`
12 | SourceTable string `mapstructure:"source-table"`
13 | TargetSchema string `mapstructure:"target-schema"`
14 | TargetTable string `mapstructure:"target-table"`
15 | DmlTopic string `mapstructure:"dml-topic"`
16 | ColumnsMapper ColumnsMapper
17 | }
18 |
19 | type ColumnsMapper struct {
20 | PrimaryKeys []string // source
21 | SourceColumns []string
22 | TargetColumns []string
23 | MapMapper map[string]string
24 | MapMapperOrder []string
25 | }
26 |
27 | type Routers struct {
28 | Raws []*Router
29 | Maps map[string]*Router
30 | }
31 |
32 | var MapRouterKeyDelimiter = ":"
33 |
34 | func (r *Routers) InitRouters(config map[string]interface{}) error {
35 | r.initRaws(config)
36 | r.initMaps()
37 | return nil
38 | }
39 |
40 | func (r *Routers) initRaws(config map[string]interface{}) {
41 | configRules := config["routers"]
42 | err := mapstructure.Decode(configRules, &r.Raws)
43 | if err != nil {
44 | log.Fatal("output.config.routers config parsing failed. err: ", err.Error())
45 | }
46 | }
47 |
48 | func (r *Routers) initMaps() {
49 | if len(r.Raws) == 0 {
50 | log.Fatal("routers config cannot be empty")
51 | }
52 | r.Maps = make(map[string]*Router)
53 | for _, router := range r.Raws {
54 | r.Maps[GenerateMapRouterKey(router.SourceSchema, router.SourceTable)] = router
55 | }
56 | }
57 |
58 | func GenerateMapRouterKey(schema string, table string) string {
59 | return schema + MapRouterKeyDelimiter + table
60 | }
61 |
62 | func GenerateMapRouterVersionKey(schema string, table string, version uint) string {
63 | return schema + MapRouterKeyDelimiter + table + MapRouterKeyDelimiter + strconv.Itoa(int(version))
64 | }
65 |
66 | func SplitMapRouterKey(key string) (schema string, table string) {
67 | splits := strings.Split(key, MapRouterKeyDelimiter)
68 | return splits[0], splits[1]
69 | }
70 |
71 | func SplitMapRouterVersionKey(key string) (schema string, table string, version uint) {
72 | splits := strings.Split(key, MapRouterKeyDelimiter)
73 | tmpVersion, _ := strconv.Atoi(splits[2])
74 | return splits[0], splits[1], uint(tmpVersion)
75 | }
76 |
--------------------------------------------------------------------------------
/metas/table.go:
--------------------------------------------------------------------------------
1 | package metas
2 |
3 | import "github.com/goccy/go-json"
4 |
5 | type ColumnType = int
6 |
7 | const (
8 | TypeNumber ColumnType = iota + 1 // tinyint, smallint, mediumint, int, bigint, year
9 | TypeFloat // float, double
10 | TypeEnum // enum
11 | TypeSet // set
12 | TypeString // other
13 | TypeDatetime // datetime
14 | TypeTimestamp // timestamp
15 | TypeDate // date
16 | TypeTime // time
17 | TypeBit // bit
18 | TypeJson // json
19 | TypeDecimal // decimal
20 | TypeBinary // binary
21 | )
22 |
23 | type Table struct {
24 | Schema string
25 | Name string
26 | Comment string
27 | Columns []Column
28 | PrimaryKeyColumns []Column
29 | Version uint
30 | }
31 |
32 | type Column struct {
33 | Name string
34 | Type ColumnType
35 | RawType string
36 | Comment string
37 | IsPrimaryKey bool
38 | }
39 |
40 | type DdlStatement struct {
41 | Schema string
42 | Name string
43 | RawSql string
44 | IsAlterTable bool
45 | IsCreateTable bool
46 | CreateTable struct {
47 | IsLikeCreateTable bool
48 | ReferTable struct {
49 | Schema string
50 | Name string
51 | }
52 | IsSelectCreateTable bool
53 | SelectRawSql string
54 | }
55 | IsDropTable bool
56 | IsRenameTable bool
57 | IsTruncateTable bool
58 | }
59 |
60 | func (t *Table) DeepCopy() (*Table, error) {
61 | b, err := json.Marshal(t)
62 | if err != nil {
63 | return nil, err
64 | }
65 | ret := &Table{}
66 | if err = json.Unmarshal(b, ret); err != nil {
67 | panic(err)
68 | }
69 | return ret, nil
70 | }
71 |
--------------------------------------------------------------------------------
/metrics/metrics.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "github.com/prometheus/client_golang/prometheus"
5 | "github.com/prometheus/client_golang/prometheus/promauto"
6 | )
7 |
8 | var OpsStartTime = prometheus.NewGauge(prometheus.GaugeOpts{
9 | Namespace: "qin_cdc",
10 | Subsystem: "start",
11 | Name: "time",
12 | Help: "qin-cdc startup timestamp(s).",
13 | })
14 |
15 | var OpsReadProcessed = promauto.NewCounter(prometheus.CounterOpts{
16 | Name: "qin_cdc_read_processed_ops_total",
17 | Help: "The total number of read processed events",
18 | })
19 |
20 | var OpsWriteProcessed = promauto.NewCounter(prometheus.CounterOpts{
21 | Name: "qin_cdc_write_processed_ops_total",
22 | Help: "The total number of write processed events",
23 | })
24 |
25 | var DelayReadTime = prometheus.NewGauge(prometheus.GaugeOpts{
26 | Namespace: "qin_cdc",
27 | Subsystem: "read_delay",
28 | Name: "time_seconds",
29 | Help: "Delay in seconds to read the binlog at the source.",
30 | })
31 |
32 | var DelayWriteTime = prometheus.NewGauge(prometheus.GaugeOpts{
33 | Namespace: "qin_cdc",
34 | Subsystem: "write_delay",
35 | Name: "time_seconds",
36 | Help: "Delay in seconds to write at the destination.",
37 | })
38 |
39 | func init() {
40 | prometheus.MustRegister(OpsStartTime, DelayReadTime, DelayWriteTime)
41 | }
42 |
--------------------------------------------------------------------------------
/outputs/doris/doris.go:
--------------------------------------------------------------------------------
1 | package doris
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/mitchellh/mapstructure"
7 | "github.com/siddontang/go-log/log"
8 | "github.com/sqlpub/qin-cdc/config"
9 | "github.com/sqlpub/qin-cdc/core"
10 | "github.com/sqlpub/qin-cdc/metas"
11 | "github.com/sqlpub/qin-cdc/metrics"
12 | "io"
13 | "net/http"
14 | "strings"
15 | "time"
16 | )
17 |
18 | type OutputPlugin struct {
19 | *config.DorisConfig
20 | Done chan bool
21 | metas *core.Metas
22 | msgTxnBuffer struct {
23 | size int
24 | tableMsgMap map[string][]*core.Msg
25 | }
26 | client *http.Client
27 | transport *http.Transport
28 | lastPosition string
29 | }
30 |
31 | func (o *OutputPlugin) Configure(conf map[string]interface{}) error {
32 | o.DorisConfig = &config.DorisConfig{}
33 | var targetConf = conf["target"]
34 | if err := mapstructure.Decode(targetConf, o.DorisConfig); err != nil {
35 | return err
36 | }
37 | return nil
38 | }
39 |
40 | func (o *OutputPlugin) NewOutput(metas *core.Metas) {
41 | o.Done = make(chan bool)
42 | o.metas = metas
43 | // options handle
44 | if o.DorisConfig.Options.BatchSize == 0 {
45 | o.DorisConfig.Options.BatchSize = DefaultBatchSize
46 | }
47 | if o.DorisConfig.Options.BatchIntervalMs == 0 {
48 | o.DorisConfig.Options.BatchIntervalMs = DefaultBatchIntervalMs
49 | }
50 | o.msgTxnBuffer.size = 0
51 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
52 |
53 | o.transport = &http.Transport{}
54 | o.client = &http.Client{
55 | Transport: o.transport,
56 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
57 | req.Header.Add("Authorization", "Basic "+o.auth())
58 | // log.Debugf("重定向请求到be: %v", req.URL)
59 | return nil // return nil nil回重定向。
60 | },
61 | }
62 | }
63 |
64 | func (o *OutputPlugin) Start(out chan *core.Msg, pos core.Position) {
65 | // first pos
66 | o.lastPosition = pos.Get()
67 | go func() {
68 | ticker := time.NewTicker(time.Millisecond * time.Duration(o.Options.BatchIntervalMs))
69 | defer ticker.Stop()
70 | for {
71 | select {
72 | case data := <-out:
73 | switch data.Type {
74 | case core.MsgCtl:
75 | o.lastPosition = data.InputContext.Pos
76 | case core.MsgDML:
77 | o.appendMsgTxnBuffer(data)
78 | if o.msgTxnBuffer.size >= o.DorisConfig.Options.BatchSize {
79 | o.flushMsgTxnBuffer(pos)
80 | }
81 | }
82 | case <-ticker.C:
83 | o.flushMsgTxnBuffer(pos)
84 | case <-o.Done:
85 | o.flushMsgTxnBuffer(pos)
86 | return
87 | }
88 |
89 | }
90 | }()
91 | }
92 |
93 | func (o *OutputPlugin) Close() {
94 | log.Infof("output is closing...")
95 | close(o.Done)
96 | <-o.Done
97 | log.Infof("output is closed")
98 | }
99 |
100 | func (o *OutputPlugin) appendMsgTxnBuffer(msg *core.Msg) {
101 | key := metas.GenerateMapRouterKey(msg.Database, msg.Table)
102 | o.msgTxnBuffer.tableMsgMap[key] = append(o.msgTxnBuffer.tableMsgMap[key], msg)
103 | o.msgTxnBuffer.size += 1
104 | }
105 |
106 | func (o *OutputPlugin) flushMsgTxnBuffer(pos core.Position) {
107 | defer func() {
108 | // flush position
109 | err := pos.Update(o.lastPosition)
110 | if err != nil {
111 | log.Fatalf(err.Error())
112 | }
113 | }()
114 |
115 | if o.msgTxnBuffer.size == 0 {
116 | return
117 | }
118 | // table level export
119 | for k, msgs := range o.msgTxnBuffer.tableMsgMap {
120 | columnsMapper := o.metas.Routers.Maps[k].ColumnsMapper
121 | targetSchema := o.metas.Routers.Maps[k].TargetSchema
122 | targetTable := o.metas.Routers.Maps[k].TargetTable
123 | err := o.execute(msgs, columnsMapper, targetSchema, targetTable)
124 | if err != nil {
125 | log.Fatalf("do %s bulk err %v", PluginName, err)
126 | }
127 | }
128 | o.clearMsgTxnBuffer()
129 | }
130 |
131 | func (o *OutputPlugin) clearMsgTxnBuffer() {
132 | o.msgTxnBuffer.size = 0
133 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
134 | }
135 |
136 | func (o *OutputPlugin) execute(msgs []*core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) error {
137 | if len(msgs) == 0 {
138 | return nil
139 | }
140 | var jsonList []string
141 |
142 | jsonList = o.generateJson(msgs)
143 | for _, s := range jsonList {
144 | log.Debugf("%s load %s.%s row data: %v", PluginName, targetSchema, targetTable, s)
145 | }
146 | log.Debugf("%s bulk load %s.%s row data num: %d", PluginName, targetSchema, targetTable, len(jsonList))
147 | var err error
148 | for i := 0; i < RetryCount; i++ {
149 | err = o.sendData(jsonList, columnsMapper, targetSchema, targetTable)
150 | if err != nil {
151 | log.Warnf("send data failed, err: %v, execute retry...", err.Error())
152 | if i+1 == RetryCount {
153 | break
154 | }
155 | time.Sleep(time.Duration(RetryInterval*(i+1)) * time.Second)
156 | continue
157 | }
158 | break
159 | }
160 | return err
161 | }
162 |
163 | func (o *OutputPlugin) sendData(content []string, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) error {
164 | loadUrl := fmt.Sprintf("http://%s:%d/api/%s/%s/_stream_load",
165 | o.Host, o.LoadPort, targetSchema, targetTable)
166 | newContent := `[` + strings.Join(content, ",") + `]`
167 | req, _ := http.NewRequest("PUT", loadUrl, strings.NewReader(newContent))
168 |
169 | // req.Header.Add
170 | req.Header.Add("Authorization", "Basic "+o.auth())
171 | req.Header.Add("Expect", "100-continue")
172 | req.Header.Add("strict_mode", "true")
173 | // req.Header.Add("label", "39c25a5c-7000-496e-a98e-348a264c81de")
174 | req.Header.Add("format", "json")
175 | req.Header.Add("strip_outer_array", "true")
176 | req.Header.Add("merge_type", "MERGE")
177 | req.Header.Add("delete", DeleteCondition)
178 |
179 | var columnArray []string
180 | for _, column := range columnsMapper.SourceColumns {
181 | columnArray = append(columnArray, column)
182 | }
183 | columnArray = append(columnArray, DeleteColumn)
184 | columns := fmt.Sprintf("%s", strings.Join(columnArray, ","))
185 | req.Header.Add("columns", columns)
186 |
187 | response, err := o.client.Do(req)
188 | if err != nil {
189 | return err
190 | }
191 | defer func(Body io.ReadCloser) {
192 | _ = Body.Close()
193 | }(response.Body)
194 | returnMap, err := o.parseResponse(response)
195 | if err != nil {
196 | return err
197 | }
198 | if returnMap["Status"] != "Success" {
199 | message := returnMap["Message"]
200 | errorUrl := returnMap["ErrorURL"]
201 | errorMsg := message.(string) +
202 | fmt.Sprintf(", targetTable: %s.%s", targetSchema, targetTable) +
203 | fmt.Sprintf(", visit ErrorURL to view error details, ErrorURL: %s", errorUrl)
204 | return errors.New(errorMsg)
205 | }
206 | // prom write event number counter
207 | numberLoadedRows := returnMap["NumberLoadedRows"]
208 | metrics.OpsWriteProcessed.Add(numberLoadedRows.(float64))
209 | return nil
210 | }
211 |
--------------------------------------------------------------------------------
/outputs/doris/doris_meta.go:
--------------------------------------------------------------------------------
1 | package doris
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | _ "github.com/go-sql-driver/mysql"
7 | "github.com/juju/errors"
8 | "github.com/mitchellh/mapstructure"
9 | "github.com/sqlpub/qin-cdc/config"
10 | "github.com/sqlpub/qin-cdc/metas"
11 | "sync"
12 | "time"
13 | )
14 |
15 | type MetaPlugin struct {
16 | *config.DorisConfig
17 | tables map[string]*metas.Table
18 | tablesVersion map[string]*metas.Table
19 | db *sql.DB
20 | mu sync.Mutex
21 | }
22 |
23 | func (m *MetaPlugin) Configure(conf map[string]interface{}) error {
24 | m.DorisConfig = &config.DorisConfig{}
25 | var target = conf["target"]
26 | if err := mapstructure.Decode(target, m.DorisConfig); err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
32 | func (m *MetaPlugin) LoadMeta(routers []*metas.Router) (err error) {
33 | m.tables = make(map[string]*metas.Table)
34 | m.tablesVersion = make(map[string]*metas.Table)
35 | dsn := fmt.Sprintf(
36 | "%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4&timeout=3s&interpolateParams=true",
37 | m.DorisConfig.UserName, m.DorisConfig.Password,
38 | m.DorisConfig.Host, m.DorisConfig.Port)
39 | m.db, err = sql.Open("mysql", dsn)
40 | if err != nil {
41 | return err
42 | }
43 | m.db.SetConnMaxLifetime(time.Minute * 3)
44 | m.db.SetMaxOpenConns(2)
45 | m.db.SetMaxIdleConns(2)
46 | err = m.db.Ping()
47 | if err != nil {
48 | return err
49 | }
50 | for _, router := range routers {
51 | rows, err := m.db.Query("select "+
52 | "column_name,column_default,is_nullable,data_type,column_type,column_key "+
53 | "from information_schema.columns "+
54 | "where table_schema = ? and table_name = ? "+
55 | "order by ordinal_position", router.TargetSchema, router.TargetTable)
56 | if err != nil {
57 | return err
58 | }
59 | table := &metas.Table{
60 | Schema: router.TargetSchema,
61 | Name: router.TargetTable,
62 | }
63 | for rows.Next() {
64 | var columnName, isNullable, dataType, columnType, columnKey string
65 | var columnDefault sql.NullString
66 | err = rows.Scan(&columnName, &columnDefault, &isNullable, &dataType, &columnType, &columnKey)
67 | if err != nil {
68 | return err
69 | }
70 | var column metas.Column
71 | column.Name = columnName
72 | column.RawType = columnType
73 | switch dataType {
74 | case "tinyint", "smallint", "mediumint", "int", "bigint":
75 | column.Type = metas.TypeNumber
76 | case "float", "double":
77 | column.Type = metas.TypeFloat
78 | case "enum":
79 | column.Type = metas.TypeEnum
80 | case "set":
81 | column.Type = metas.TypeSet
82 | case "datetime":
83 | column.Type = metas.TypeDatetime
84 | case "timestamp":
85 | column.Type = metas.TypeTimestamp
86 | case "date":
87 | column.Type = metas.TypeDate
88 | case "time":
89 | column.Type = metas.TypeTime
90 | case "bit":
91 | column.Type = metas.TypeBit
92 | case "json":
93 | column.Type = metas.TypeJson
94 | case "decimal":
95 | column.Type = metas.TypeDecimal
96 | default:
97 | column.Type = metas.TypeString
98 | }
99 | if columnKey == "PRI" {
100 | column.IsPrimaryKey = true
101 | }
102 | table.Columns = append(table.Columns, column)
103 | }
104 | if table.Columns == nil {
105 | return errors.Errorf("load meta %s.%s not found", router.TargetSchema, router.TargetTable)
106 | }
107 | err = m.Add(table)
108 | if err != nil {
109 | return err
110 | }
111 | }
112 | return nil
113 | }
114 |
115 | func (m *MetaPlugin) GetMeta(router *metas.Router) (table interface{}, err error) {
116 | return m.Get(router.SourceSchema, router.SourceTable)
117 | }
118 |
119 | func (m *MetaPlugin) Get(schema string, tableName string) (table *metas.Table, err error) {
120 | key := metas.GenerateMapRouterKey(schema, tableName)
121 | m.mu.Lock()
122 | defer m.mu.Unlock()
123 | return m.tables[key], err
124 | }
125 |
126 | func (m *MetaPlugin) GetAll() map[string]*metas.Table {
127 | m.mu.Lock()
128 | defer m.mu.Unlock()
129 | return m.tables
130 | }
131 |
132 | func (m *MetaPlugin) GetVersion(schema string, tableName string, version uint) (table *metas.Table, err error) {
133 | key := metas.GenerateMapRouterVersionKey(schema, tableName, version)
134 | m.mu.Lock()
135 | defer m.mu.Unlock()
136 | return m.tablesVersion[key], err
137 | }
138 |
139 | func (m *MetaPlugin) GetVersions(schema string, tableName string) []*metas.Table {
140 | m.mu.Lock()
141 | defer m.mu.Unlock()
142 | tables := make([]*metas.Table, 0)
143 | for k, table := range m.tablesVersion {
144 | s, t, _ := metas.SplitMapRouterVersionKey(k)
145 | if schema == s && tableName == t {
146 | tables = append(tables, table)
147 | }
148 | }
149 | return tables
150 | }
151 |
152 | func (m *MetaPlugin) Add(newTable *metas.Table) error {
153 | m.mu.Lock()
154 | defer m.mu.Unlock()
155 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
156 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
157 | return nil
158 | }
159 |
160 | func (m *MetaPlugin) Update(newTable *metas.Table) error {
161 | m.mu.Lock()
162 | defer m.mu.Unlock()
163 | newTable.Version += 1
164 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
165 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
166 | return nil
167 | }
168 |
169 | func (m *MetaPlugin) Delete(schema string, name string) error {
170 | m.mu.Lock()
171 | defer m.mu.Unlock()
172 | delete(m.tables, metas.GenerateMapRouterKey(schema, name))
173 | for _, table := range m.GetVersions(schema, name) {
174 | delete(m.tablesVersion, metas.GenerateMapRouterVersionKey(schema, name, table.Version))
175 | }
176 | return nil
177 | }
178 |
179 | func (m *MetaPlugin) Save() error {
180 | return nil
181 | }
182 |
183 | func (m *MetaPlugin) Close() {
184 | if m.db != nil {
185 | _ = m.db.Close()
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/outputs/doris/doris_utils.go:
--------------------------------------------------------------------------------
1 | package doris
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "github.com/goccy/go-json"
7 | "github.com/siddontang/go-log/log"
8 | "github.com/sqlpub/qin-cdc/core"
9 | "io"
10 | "net/http"
11 | )
12 |
13 | const (
14 | PluginName = "doris"
15 | DefaultBatchSize int = 10240
16 | DefaultBatchIntervalMs int = 3000
17 | DeleteColumn string = "_delete_sign_"
18 | RetryCount int = 3
19 | RetryInterval int = 5
20 | )
21 |
22 | var DeleteCondition = fmt.Sprintf("%s=1", DeleteColumn)
23 |
24 | func (o *OutputPlugin) auth() string {
25 | s := o.UserName + ":" + o.Password
26 | b := []byte(s)
27 |
28 | sEnc := base64.StdEncoding.EncodeToString(b)
29 | return sEnc
30 | }
31 |
32 | func (o *OutputPlugin) parseResponse(response *http.Response) (map[string]interface{}, error) {
33 | var result map[string]interface{}
34 | body, err := io.ReadAll(response.Body)
35 | if err == nil {
36 | err = json.Unmarshal(body, &result)
37 | }
38 |
39 | return result, err
40 | }
41 |
42 | func (o *OutputPlugin) generateJson(msgs []*core.Msg) []string {
43 | var jsonList []string
44 |
45 | for _, event := range msgs {
46 | switch event.DmlMsg.Action {
47 | case core.InsertAction:
48 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
49 | event.DmlMsg.Data[DeleteColumn] = 0
50 | b, _ := json.Marshal(event.DmlMsg.Data)
51 | jsonList = append(jsonList, string(b))
52 | case core.UpdateAction:
53 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
54 | event.DmlMsg.Data[DeleteColumn] = 0
55 | b, _ := json.Marshal(event.DmlMsg.Data)
56 | jsonList = append(jsonList, string(b))
57 | case core.DeleteAction:
58 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
59 | event.DmlMsg.Data[DeleteColumn] = 1
60 | b, _ := json.Marshal(event.DmlMsg.Data)
61 | jsonList = append(jsonList, string(b))
62 | case core.ReplaceAction: // for mongo
63 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
64 | event.DmlMsg.Data[DeleteColumn] = 0
65 | b, _ := json.Marshal(event.DmlMsg.Data)
66 | jsonList = append(jsonList, string(b))
67 | default:
68 | log.Fatalf("unhandled message type: %v", event)
69 | }
70 | }
71 | return jsonList
72 | }
73 |
--------------------------------------------------------------------------------
/outputs/init.go:
--------------------------------------------------------------------------------
1 | package outputs
2 |
3 | import (
4 | "github.com/sqlpub/qin-cdc/outputs/doris"
5 | "github.com/sqlpub/qin-cdc/outputs/kafka"
6 | "github.com/sqlpub/qin-cdc/outputs/mysql"
7 | "github.com/sqlpub/qin-cdc/outputs/starrocks"
8 | "github.com/sqlpub/qin-cdc/registry"
9 | )
10 |
11 | func init() {
12 | // registry output plugins
13 | registry.RegisterPlugin(registry.OutputPlugin, starrocks.PluginName, &starrocks.OutputPlugin{})
14 | registry.RegisterPlugin(registry.MetaPlugin, string(registry.OutputPlugin+starrocks.PluginName), &starrocks.MetaPlugin{})
15 |
16 | registry.RegisterPlugin(registry.OutputPlugin, doris.PluginName, &doris.OutputPlugin{})
17 | registry.RegisterPlugin(registry.MetaPlugin, string(registry.OutputPlugin+doris.PluginName), &doris.MetaPlugin{})
18 |
19 | registry.RegisterPlugin(registry.OutputPlugin, mysql.PluginName, &mysql.OutputPlugin{})
20 | registry.RegisterPlugin(registry.MetaPlugin, string(registry.OutputPlugin+mysql.PluginName), &mysql.MetaPlugin{})
21 |
22 | registry.RegisterPlugin(registry.OutputPlugin, kafka.PluginName, &kafka.OutputPlugin{})
23 | registry.RegisterPlugin(registry.MetaPlugin, string(registry.OutputPlugin+kafka.PluginName), &kafka.MetaPlugin{})
24 | }
25 |
--------------------------------------------------------------------------------
/outputs/kafka/kafka.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | import (
4 | "fmt"
5 | gokafka "github.com/confluentinc/confluent-kafka-go/v2/kafka"
6 | "github.com/goccy/go-json"
7 | "github.com/mitchellh/mapstructure"
8 | "github.com/siddontang/go-log/log"
9 | "github.com/sqlpub/qin-cdc/config"
10 | "github.com/sqlpub/qin-cdc/core"
11 | "github.com/sqlpub/qin-cdc/metas"
12 | "github.com/sqlpub/qin-cdc/metrics"
13 | "strconv"
14 | "time"
15 | )
16 |
17 | type OutputPlugin struct {
18 | *config.KafkaConfig
19 | Done chan bool
20 | metas *core.Metas
21 | formatInterface formatInterface
22 | msgTxnBuffer struct {
23 | size int
24 | tableMsgMap map[string][]*core.Msg
25 | }
26 | client *gokafka.Producer
27 | lastPosition string
28 | }
29 |
30 | func (o *OutputPlugin) Configure(conf map[string]interface{}) error {
31 | o.KafkaConfig = &config.KafkaConfig{}
32 | var targetConf = conf["target"]
33 | if err := mapstructure.Decode(targetConf, o.KafkaConfig); err != nil {
34 | return err
35 | }
36 | o.initFormatPlugin(fmt.Sprintf("%v", o.Options.OutputFormat))
37 | return nil
38 | }
39 |
40 | func (o *OutputPlugin) NewOutput(metas *core.Metas) {
41 | o.Done = make(chan bool)
42 | o.metas = metas
43 | // options handle
44 | if o.KafkaConfig.Options.BatchSize == 0 {
45 | o.KafkaConfig.Options.BatchSize = DefaultBatchSize
46 | }
47 | if o.KafkaConfig.Options.BatchIntervalMs == 0 {
48 | o.KafkaConfig.Options.BatchIntervalMs = DefaultBatchIntervalMs
49 | }
50 |
51 | o.msgTxnBuffer.size = 0
52 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
53 |
54 | var err error
55 | o.client, err = getProducer(o.KafkaConfig)
56 | if err != nil {
57 | log.Fatal("output config client failed. err: ", err.Error())
58 | }
59 | }
60 |
61 | func (o *OutputPlugin) Start(out chan *core.Msg, pos core.Position) {
62 | // first pos
63 | o.lastPosition = pos.Get()
64 | go func() {
65 | ticker := time.NewTicker(time.Millisecond * time.Duration(o.Options.BatchIntervalMs))
66 | defer ticker.Stop()
67 | for {
68 | select {
69 | case data := <-out:
70 | switch data.Type {
71 | case core.MsgCtl:
72 | o.lastPosition = data.InputContext.Pos
73 | case core.MsgDML:
74 | o.appendMsgTxnBuffer(data)
75 | if o.msgTxnBuffer.size >= o.KafkaConfig.Options.BatchSize {
76 | o.flushMsgTxnBuffer(pos)
77 | }
78 | }
79 | case e := <-o.client.Events():
80 | switch ev := e.(type) {
81 | case *gokafka.Message:
82 | m := ev
83 | if m.TopicPartition.Error != nil {
84 | log.Fatalf("Delivery failed: %v", m.TopicPartition.Error)
85 | }
86 | _, ok := m.Opaque.(*core.Msg)
87 | if !ok {
88 | log.Fatalf("kafka send failed to get meta data")
89 | }
90 | case gokafka.Error:
91 | log.Fatalf("kafka producer failed, err: %v", ev)
92 | default:
93 | log.Infof("Ignored event: %s", ev)
94 | }
95 | case <-ticker.C:
96 | o.flushMsgTxnBuffer(pos)
97 | case <-o.Done:
98 | o.flushMsgTxnBuffer(pos)
99 | return
100 | }
101 |
102 | }
103 | }()
104 | }
105 |
106 | func (o *OutputPlugin) Close() {
107 | log.Infof("output is closing...")
108 | close(o.Done)
109 | <-o.Done
110 | closeProducer(o.client)
111 | log.Infof("output is closed")
112 | }
113 |
114 | func (o *OutputPlugin) appendMsgTxnBuffer(msg *core.Msg) {
115 | key := metas.GenerateMapRouterVersionKey(msg.Database, msg.Table, msg.DmlMsg.TableVersion)
116 | o.msgTxnBuffer.tableMsgMap[key] = append(o.msgTxnBuffer.tableMsgMap[key], msg)
117 | o.msgTxnBuffer.size += 1
118 | }
119 |
120 | func (o *OutputPlugin) flushMsgTxnBuffer(pos core.Position) {
121 | defer func() {
122 | // flush position
123 | err := pos.Update(o.lastPosition)
124 | if err != nil {
125 | log.Fatalf(err.Error())
126 | }
127 | }()
128 |
129 | if o.msgTxnBuffer.size == 0 {
130 | return
131 | }
132 | // table level send
133 | for k, msgs := range o.msgTxnBuffer.tableMsgMap {
134 | // columnsMapper := o.metas.Routers.Maps[k].ColumnsMapper
135 | schemaName, tableName, version := metas.SplitMapRouterVersionKey(k)
136 | dmlTopic := o.metas.Routers.Maps[metas.GenerateMapRouterKey(schemaName, tableName)].DmlTopic
137 | table, err := o.metas.Input.GetVersion(schemaName, tableName, version)
138 | if err != nil {
139 | log.Fatalf("get input table meta failed, err: %v", err.Error())
140 | }
141 | err = o.execute(msgs, table, dmlTopic)
142 | if err != nil {
143 | log.Fatalf("output %s send err %v", PluginName, err)
144 | }
145 | }
146 | o.clearMsgTxnBuffer()
147 | }
148 |
149 | func (o *OutputPlugin) clearMsgTxnBuffer() {
150 | o.msgTxnBuffer.size = 0
151 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
152 | }
153 |
154 | func (o *OutputPlugin) execute(msgs []*core.Msg, table *metas.Table, dmlTopic string) error {
155 | for _, msg := range msgs {
156 | formatMsg := o.formatInterface.formatMsg(msg, table)
157 | bFormatMsg, err := json.Marshal(formatMsg)
158 | if err != nil {
159 | return err
160 | }
161 | pksData, err := GenPrimaryKeys(table.PrimaryKeyColumns, msg.DmlMsg.Data)
162 | if err != nil {
163 | return err
164 | }
165 | _, dataHash, err := DataHash(pksData)
166 | if err != nil {
167 | return err
168 | }
169 | kPartition := dataHash % uint64(o.PartitionNum)
170 | kKey := strconv.FormatUint(dataHash, 10)
171 |
172 | kMsg := gokafka.Message{
173 | TopicPartition: gokafka.TopicPartition{Topic: &dmlTopic, Partition: int32(kPartition)},
174 | Key: []byte(kKey),
175 | Value: bFormatMsg,
176 | Opaque: msg,
177 | }
178 |
179 | err = o.send(&kMsg)
180 | if err != nil {
181 | return err
182 | }
183 | log.Debugf("output %s msg: %v", PluginName, string(bFormatMsg))
184 | // prom write event number counter
185 | metrics.OpsWriteProcessed.Add(1)
186 | }
187 | return nil
188 | }
189 |
190 | func (o *OutputPlugin) send(message *gokafka.Message) error {
191 | var err error
192 | for i := 0; i < RetryCount; i++ {
193 | err = o.client.Produce(message, nil)
194 | if err != nil {
195 | log.Warnf("kafka send data failed, err: %v, start retry...", err.Error())
196 | if i+1 == RetryCount {
197 | break
198 | }
199 | time.Sleep(time.Duration(RetryInterval*(i+1)) * time.Second)
200 | continue
201 | }
202 | break
203 | }
204 | if err != nil {
205 | return err
206 | }
207 | return err
208 | }
209 |
--------------------------------------------------------------------------------
/outputs/kafka/kafka_meta.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | import (
4 | gokafka "github.com/confluentinc/confluent-kafka-go/v2/kafka"
5 | "github.com/mitchellh/mapstructure"
6 | "github.com/sqlpub/qin-cdc/config"
7 | "github.com/sqlpub/qin-cdc/metas"
8 | "sync"
9 | )
10 |
11 | type MetaPlugin struct {
12 | *config.KafkaConfig
13 | topics map[string]*Topic
14 | producer *gokafka.Producer
15 | mu sync.Mutex
16 | }
17 |
18 | type Topic struct {
19 | Name string
20 | Partition int
21 | }
22 |
23 | func (m *MetaPlugin) Configure(conf map[string]interface{}) error {
24 | m.KafkaConfig = &config.KafkaConfig{}
25 | var target = conf["target"]
26 | if err := mapstructure.Decode(target, m.KafkaConfig); err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
32 | func (m *MetaPlugin) LoadMeta(routers []*metas.Router) (err error) {
33 | m.topics = make(map[string]*Topic)
34 | m.producer, err = getProducer(m.KafkaConfig)
35 | if err != nil {
36 | return err
37 | }
38 | for _, router := range routers {
39 | dmlTopic := router.DmlTopic
40 | if ok := m.topics[dmlTopic]; ok != nil {
41 | m.topics[dmlTopic].Name = dmlTopic
42 | metadata, err := m.producer.GetMetadata(&dmlTopic, false, 3000)
43 | if err != nil {
44 | return err
45 | }
46 | m.topics[dmlTopic].Partition = len(metadata.Topics[dmlTopic].Partitions)
47 | }
48 | }
49 | return nil
50 | }
51 |
52 | func (m *MetaPlugin) GetMeta(router *metas.Router) (topic interface{}, err error) {
53 | return m.Get(router.DmlTopic)
54 | }
55 |
56 | func (m *MetaPlugin) Get(topicName string) (topic *Topic, err error) {
57 | return m.topics[topicName], err
58 | }
59 |
60 | func (m *MetaPlugin) GetAll() map[string]*Topic {
61 | m.mu.Lock()
62 | defer m.mu.Unlock()
63 | return m.topics
64 | }
65 |
66 | func (m *MetaPlugin) Add(newTopic *Topic) error {
67 | m.mu.Lock()
68 | defer m.mu.Unlock()
69 | m.topics[newTopic.Name] = newTopic
70 | return nil
71 | }
72 |
73 | func (m *MetaPlugin) Update(newTopic *Topic) error {
74 | m.mu.Lock()
75 | defer m.mu.Unlock()
76 | m.topics[newTopic.Name] = newTopic
77 | return nil
78 | }
79 |
80 | func (m *MetaPlugin) Delete(topicName string) error {
81 | m.mu.Lock()
82 | defer m.mu.Unlock()
83 | delete(m.topics, topicName)
84 | return nil
85 | }
86 |
87 | func (m *MetaPlugin) Save() error {
88 | return nil
89 | }
90 |
91 | func (m *MetaPlugin) Close() {
92 | closeProducer(m.producer)
93 | }
94 |
--------------------------------------------------------------------------------
/outputs/kafka/kafka_utils.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | import (
4 | "fmt"
5 | gokafka "github.com/confluentinc/confluent-kafka-go/v2/kafka"
6 | "github.com/juju/errors"
7 | "github.com/mitchellh/hashstructure/v2"
8 | "github.com/siddontang/go-log/log"
9 | "github.com/sqlpub/qin-cdc/config"
10 | "github.com/sqlpub/qin-cdc/core"
11 | "github.com/sqlpub/qin-cdc/metas"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type formatType string
17 |
18 | var inputSequence uint64
19 |
20 | const (
21 | PluginName = "kafka"
22 | DefaultBatchSize int = 10240
23 | DefaultBatchIntervalMs int = 100
24 | RetryCount int = 3
25 | RetryInterval int = 5
26 | defaultJson formatType = "json"
27 | aliyunDtsCanal formatType = "aliyun_dts_canal"
28 | )
29 |
30 | func getProducer(conf *config.KafkaConfig) (producer *gokafka.Producer, err error) {
31 | kafkaConf := &gokafka.ConfigMap{
32 | "api.version.request": "true",
33 | "message.max.bytes": 1000000,
34 | "linger.ms": 10,
35 | "retries": 30,
36 | "retry.backoff.ms": 1000,
37 | "acks": "1"}
38 | err = kafkaConf.SetKey("bootstrap.servers", strings.Join(conf.Brokers, ","))
39 | if err != nil {
40 | return nil, err
41 | }
42 | err = kafkaConf.SetKey("security.protocol", "plaintext")
43 | if err != nil {
44 | return nil, err
45 | }
46 | producer, err = gokafka.NewProducer(kafkaConf)
47 | return producer, err
48 | }
49 |
50 | func closeProducer(producer *gokafka.Producer) {
51 | if producer != nil {
52 | producer.Close()
53 | }
54 | }
55 |
56 | type formatInterface interface {
57 | formatMsg(event *core.Msg, table *metas.Table) interface{}
58 | }
59 |
60 | func (o *OutputPlugin) initFormatPlugin(outputFormat string) {
61 | // init kafka format handle func
62 | outputFormatType := formatType(fmt.Sprintf("%v", outputFormat))
63 | switch outputFormatType {
64 | case defaultJson:
65 | o.formatInterface = &defaultJsonFormat{}
66 | case aliyunDtsCanal:
67 | o.formatInterface = &aliyunDtsCanalFormat{}
68 | default:
69 | log.Fatalf("Unknown format type: %v", outputFormatType)
70 | }
71 | }
72 |
73 | type defaultJsonFormat struct{}
74 |
75 | type kafkaDefaultMsg struct {
76 | Database string `json:"database"`
77 | Table string `json:"table"`
78 | Type core.ActionType `json:"type"`
79 | Ts uint32 `json:"ts"`
80 | Data map[string]interface{} `json:"data"`
81 | Old map[string]interface{} `json:"old"`
82 | }
83 |
84 | func (djf *defaultJsonFormat) formatMsg(event *core.Msg, table *metas.Table) interface{} {
85 | kMsg := &kafkaDefaultMsg{
86 | Database: event.Database,
87 | Table: event.Table,
88 | Type: event.DmlMsg.Action,
89 | Ts: uint32(event.Timestamp.Unix()),
90 | Data: event.DmlMsg.Data,
91 | Old: event.DmlMsg.Old,
92 | }
93 | return kMsg
94 | }
95 |
96 | type aliyunDtsCanalFormat struct{}
97 |
98 | type kafkaMsgForAliyunDtsCanal struct {
99 | Database string `json:"database"`
100 | Table string `json:"table"`
101 | Type string `json:"type"`
102 | Es uint64 `json:"es"` // source write datetime
103 | Ts uint64 `json:"ts"` // target write datetime
104 | Data []map[string]interface{} `json:"data"`
105 | Old []map[string]interface{} `json:"old"`
106 | SqlType map[string]interface{} `json:"sqlType"`
107 | MysqlType map[string]interface{} `json:"mysqlType"`
108 | ServerId string `json:"serverId"`
109 | Sql string `json:"sql"`
110 | PkNames []string `json:"pkNames"`
111 | IsDdl bool `json:"isDdl"`
112 | Id uint64 `json:"id"`
113 | Gtid *string `json:"gtid"`
114 | }
115 |
116 | func (adc *aliyunDtsCanalFormat) formatMsg(event *core.Msg, table *metas.Table) interface{} {
117 | datas := make([]map[string]interface{}, 0)
118 | datas = append(datas, event.DmlMsg.Data)
119 | var olds []map[string]interface{}
120 | if event.DmlMsg.Old != nil {
121 | olds = make([]map[string]interface{}, 0)
122 | olds = append(olds, event.DmlMsg.Old)
123 | }
124 | sqlType := make(map[string]interface{})
125 | mysqlType := make(map[string]interface{})
126 | for _, column := range table.Columns {
127 | if event.DmlMsg.Data[column.Name] != nil {
128 | event.DmlMsg.Data[column.Name] = fmt.Sprintf("%v", event.DmlMsg.Data[column.Name]) // to string
129 | }
130 | if event.DmlMsg.Old[column.Name] != nil {
131 | event.DmlMsg.Old[column.Name] = fmt.Sprintf("%v", event.DmlMsg.Old[column.Name]) // to string
132 | }
133 | switch column.Type {
134 | case metas.TypeNumber: // tinyint, smallint, int, bigint, year
135 | if strings.HasPrefix(column.RawType, "smallint") {
136 | sqlType[column.Name] = 2
137 | mysqlType[column.Name] = "smallint"
138 | continue
139 | } else if strings.HasPrefix(column.RawType, "tinyint") {
140 | sqlType[column.Name] = 1
141 | mysqlType[column.Name] = "tinyint"
142 | continue
143 | } else if strings.HasPrefix(column.RawType, "mediumint") {
144 | sqlType[column.Name] = 9
145 | mysqlType[column.Name] = "mediumint"
146 | continue
147 | } else if strings.HasPrefix(column.RawType, "bigint") {
148 | sqlType[column.Name] = 8
149 | mysqlType[column.Name] = "bigint"
150 | continue
151 | } else if strings.HasPrefix(column.RawType, "year") {
152 | mysqlType[column.Name] = "year"
153 | continue
154 | } else {
155 | sqlType[column.Name] = 3
156 | mysqlType[column.Name] = "int"
157 | continue
158 | }
159 | case metas.TypeFloat: // float, double
160 | if strings.HasPrefix(column.RawType, "float") {
161 | sqlType[column.Name] = 4
162 | mysqlType[column.Name] = "float"
163 | continue
164 | } else if strings.HasPrefix(column.RawType, "double") {
165 | sqlType[column.Name] = 5
166 | mysqlType[column.Name] = "double"
167 | continue
168 | }
169 | case metas.TypeEnum: // enum
170 | sqlType[column.Name] = 247
171 | mysqlType[column.Name] = "enum"
172 | continue
173 | case metas.TypeSet: // set
174 | sqlType[column.Name] = 248
175 | mysqlType[column.Name] = "set"
176 | continue
177 | case metas.TypeString: // other
178 | if strings.HasSuffix(column.RawType, "text") {
179 | sqlType[column.Name] = 15
180 | mysqlType[column.Name] = "text"
181 | continue
182 | } else if strings.HasPrefix(column.RawType, "char") {
183 | sqlType[column.Name] = 254
184 | mysqlType[column.Name] = "char"
185 | continue
186 | } else {
187 | sqlType[column.Name] = 253
188 | mysqlType[column.Name] = "varchar"
189 | continue
190 | }
191 | case metas.TypeDatetime: // datetime
192 | sqlType[column.Name] = 12
193 | mysqlType[column.Name] = "datetime"
194 | continue
195 | case metas.TypeTimestamp: // timestamp
196 | sqlType[column.Name] = 7
197 | mysqlType[column.Name] = "timestamp"
198 | continue
199 | case metas.TypeDate: // date
200 | sqlType[column.Name] = 10
201 | mysqlType[column.Name] = "date"
202 | continue
203 | case metas.TypeTime: // time
204 | sqlType[column.Name] = 11
205 | mysqlType[column.Name] = "time"
206 | continue
207 | case metas.TypeBit: // bit
208 | sqlType[column.Name] = 16
209 | mysqlType[column.Name] = "bit"
210 | continue
211 | case metas.TypeJson: // json
212 | sqlType[column.Name] = 245
213 | mysqlType[column.Name] = "json"
214 | continue
215 | case metas.TypeDecimal: // decimal
216 | sqlType[column.Name] = 246
217 | mysqlType[column.Name] = "decimal"
218 | continue
219 | case metas.TypeBinary:
220 | sqlType[column.Name] = 252
221 | if strings.HasPrefix(column.RawType, "binary") {
222 | mysqlType[column.Name] = "binary"
223 | } else {
224 | mysqlType[column.Name] = "blob"
225 | }
226 | continue
227 | default:
228 | sqlType[column.Name] = column.Type
229 | mysqlType[column.Name] = column.RawType
230 | }
231 | }
232 |
233 | pkNames := make([]string, 0)
234 | for _, primaryKeyColumn := range table.PrimaryKeyColumns {
235 | pkNames = append(pkNames, primaryKeyColumn.Name)
236 | }
237 | inputSequence++
238 | kMsg := &kafkaMsgForAliyunDtsCanal{
239 | Database: event.Database,
240 | Table: event.Table,
241 | Type: strings.ToUpper(string(event.DmlMsg.Action)),
242 | Es: uint64(event.Timestamp.UnixMilli()),
243 | Ts: uint64(time.Now().UnixMilli()),
244 | Data: datas,
245 | Old: olds,
246 | SqlType: sqlType,
247 | MysqlType: mysqlType,
248 | ServerId: "",
249 | Sql: "",
250 | PkNames: pkNames,
251 | IsDdl: false,
252 | Id: inputSequence,
253 | Gtid: nil,
254 | }
255 | return kMsg
256 | }
257 |
258 | func DataHash(key interface{}) (string, uint64, error) {
259 | hash, err := hashstructure.Hash(key, hashstructure.FormatV2, nil)
260 | if err != nil {
261 | return "", 0, err
262 | }
263 | return fmt.Sprint(key), hash, nil
264 | }
265 |
266 | func GenPrimaryKeys(pkColumns []metas.Column, rowData map[string]interface{}) (map[string]interface{}, error) {
267 | pks := make(map[string]interface{})
268 | for i := 0; i < len(pkColumns); i++ {
269 | pkName := pkColumns[i].Name
270 | pks[pkName] = rowData[pkName]
271 | if pks[pkName] == nil {
272 | return nil, errors.Errorf("primary key nil, pkName: %v, data: %v", pkName, rowData)
273 | }
274 | }
275 | return pks, nil
276 | }
277 |
--------------------------------------------------------------------------------
/outputs/mysql/mysql.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "database/sql"
5 | "github.com/juju/errors"
6 | "github.com/mitchellh/mapstructure"
7 | "github.com/siddontang/go-log/log"
8 | "github.com/sqlpub/qin-cdc/config"
9 | "github.com/sqlpub/qin-cdc/core"
10 | "github.com/sqlpub/qin-cdc/metas"
11 | "github.com/sqlpub/qin-cdc/metrics"
12 | "time"
13 | )
14 |
15 | type OutputPlugin struct {
16 | *config.MysqlConfig
17 | Done chan bool
18 | metas *core.Metas
19 | msgTxnBuffer struct {
20 | size int
21 | tableMsgMap map[string][]*core.Msg
22 | }
23 | client *sql.DB
24 | lastPosition string
25 | }
26 |
27 | func (o *OutputPlugin) Configure(conf map[string]interface{}) error {
28 | o.MysqlConfig = &config.MysqlConfig{}
29 | var targetConf = conf["target"]
30 | if err := mapstructure.Decode(targetConf, o.MysqlConfig); err != nil {
31 | return err
32 | }
33 | return nil
34 | }
35 |
36 | func (o *OutputPlugin) NewOutput(metas *core.Metas) {
37 | o.Done = make(chan bool)
38 | o.metas = metas
39 | // options handle
40 | if o.MysqlConfig.Options.BatchSize == 0 {
41 | o.MysqlConfig.Options.BatchSize = DefaultBatchSize
42 | }
43 | if o.MysqlConfig.Options.BatchIntervalMs == 0 {
44 | o.MysqlConfig.Options.BatchIntervalMs = DefaultBatchIntervalMs
45 | }
46 | o.msgTxnBuffer.size = 0
47 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
48 |
49 | var err error
50 | o.client, err = getConn(o.MysqlConfig)
51 | if err != nil {
52 | log.Fatal("output config client failed. err: ", err.Error())
53 | }
54 | }
55 |
56 | func (o *OutputPlugin) Start(out chan *core.Msg, pos core.Position) {
57 | // first pos
58 | o.lastPosition = pos.Get()
59 | go func() {
60 | ticker := time.NewTicker(time.Millisecond * time.Duration(o.Options.BatchIntervalMs))
61 | defer ticker.Stop()
62 | for {
63 | select {
64 | case data := <-out:
65 | switch data.Type {
66 | case core.MsgCtl:
67 | o.lastPosition = data.InputContext.Pos
68 | case core.MsgDML:
69 | o.appendMsgTxnBuffer(data)
70 | if o.msgTxnBuffer.size >= o.MysqlConfig.Options.BatchSize {
71 | o.flushMsgTxnBuffer(pos)
72 | }
73 | }
74 | case <-ticker.C:
75 | o.flushMsgTxnBuffer(pos)
76 | case <-o.Done:
77 | o.flushMsgTxnBuffer(pos)
78 | return
79 | }
80 |
81 | }
82 | }()
83 | }
84 |
85 | func (o *OutputPlugin) Close() {
86 | log.Infof("output is closing...")
87 | close(o.Done)
88 | <-o.Done
89 | closeConn(o.client)
90 | log.Infof("output is closed")
91 | }
92 |
93 | func (o *OutputPlugin) appendMsgTxnBuffer(msg *core.Msg) {
94 | key := metas.GenerateMapRouterKey(msg.Database, msg.Table)
95 | o.msgTxnBuffer.tableMsgMap[key] = append(o.msgTxnBuffer.tableMsgMap[key], msg)
96 | o.msgTxnBuffer.size += 1
97 | }
98 |
99 | func (o *OutputPlugin) flushMsgTxnBuffer(pos core.Position) {
100 | defer func() {
101 | // flush position
102 | err := pos.Update(o.lastPosition)
103 | if err != nil {
104 | log.Fatalf(err.Error())
105 | }
106 | }()
107 |
108 | if o.msgTxnBuffer.size == 0 {
109 | return
110 | }
111 | // table level export
112 | for k, msgs := range o.msgTxnBuffer.tableMsgMap {
113 | columnsMapper := o.metas.Routers.Maps[k].ColumnsMapper
114 | targetSchema := o.metas.Routers.Maps[k].TargetSchema
115 | targetTable := o.metas.Routers.Maps[k].TargetTable
116 | err := o.execute(msgs, columnsMapper, targetSchema, targetTable)
117 | if err != nil {
118 | log.Fatalf("do %s bulk err %v", PluginName, err)
119 | }
120 | }
121 | o.clearMsgTxnBuffer()
122 | }
123 |
124 | func (o *OutputPlugin) clearMsgTxnBuffer() {
125 | o.msgTxnBuffer.size = 0
126 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
127 | }
128 |
129 | func (o *OutputPlugin) execute(msgs []*core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) error {
130 | if len(columnsMapper.PrimaryKeys) == 0 {
131 | return errors.Errorf("only support data has primary key")
132 | }
133 |
134 | splitMsgsList := o.splitMsgs(msgs)
135 | for _, splitMsgs := range splitMsgsList {
136 | if splitMsgs[0].DmlMsg.Action != core.DeleteAction {
137 | // insert and update can bulk exec
138 | bulkSQL, args, err := o.generateBulkInsertOnDuplicateKeyUpdateSQL(splitMsgs, columnsMapper, targetSchema, targetTable)
139 | err = o.executeSQL(bulkSQL, args)
140 | if err != nil {
141 | return err
142 | }
143 | log.Debugf("output %s sql: %v; args: %v", PluginName, bulkSQL, args)
144 | } else {
145 | if len(columnsMapper.PrimaryKeys) > 1 { // multi-pk single sql exec (delete from table where pk1 = ? and pk2 = ? ...)
146 | for _, msg := range splitMsgs {
147 | singleSQL, args, err := o.generateSingleDeleteSQL(msg, columnsMapper, targetSchema, targetTable)
148 | err = o.executeSQL(singleSQL, args)
149 | if err != nil {
150 | return err
151 | }
152 | log.Debugf("output %s sql: %v; args: %v", PluginName, singleSQL, args)
153 | }
154 |
155 | } else { // one-pk bulk sql exec (delete from table where pk in (?,? ..))
156 | bulkSQL, args, err := o.generateBulkDeleteSQL(splitMsgs, columnsMapper, targetSchema, targetTable)
157 | err = o.executeSQL(bulkSQL, args)
158 | if err != nil {
159 | return err
160 | }
161 | log.Debugf("output %s sql: %v; args: %v", PluginName, bulkSQL, args)
162 | }
163 | }
164 | // log.Debugf("%s bulk sync %s.%s row data num: %d", PluginName, targetSchema, targetTable, len(msgs))
165 |
166 | // prom write event number counter
167 | metrics.OpsWriteProcessed.Add(float64(len(splitMsgs)))
168 | }
169 | return nil
170 | }
171 |
172 | func (o *OutputPlugin) splitMsgs(msgs []*core.Msg) [][]*core.Msg {
173 | msgsList := make([][]*core.Msg, 0)
174 | tmpMsgs := make([]*core.Msg, 0)
175 | tmpDeleteMsgs := make([]*core.Msg, 0)
176 | var nextMsgAction core.ActionType
177 | lenMsgs := len(msgs)
178 | // split delete msg
179 | for i, msg := range msgs {
180 | if i < lenMsgs-1 {
181 | nextMsgAction = msgs[i+1].DmlMsg.Action
182 | } else {
183 | // last msg
184 | nextMsgAction = ""
185 | }
186 |
187 | if msg.DmlMsg.Action == core.DeleteAction {
188 | tmpDeleteMsgs = append(tmpDeleteMsgs, msg)
189 | if nextMsgAction != core.DeleteAction {
190 | msgsList = append(msgsList, tmpDeleteMsgs)
191 | tmpDeleteMsgs = make([]*core.Msg, 0)
192 | }
193 | } else {
194 | tmpMsgs = append(tmpMsgs, msg)
195 | if nextMsgAction != core.InsertAction && nextMsgAction != core.UpdateAction {
196 | msgsList = append(msgsList, tmpMsgs)
197 | tmpMsgs = make([]*core.Msg, 0)
198 | }
199 | }
200 | }
201 | return msgsList
202 | }
203 |
204 | func (o *OutputPlugin) executeSQL(sqlCmd string, args []interface{}) error {
205 | var err error
206 | var result sql.Result
207 | for i := 0; i < RetryCount; i++ {
208 | result, err = o.client.Exec(sqlCmd, args...)
209 | if err != nil {
210 | log.Warnf("exec data failed, err: %v, execute retry...", err.Error())
211 | if i+1 == RetryCount {
212 | break
213 | }
214 | time.Sleep(time.Duration(RetryInterval*(i+1)) * time.Second)
215 | continue
216 | }
217 | break
218 | }
219 | if err != nil {
220 | return err
221 | }
222 | if result == nil {
223 | return errors.Errorf("execute bulksql retry failed, result is nil")
224 | }
225 | return err
226 | }
227 |
--------------------------------------------------------------------------------
/outputs/mysql/mysql_meta.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/mitchellh/mapstructure"
7 | "github.com/sqlpub/qin-cdc/config"
8 | "github.com/sqlpub/qin-cdc/metas"
9 | "strings"
10 | "sync"
11 | )
12 |
13 | type MetaPlugin struct {
14 | *config.MysqlConfig
15 | tables map[string]*metas.Table
16 | tablesVersion map[string]*metas.Table
17 | db *sql.DB
18 | mu sync.Mutex
19 | }
20 |
21 | func (m *MetaPlugin) Configure(conf map[string]interface{}) error {
22 | m.MysqlConfig = &config.MysqlConfig{}
23 | var target = conf["target"]
24 | if err := mapstructure.Decode(target, m.MysqlConfig); err != nil {
25 | return err
26 | }
27 | return nil
28 | }
29 |
30 | func (m *MetaPlugin) LoadMeta(routers []*metas.Router) (err error) {
31 | m.tables = make(map[string]*metas.Table)
32 | m.tablesVersion = make(map[string]*metas.Table)
33 | m.db, err = getConn(m.MysqlConfig)
34 | if err != nil {
35 | return err
36 | }
37 | for _, router := range routers {
38 | row := m.db.QueryRow(fmt.Sprintf("show create table `%s`.`%s`", router.TargetSchema, router.TargetTable))
39 | if row.Err() != nil {
40 | return err
41 | }
42 | var tableName string
43 | var createTableDdlStr string
44 | err = row.Scan(&tableName, &createTableDdlStr)
45 | if err != nil {
46 | return err
47 | }
48 | createTableDdlStr = strings.Replace(createTableDdlStr, "CREATE TABLE ", fmt.Sprintf("CREATE TABLE `%s`.", router.SourceSchema), 1)
49 | table := &metas.Table{}
50 | table, err = metas.NewTable(createTableDdlStr)
51 | if err != nil {
52 | return err
53 | }
54 | err = m.Add(table)
55 | if err != nil {
56 | return err
57 | }
58 | }
59 | return nil
60 | }
61 |
62 | func (m *MetaPlugin) GetMeta(router *metas.Router) (table interface{}, err error) {
63 | return m.Get(router.SourceSchema, router.SourceTable)
64 | }
65 |
66 | func (m *MetaPlugin) Get(schema string, tableName string) (table *metas.Table, err error) {
67 | key := metas.GenerateMapRouterKey(schema, tableName)
68 | m.mu.Lock()
69 | defer m.mu.Unlock()
70 | return m.tables[key], err
71 | }
72 |
73 | func (m *MetaPlugin) GetAll() map[string]*metas.Table {
74 | m.mu.Lock()
75 | defer m.mu.Unlock()
76 | return m.tables
77 | }
78 |
79 | func (m *MetaPlugin) GetVersion(schema string, tableName string, version uint) (table *metas.Table, err error) {
80 | key := metas.GenerateMapRouterVersionKey(schema, tableName, version)
81 | m.mu.Lock()
82 | defer m.mu.Unlock()
83 | return m.tablesVersion[key], err
84 | }
85 |
86 | func (m *MetaPlugin) GetVersions(schema string, tableName string) []*metas.Table {
87 | m.mu.Lock()
88 | defer m.mu.Unlock()
89 | tables := make([]*metas.Table, 0)
90 | for k, table := range m.tablesVersion {
91 | s, t, _ := metas.SplitMapRouterVersionKey(k)
92 | if schema == s && tableName == t {
93 | tables = append(tables, table)
94 | }
95 | }
96 | return tables
97 | }
98 |
99 | func (m *MetaPlugin) Add(newTable *metas.Table) error {
100 | m.mu.Lock()
101 | defer m.mu.Unlock()
102 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
103 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
104 | return nil
105 | }
106 |
107 | func (m *MetaPlugin) Update(newTable *metas.Table) error {
108 | m.mu.Lock()
109 | defer m.mu.Unlock()
110 | newTable.Version += 1
111 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
112 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
113 | return nil
114 | }
115 |
116 | func (m *MetaPlugin) Delete(schema string, name string) error {
117 | m.mu.Lock()
118 | defer m.mu.Unlock()
119 | delete(m.tables, metas.GenerateMapRouterKey(schema, name))
120 | for _, table := range m.GetVersions(schema, name) {
121 | delete(m.tablesVersion, metas.GenerateMapRouterVersionKey(schema, name, table.Version))
122 | }
123 | return nil
124 | }
125 |
126 | func (m *MetaPlugin) Save() error {
127 | return nil
128 | }
129 |
130 | func (m *MetaPlugin) Close() {
131 | closeConn(m.db)
132 | }
133 |
--------------------------------------------------------------------------------
/outputs/mysql/mysql_utils.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | _ "github.com/go-sql-driver/mysql"
7 | "github.com/juju/errors"
8 | "github.com/siddontang/go-log/log"
9 | "github.com/sqlpub/qin-cdc/config"
10 | "github.com/sqlpub/qin-cdc/core"
11 | "github.com/sqlpub/qin-cdc/metas"
12 | "strings"
13 | "time"
14 | )
15 |
16 | const (
17 | PluginName = "mysql"
18 | DefaultBatchSize int = 10240
19 | DefaultBatchIntervalMs int = 100
20 | RetryCount int = 3
21 | RetryInterval int = 5
22 | )
23 |
24 | func getConn(conf *config.MysqlConfig) (db *sql.DB, err error) {
25 | dsn := fmt.Sprintf(
26 | "%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4&timeout=3s",
27 | conf.UserName, conf.Password,
28 | conf.Host, conf.Port)
29 | db, err = sql.Open("mysql", dsn)
30 | if err != nil {
31 | return db, err
32 | }
33 | db.SetConnMaxLifetime(time.Minute * 3)
34 | db.SetMaxOpenConns(2)
35 | db.SetMaxIdleConns(2)
36 | return db, err
37 | }
38 |
39 | func closeConn(db *sql.DB) {
40 | if db != nil {
41 | _ = db.Close()
42 | }
43 | }
44 |
45 | func (o *OutputPlugin) generateBulkInsertOnDuplicateKeyUpdateSQL(msgs []*core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) (string, []interface{}, error) {
46 | pks := make(map[string]interface{}, len(columnsMapper.PrimaryKeys))
47 | for _, pk := range columnsMapper.PrimaryKeys {
48 | pks[pk] = nil
49 | }
50 |
51 | updateColumnsIdx := 0
52 | columnNamesAssignWithoutPks := make([]string, len(columnsMapper.MapMapper)-len(columnsMapper.PrimaryKeys))
53 | allColumnNamesInSQL := make([]string, 0, len(columnsMapper.MapMapper))
54 | allColumnPlaceHolder := make([]string, 0, len(columnsMapper.MapMapper))
55 | for _, sourceColumn := range columnsMapper.MapMapperOrder {
56 | columnNameInSQL := fmt.Sprintf("`%s`", columnsMapper.MapMapper[sourceColumn])
57 | allColumnNamesInSQL = append(allColumnNamesInSQL, columnNameInSQL)
58 | allColumnPlaceHolder = append(allColumnPlaceHolder, "?")
59 | _, ok := pks[sourceColumn]
60 | if !ok {
61 | columnNamesAssignWithoutPks[updateColumnsIdx] = fmt.Sprintf("%s = VALUES(%s)", columnNameInSQL, columnNameInSQL)
62 | updateColumnsIdx++
63 | }
64 | }
65 | sqlInsert := fmt.Sprintf("INSERT INTO `%s`.`%s` (%s) VALUES ",
66 | targetSchema,
67 | targetTable,
68 | strings.Join(allColumnNamesInSQL, ","))
69 | args := make([]interface{}, 0, len(columnsMapper.MapMapper)*len(msgs))
70 | for i, msg := range msgs {
71 | switch msg.DmlMsg.Action {
72 | case core.InsertAction, core.UpdateAction:
73 | for _, sourceColumn := range columnsMapper.MapMapperOrder {
74 | columnData := msg.DmlMsg.Data[sourceColumn]
75 | args = append(args, columnData)
76 | }
77 | if i == 0 {
78 | sqlInsert += fmt.Sprintf("(%s)", strings.Join(allColumnPlaceHolder, ","))
79 | } else {
80 | sqlInsert += fmt.Sprintf(",(%s)", strings.Join(allColumnPlaceHolder, ","))
81 | }
82 | default:
83 | log.Fatalf("unhandled message type: %v", msg)
84 | }
85 | }
86 | sqlUpdate := fmt.Sprintf("ON DUPLICATE KEY UPDATE %s", strings.Join(columnNamesAssignWithoutPks, ","))
87 | return fmt.Sprintf("%s %s", sqlInsert, sqlUpdate), args, nil
88 | }
89 |
90 | func (o *OutputPlugin) generateSingleDeleteSQL(msg *core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) (string, []interface{}, error) {
91 | pks := make(map[string]interface{}, len(columnsMapper.PrimaryKeys))
92 | for _, pk := range columnsMapper.PrimaryKeys {
93 | pks[pk] = nil
94 | }
95 |
96 | var whereSql []string
97 | var args []interface{}
98 | for sourceColumn, targetColumn := range columnsMapper.MapMapper {
99 | pkData, ok := pks[sourceColumn]
100 | if !ok {
101 | continue
102 | }
103 | whereSql = append(whereSql, fmt.Sprintf("`%s` = ?", targetColumn))
104 | args = append(args, pkData)
105 | }
106 | if len(whereSql) == 0 {
107 | return "", nil, errors.Errorf("where sql is empty, probably missing pk")
108 | }
109 |
110 | stmt := fmt.Sprintf("DELETE FROM `%s`.`%s` WHERE %s", targetSchema, targetTable, strings.Join(whereSql, " AND "))
111 | return stmt, args, nil
112 | }
113 |
114 | func (o *OutputPlugin) generateBulkDeleteSQL(msgs []*core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) (string, []interface{}, error) {
115 | pkName := columnsMapper.PrimaryKeys[0]
116 | var whereSql []string
117 | var args []interface{}
118 | for _, msg := range msgs {
119 | pkData, ok := msg.DmlMsg.Data[pkName]
120 | if !ok {
121 | continue
122 | }
123 | whereSql = append(whereSql, "?")
124 | args = append(args, pkData)
125 |
126 | }
127 | targetPkName := columnsMapper.MapMapper[pkName]
128 | if len(whereSql) == 0 {
129 | return "", nil, errors.Errorf("where sql is empty, probably missing pk")
130 | }
131 |
132 | stmt := fmt.Sprintf("DELETE FROM `%s`.`%s` WHERE `%s` IN (%s)", targetSchema, targetTable, targetPkName, strings.Join(whereSql, ","))
133 | return stmt, args, nil
134 | }
135 |
--------------------------------------------------------------------------------
/outputs/starrocks/starrocks.go:
--------------------------------------------------------------------------------
1 | package starrocks
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/mitchellh/mapstructure"
7 | "github.com/siddontang/go-log/log"
8 | "github.com/sqlpub/qin-cdc/config"
9 | "github.com/sqlpub/qin-cdc/core"
10 | "github.com/sqlpub/qin-cdc/metas"
11 | "github.com/sqlpub/qin-cdc/metrics"
12 | "io"
13 | "net/http"
14 | "strings"
15 | "time"
16 | )
17 |
18 | type OutputPlugin struct {
19 | *config.StarrocksConfig
20 | Done chan bool
21 | metas *core.Metas
22 | msgTxnBuffer struct {
23 | size int
24 | tableMsgMap map[string][]*core.Msg
25 | }
26 | client *http.Client
27 | transport *http.Transport
28 | lastPosition string
29 | }
30 |
31 | func (o *OutputPlugin) Configure(conf map[string]interface{}) error {
32 | o.StarrocksConfig = &config.StarrocksConfig{}
33 | var targetConf = conf["target"]
34 | if err := mapstructure.Decode(targetConf, o.StarrocksConfig); err != nil {
35 | return err
36 | }
37 | return nil
38 | }
39 |
40 | func (o *OutputPlugin) NewOutput(metas *core.Metas) {
41 | o.Done = make(chan bool)
42 | o.metas = metas
43 | // options handle
44 | if o.StarrocksConfig.Options.BatchSize == 0 {
45 | o.StarrocksConfig.Options.BatchSize = DefaultBatchSize
46 | }
47 | if o.StarrocksConfig.Options.BatchIntervalMs == 0 {
48 | o.StarrocksConfig.Options.BatchIntervalMs = DefaultBatchIntervalMs
49 | }
50 | o.msgTxnBuffer.size = 0
51 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
52 |
53 | o.transport = &http.Transport{}
54 | o.client = &http.Client{
55 | Transport: o.transport,
56 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
57 | req.Header.Add("Authorization", "Basic "+o.auth())
58 | // log.Debugf("重定向请求到be: %v", req.URL)
59 | return nil // return nil nil回重定向。
60 | },
61 | }
62 | }
63 |
64 | func (o *OutputPlugin) Start(out chan *core.Msg, pos core.Position) {
65 | // first pos
66 | o.lastPosition = pos.Get()
67 | go func() {
68 | ticker := time.NewTicker(time.Millisecond * time.Duration(o.Options.BatchIntervalMs))
69 | defer ticker.Stop()
70 | for {
71 | select {
72 | case data := <-out:
73 | switch data.Type {
74 | case core.MsgCtl:
75 | o.lastPosition = data.InputContext.Pos
76 | case core.MsgDML:
77 | o.appendMsgTxnBuffer(data)
78 | if o.msgTxnBuffer.size >= o.StarrocksConfig.Options.BatchSize {
79 | o.flushMsgTxnBuffer(pos)
80 | }
81 | }
82 | case <-ticker.C:
83 | o.flushMsgTxnBuffer(pos)
84 | case <-o.Done:
85 | o.flushMsgTxnBuffer(pos)
86 | return
87 | }
88 |
89 | }
90 | }()
91 | }
92 |
93 | func (o *OutputPlugin) Close() {
94 | log.Infof("output is closing...")
95 | close(o.Done)
96 | <-o.Done
97 | log.Infof("output is closed")
98 | }
99 |
100 | func (o *OutputPlugin) appendMsgTxnBuffer(msg *core.Msg) {
101 | key := metas.GenerateMapRouterKey(msg.Database, msg.Table)
102 | o.msgTxnBuffer.tableMsgMap[key] = append(o.msgTxnBuffer.tableMsgMap[key], msg)
103 | o.msgTxnBuffer.size += 1
104 | }
105 |
106 | func (o *OutputPlugin) flushMsgTxnBuffer(pos core.Position) {
107 | defer func() {
108 | // flush position
109 | err := pos.Update(o.lastPosition)
110 | if err != nil {
111 | log.Fatalf(err.Error())
112 | }
113 | }()
114 |
115 | if o.msgTxnBuffer.size == 0 {
116 | return
117 | }
118 | // table level export
119 | for k, msgs := range o.msgTxnBuffer.tableMsgMap {
120 | columnsMapper := o.metas.Routers.Maps[k].ColumnsMapper
121 | targetSchema := o.metas.Routers.Maps[k].TargetSchema
122 | targetTable := o.metas.Routers.Maps[k].TargetTable
123 | err := o.execute(msgs, columnsMapper, targetSchema, targetTable)
124 | if err != nil {
125 | log.Fatalf("do %s bulk err %v", PluginName, err)
126 | }
127 | }
128 | o.clearMsgTxnBuffer()
129 | }
130 |
131 | func (o *OutputPlugin) clearMsgTxnBuffer() {
132 | o.msgTxnBuffer.size = 0
133 | o.msgTxnBuffer.tableMsgMap = make(map[string][]*core.Msg)
134 | }
135 |
136 | func (o *OutputPlugin) execute(msgs []*core.Msg, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) error {
137 | if len(msgs) == 0 {
138 | return nil
139 | }
140 | var jsonList []string
141 |
142 | jsonList = o.generateJson(msgs)
143 | for _, s := range jsonList {
144 | log.Debugf("%s load %s.%s row data: %v", PluginName, targetSchema, targetTable, s)
145 | }
146 | log.Debugf("%s bulk load %s.%s row data num: %d", PluginName, targetSchema, targetTable, len(jsonList))
147 | var err error
148 | for i := 0; i < RetryCount; i++ {
149 | err = o.sendData(jsonList, columnsMapper, targetSchema, targetTable)
150 | if err != nil {
151 | log.Warnf("send data failed, err: %v, execute retry...", err.Error())
152 | if i+1 == RetryCount {
153 | break
154 | }
155 | time.Sleep(time.Duration(RetryInterval*(i+1)) * time.Second)
156 | continue
157 | }
158 | break
159 | }
160 | return err
161 | }
162 |
163 | func (o *OutputPlugin) sendData(content []string, columnsMapper metas.ColumnsMapper, targetSchema string, targetTable string) error {
164 | loadUrl := fmt.Sprintf("http://%s:%d/api/%s/%s/_stream_load",
165 | o.Host, o.LoadPort, targetSchema, targetTable)
166 | newContent := `[` + strings.Join(content, ",") + `]`
167 | req, _ := http.NewRequest("PUT", loadUrl, strings.NewReader(newContent))
168 |
169 | // req.Header.Add
170 | req.Header.Add("Authorization", "Basic "+o.auth())
171 | req.Header.Add("Expect", "100-continue")
172 | req.Header.Add("strict_mode", "true")
173 | // req.Header.Add("label", "39c25a5c-7000-496e-a98e-348a264c81de")
174 | req.Header.Add("format", "json")
175 | req.Header.Add("strip_outer_array", "true")
176 |
177 | var columnArray []string
178 | for _, column := range columnsMapper.SourceColumns {
179 | columnArray = append(columnArray, column)
180 | }
181 | columnArray = append(columnArray, DeleteColumn)
182 | columns := fmt.Sprintf("%s, __op = %s", strings.Join(columnArray, ","), DeleteColumn)
183 | req.Header.Add("columns", columns)
184 |
185 | response, err := o.client.Do(req)
186 | if err != nil {
187 | return err
188 | }
189 | defer func(Body io.ReadCloser) {
190 | _ = Body.Close()
191 | }(response.Body)
192 | returnMap, err := o.parseResponse(response)
193 | if err != nil {
194 | return err
195 | }
196 | if returnMap["Status"] != "Success" {
197 | message := returnMap["Message"]
198 | errorUrl := returnMap["ErrorURL"]
199 | errorMsg := message.(string) +
200 | fmt.Sprintf(", targetTable: %s.%s", targetSchema, targetTable) +
201 | fmt.Sprintf(", visit ErrorURL to view error details, ErrorURL: %s", errorUrl)
202 | return errors.New(errorMsg)
203 | }
204 | // prom write event number counter
205 | numberLoadedRows := returnMap["NumberLoadedRows"]
206 | metrics.OpsWriteProcessed.Add(numberLoadedRows.(float64))
207 | return nil
208 | }
209 |
--------------------------------------------------------------------------------
/outputs/starrocks/starrocks_meta.go:
--------------------------------------------------------------------------------
1 | package starrocks
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/juju/errors"
7 | "github.com/mitchellh/mapstructure"
8 | "github.com/sqlpub/qin-cdc/config"
9 | "github.com/sqlpub/qin-cdc/metas"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type MetaPlugin struct {
15 | *config.StarrocksConfig
16 | tables map[string]*metas.Table
17 | tablesVersion map[string]*metas.Table
18 | db *sql.DB
19 | mu sync.Mutex
20 | }
21 |
22 | func (m *MetaPlugin) Configure(conf map[string]interface{}) error {
23 | m.StarrocksConfig = &config.StarrocksConfig{}
24 | var target = conf["target"]
25 | if err := mapstructure.Decode(target, m.StarrocksConfig); err != nil {
26 | return err
27 | }
28 | return nil
29 | }
30 |
31 | func (m *MetaPlugin) LoadMeta(routers []*metas.Router) (err error) {
32 | m.tables = make(map[string]*metas.Table)
33 | m.tablesVersion = make(map[string]*metas.Table)
34 | dsn := fmt.Sprintf(
35 | "%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4&timeout=3s&interpolateParams=true",
36 | m.StarrocksConfig.UserName, m.StarrocksConfig.Password,
37 | m.StarrocksConfig.Host, m.StarrocksConfig.Port)
38 | m.db, err = sql.Open("mysql", dsn)
39 | if err != nil {
40 | return err
41 | }
42 | m.db.SetConnMaxLifetime(time.Minute * 3)
43 | m.db.SetMaxOpenConns(2)
44 | m.db.SetMaxIdleConns(2)
45 | for _, router := range routers {
46 | rows, err := m.db.Query("select "+
47 | "column_name,column_default,is_nullable,data_type,column_type,column_key "+
48 | "from information_schema.columns "+
49 | "where table_schema = ? and table_name = ? "+
50 | "order by ordinal_position", router.TargetSchema, router.TargetTable)
51 | if err != nil {
52 | return err
53 | }
54 | table := &metas.Table{
55 | Schema: router.TargetSchema,
56 | Name: router.TargetTable,
57 | }
58 | for rows.Next() {
59 | var columnName, isNullable, dataType, columnType, columnKey string
60 | var columnDefault sql.NullString
61 | err = rows.Scan(&columnName, &columnDefault, &isNullable, &dataType, &columnType, &columnKey)
62 | if err != nil {
63 | return err
64 | }
65 | var column metas.Column
66 | column.Name = columnName
67 | column.RawType = columnType
68 | switch dataType {
69 | case "tinyint", "smallint", "mediumint", "int", "bigint":
70 | column.Type = metas.TypeNumber
71 | case "float", "double":
72 | column.Type = metas.TypeFloat
73 | case "enum":
74 | column.Type = metas.TypeEnum
75 | case "set":
76 | column.Type = metas.TypeSet
77 | case "datetime":
78 | column.Type = metas.TypeDatetime
79 | case "timestamp":
80 | column.Type = metas.TypeTimestamp
81 | case "date":
82 | column.Type = metas.TypeDate
83 | case "time":
84 | column.Type = metas.TypeTime
85 | case "bit":
86 | column.Type = metas.TypeBit
87 | case "json":
88 | column.Type = metas.TypeJson
89 | case "decimal":
90 | column.Type = metas.TypeDecimal
91 | default:
92 | column.Type = metas.TypeString
93 | }
94 | if columnKey == "PRI" {
95 | column.IsPrimaryKey = true
96 | }
97 | table.Columns = append(table.Columns, column)
98 | }
99 | if table.Columns == nil {
100 | return errors.Errorf("load meta %s.%s not found", router.TargetSchema, router.TargetTable)
101 | }
102 | err = m.Add(table)
103 | if err != nil {
104 | return err
105 | }
106 | }
107 | return nil
108 | }
109 |
110 | func (m *MetaPlugin) GetMeta(router *metas.Router) (table interface{}, err error) {
111 | return m.Get(router.SourceSchema, router.SourceTable)
112 | }
113 |
114 | func (m *MetaPlugin) Get(schema string, tableName string) (table *metas.Table, err error) {
115 | key := metas.GenerateMapRouterKey(schema, tableName)
116 | m.mu.Lock()
117 | defer m.mu.Unlock()
118 | return m.tables[key], err
119 | }
120 |
121 | func (m *MetaPlugin) GetAll() map[string]*metas.Table {
122 | m.mu.Lock()
123 | defer m.mu.Unlock()
124 | return m.tables
125 | }
126 |
127 | func (m *MetaPlugin) GetVersion(schema string, tableName string, version uint) (table *metas.Table, err error) {
128 | key := metas.GenerateMapRouterVersionKey(schema, tableName, version)
129 | m.mu.Lock()
130 | defer m.mu.Unlock()
131 | return m.tablesVersion[key], err
132 | }
133 |
134 | func (m *MetaPlugin) GetVersions(schema string, tableName string) []*metas.Table {
135 | m.mu.Lock()
136 | defer m.mu.Unlock()
137 | tables := make([]*metas.Table, 0)
138 | for k, table := range m.tablesVersion {
139 | s, t, _ := metas.SplitMapRouterVersionKey(k)
140 | if schema == s && tableName == t {
141 | tables = append(tables, table)
142 | }
143 | }
144 | return tables
145 | }
146 |
147 | func (m *MetaPlugin) Add(newTable *metas.Table) error {
148 | m.mu.Lock()
149 | defer m.mu.Unlock()
150 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
151 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
152 | return nil
153 | }
154 |
155 | func (m *MetaPlugin) Update(newTable *metas.Table) error {
156 | m.mu.Lock()
157 | defer m.mu.Unlock()
158 | newTable.Version += 1
159 | m.tables[metas.GenerateMapRouterKey(newTable.Schema, newTable.Name)] = newTable
160 | m.tablesVersion[metas.GenerateMapRouterVersionKey(newTable.Schema, newTable.Name, newTable.Version)] = newTable
161 | return nil
162 | }
163 |
164 | func (m *MetaPlugin) Delete(schema string, name string) error {
165 | m.mu.Lock()
166 | defer m.mu.Unlock()
167 | delete(m.tables, metas.GenerateMapRouterKey(schema, name))
168 | for _, table := range m.GetVersions(schema, name) {
169 | delete(m.tablesVersion, metas.GenerateMapRouterVersionKey(schema, name, table.Version))
170 | }
171 | return nil
172 | }
173 |
174 | func (m *MetaPlugin) Save() error {
175 | return nil
176 | }
177 |
178 | func (m *MetaPlugin) Close() {
179 | if m.db != nil {
180 | _ = m.db.Close()
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/outputs/starrocks/starrocks_utils.go:
--------------------------------------------------------------------------------
1 | package starrocks
2 |
3 | import (
4 | "encoding/base64"
5 | "github.com/goccy/go-json"
6 | "github.com/siddontang/go-log/log"
7 | "github.com/sqlpub/qin-cdc/core"
8 | "io"
9 | "net/http"
10 | )
11 |
12 | const (
13 | PluginName = "starrocks"
14 | DefaultBatchSize int = 10240
15 | DefaultBatchIntervalMs int = 3000
16 | DeleteColumn string = "_delete_sign_"
17 | RetryCount int = 3
18 | RetryInterval int = 5
19 | )
20 |
21 | func (o *OutputPlugin) auth() string {
22 | s := o.UserName + ":" + o.Password
23 | b := []byte(s)
24 |
25 | sEnc := base64.StdEncoding.EncodeToString(b)
26 | return sEnc
27 | }
28 |
29 | func (o *OutputPlugin) parseResponse(response *http.Response) (map[string]interface{}, error) {
30 | var result map[string]interface{}
31 | body, err := io.ReadAll(response.Body)
32 | if err == nil {
33 | err = json.Unmarshal(body, &result)
34 | }
35 |
36 | return result, err
37 | }
38 |
39 | func (o *OutputPlugin) generateJson(msgs []*core.Msg) []string {
40 | var jsonList []string
41 |
42 | for _, event := range msgs {
43 | switch event.DmlMsg.Action {
44 | case core.InsertAction:
45 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
46 | event.DmlMsg.Data[DeleteColumn] = 0
47 | b, _ := json.Marshal(event.DmlMsg.Data)
48 | jsonList = append(jsonList, string(b))
49 | case core.UpdateAction:
50 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
51 | event.DmlMsg.Data[DeleteColumn] = 0
52 | b, _ := json.Marshal(event.DmlMsg.Data)
53 | jsonList = append(jsonList, string(b))
54 | case core.DeleteAction: // starrocks2.4版本只支持primary key模型load delete
55 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
56 | event.DmlMsg.Data[DeleteColumn] = 1
57 | b, _ := json.Marshal(event.DmlMsg.Data)
58 | jsonList = append(jsonList, string(b))
59 | case core.ReplaceAction: // for mongo
60 | // 增加虚拟列,标识操作类型 (stream load opType:UPSERT 0,DELETE:1)
61 | event.DmlMsg.Data[DeleteColumn] = 0
62 | b, _ := json.Marshal(event.DmlMsg.Data)
63 | jsonList = append(jsonList, string(b))
64 | default:
65 | log.Fatalf("unhandled message type: %v", event)
66 | }
67 | }
68 | return jsonList
69 | }
70 |
--------------------------------------------------------------------------------
/registry/registry.go:
--------------------------------------------------------------------------------
1 | package registry
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/siddontang/go-log/log"
7 | "sync"
8 | )
9 |
10 | type PluginType string
11 |
12 | const (
13 | InputPlugin PluginType = "input"
14 | OutputPlugin PluginType = "output"
15 | MetaPlugin PluginType = "meta"
16 | PositionPlugin PluginType = "position"
17 | )
18 |
19 | type Plugin interface {
20 | Configure(data map[string]interface{}) error
21 | }
22 |
23 | var registry map[PluginType]map[string]Plugin
24 | var mutex sync.Mutex
25 |
26 | func init() {
27 | registry = make(map[PluginType]map[string]Plugin)
28 | }
29 |
30 | func RegisterPlugin(pluginType PluginType, name string, v Plugin) {
31 | mutex.Lock()
32 | defer mutex.Unlock()
33 |
34 | log.Debugf("[RegisterPlugin] type: %v, name: %v", pluginType, name)
35 |
36 | _, ok := registry[pluginType]
37 | if !ok {
38 | registry[pluginType] = make(map[string]Plugin)
39 | }
40 |
41 | _, ok = registry[pluginType][name]
42 | if ok {
43 | panic(fmt.Sprintf("plugin already exists, type: %v, name: %v", pluginType, name))
44 | }
45 | registry[pluginType][name] = v
46 | }
47 |
48 | func GetPlugin(pluginType PluginType, name string) (Plugin, error) {
49 | mutex.Lock()
50 | defer mutex.Unlock()
51 |
52 | if registry == nil {
53 | return nil, errors.Errorf("empty registry")
54 | }
55 |
56 | plugins, ok := registry[pluginType]
57 | if !ok {
58 | return nil, errors.Errorf("empty plugin type: %v, name: %v", pluginType, name)
59 | }
60 | p, ok := plugins[name]
61 | if !ok {
62 | return nil, errors.Errorf("empty plugin, type: %v, name: %v", pluginType, name)
63 | }
64 | log.Infof("load %v plugin: %v", pluginType, name)
65 | return p, nil
66 | }
67 |
--------------------------------------------------------------------------------
/transforms/trans_delete_column.go:
--------------------------------------------------------------------------------
1 | package transforms
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/sqlpub/qin-cdc/core"
7 | "github.com/sqlpub/qin-cdc/utils"
8 | )
9 |
10 | const DeleteColumnTransName = "delete-column"
11 |
12 | type DeleteColumnTrans struct {
13 | name string
14 | matchSchema string
15 | matchTable string
16 | columns []string
17 | }
18 |
19 | func (dct *DeleteColumnTrans) NewTransform(config map[string]interface{}) error {
20 | columns := config["columns"]
21 | c, ok := utils.CastToSlice(columns)
22 | if !ok {
23 | return errors.Trace(errors.New("'column' should be an array"))
24 | }
25 |
26 | columnsString, err := utils.CastSliceInterfaceToSliceString(c)
27 | if err != nil {
28 | return errors.Trace(errors.New("'column' should be an array of string"))
29 | }
30 | dct.name = DeleteColumnTransName
31 | dct.matchSchema = fmt.Sprintf("%v", config["match-schema"])
32 | dct.matchTable = fmt.Sprintf("%v", config["match-table"])
33 | dct.columns = columnsString
34 | return nil
35 | }
36 |
37 | func (dct *DeleteColumnTrans) Transform(msg *core.Msg) bool {
38 | if dct.matchSchema == msg.Database && dct.matchTable == msg.Table {
39 | for _, column := range dct.columns {
40 | value := FindColumn(msg.DmlMsg.Data, column)
41 | if value != nil {
42 | delete(msg.DmlMsg.Data, column)
43 | }
44 | }
45 | }
46 | return false
47 | }
48 |
--------------------------------------------------------------------------------
/transforms/trans_rename_column.go:
--------------------------------------------------------------------------------
1 | package transforms
2 |
3 | import (
4 | "fmt"
5 | "github.com/juju/errors"
6 | "github.com/sqlpub/qin-cdc/core"
7 | "github.com/sqlpub/qin-cdc/utils"
8 | )
9 |
10 | const RenameColumnTransName = "rename-column"
11 |
12 | type RenameColumnTrans struct {
13 | name string
14 | matchSchema string
15 | matchTable string
16 | columns []string
17 | renameAs []string
18 | }
19 |
20 | func (rct *RenameColumnTrans) NewTransform(config map[string]interface{}) error {
21 | columns, ok := config["columns"]
22 | if !ok {
23 | return errors.Trace(errors.New("'columns' is not configured"))
24 | }
25 | renameAs, ok := config["rename-as"]
26 | if !ok {
27 | return errors.Trace(errors.New("'rename-as' is not configured"))
28 | }
29 |
30 | c, ok := utils.CastToSlice(columns)
31 | if !ok {
32 | return errors.Trace(errors.New("'columns' should be an array"))
33 | }
34 |
35 | columnsString, err := utils.CastSliceInterfaceToSliceString(c)
36 | if err != nil {
37 | return errors.Trace(errors.New("'columns' should be an array of string"))
38 | }
39 |
40 | ra, ok := utils.CastToSlice(renameAs)
41 | if !ok {
42 | return errors.Trace(errors.New("'rename-as' should be an array"))
43 | }
44 |
45 | renameAsString, err := utils.CastSliceInterfaceToSliceString(ra)
46 | if err != nil {
47 | return errors.Trace(errors.New("'cast-as' should be an array of string"))
48 | }
49 |
50 | if len(c) != len(ra) {
51 | return errors.Trace(errors.New("'columns' should have the same length of 'rename-as'"))
52 | }
53 |
54 | rct.name = RenameColumnTransName
55 | rct.matchSchema = fmt.Sprintf("%v", config["match-schema"])
56 | rct.matchTable = fmt.Sprintf("%v", config["match-table"])
57 | rct.columns = columnsString
58 | rct.renameAs = renameAsString
59 | return nil
60 | }
61 |
62 | func (rct *RenameColumnTrans) Transform(msg *core.Msg) bool {
63 | if rct.matchSchema == msg.Database && rct.matchTable == msg.Table {
64 | for i, column := range rct.columns {
65 | value := FindColumn(msg.DmlMsg.Data, column)
66 | if value != nil {
67 | renameAsColumn := rct.renameAs[i]
68 | msg.DmlMsg.Data[renameAsColumn] = msg.DmlMsg.Data[column]
69 | delete(msg.DmlMsg.Data, column)
70 | }
71 | }
72 | }
73 | return false
74 | }
75 |
--------------------------------------------------------------------------------
/transforms/transforms.go:
--------------------------------------------------------------------------------
1 | package transforms
2 |
3 | import (
4 | "github.com/siddontang/go-log/log"
5 | "github.com/sqlpub/qin-cdc/config"
6 | "github.com/sqlpub/qin-cdc/core"
7 | "github.com/sqlpub/qin-cdc/metas"
8 | "github.com/sqlpub/qin-cdc/metrics"
9 | )
10 |
11 | type MatcherTransforms []core.Transform
12 |
13 | func NewMatcherTransforms(transConfigs []config.TransformConfig, routers *metas.Routers) (matcher MatcherTransforms) {
14 | for _, tc := range transConfigs {
15 | switch typ := tc.Type; typ {
16 | case RenameColumnTransName:
17 | rct := &RenameColumnTrans{}
18 | if err := rct.NewTransform(tc.Config); err != nil {
19 | log.Fatal(err)
20 | }
21 | // rename router mapper column name to new column name
22 | for _, router := range routers.Raws {
23 | if router.SourceSchema == rct.matchSchema && router.SourceTable == rct.matchTable {
24 | for i, column := range rct.columns {
25 | for i2, sourceColumn := range router.ColumnsMapper.SourceColumns {
26 | if sourceColumn == column {
27 | router.ColumnsMapper.SourceColumns[i2] = rct.renameAs[i]
28 | }
29 | }
30 | }
31 | }
32 | }
33 | log.Infof("load transform: %s", RenameColumnTransName)
34 | matcher = append(matcher, rct)
35 | case DeleteColumnTransName:
36 | dct := &DeleteColumnTrans{}
37 | if err := dct.NewTransform(tc.Config); err != nil {
38 | log.Fatal(err)
39 | }
40 | // delete router mapper column name
41 | for _, router := range routers.Raws {
42 | if router.SourceSchema == dct.matchSchema && router.SourceTable == dct.matchTable {
43 | for _, column := range dct.columns {
44 | sourceColumns := make([]string, len(router.ColumnsMapper.SourceColumns))
45 | copy(sourceColumns, router.ColumnsMapper.SourceColumns)
46 | for i2, sourceColumn := range sourceColumns {
47 | if sourceColumn == column {
48 | router.ColumnsMapper.SourceColumns = append(router.ColumnsMapper.SourceColumns[:i2], router.ColumnsMapper.SourceColumns[i2+1:]...)
49 | }
50 | }
51 | }
52 | }
53 | }
54 | log.Infof("load transform: %s", DeleteColumnTransName)
55 | matcher = append(matcher, dct)
56 | default:
57 | log.Warnf("transform: %s unhandled will not take effect", typ)
58 | }
59 | }
60 | return matcher
61 | }
62 |
63 | func (m MatcherTransforms) IterateTransforms(msg *core.Msg) bool {
64 | for _, trans := range m {
65 | if trans.Transform(msg) {
66 | log.Debugf("transform msg %v", msg.DmlMsg.Data)
67 | return true
68 | }
69 | }
70 | return false
71 | }
72 |
73 | func (m MatcherTransforms) Start(in chan *core.Msg, out chan *core.Msg) {
74 | go func() {
75 | for data := range in {
76 | log.Debugf(data.ToString())
77 | if !m.IterateTransforms(data) {
78 | out <- data
79 | handleMetrics(data)
80 | }
81 | }
82 | }()
83 | }
84 |
85 | func handleMetrics(data *core.Msg) {
86 | if data.Type == core.MsgDML {
87 | metrics.OpsReadProcessed.Inc()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/transforms/utils.go:
--------------------------------------------------------------------------------
1 | package transforms
2 |
3 | func FindColumn(data map[string]interface{}, name string) interface{} {
4 | if value, ok := data[name]; ok {
5 | return value
6 | }
7 | return nil
8 | }
9 |
--------------------------------------------------------------------------------
/utils/daemon.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/sevlyar/go-daemon"
5 | "github.com/siddontang/go-log/log"
6 | )
7 |
8 | func Daemon(inputParam *Help) {
9 | if *inputParam.Daemon {
10 | cntxt := &daemon.Context{
11 | PidFileName: GetExecPath() + "/qin-cdc.pid",
12 | PidFilePerm: 0644,
13 | LogFileName: *inputParam.LogFile,
14 | LogFilePerm: 0640,
15 | WorkDir: "./",
16 | Umask: 027,
17 | }
18 | d, err := cntxt.Reborn()
19 | if err != nil {
20 | log.Fatal("daemon mode run failed, err: ", err)
21 | }
22 |
23 | if d != nil {
24 | return
25 | }
26 | defer func(cntxt *daemon.Context) {
27 | err = cntxt.Release()
28 | if err != nil {
29 | log.Fatal("daemon release failed, err: ", err)
30 | }
31 | }(cntxt)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/utils/file_path.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/siddontang/go-log/log"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | func GetExecPath() string {
10 | ex, err := os.Executable()
11 | if err != nil {
12 | log.Fatal("get exec path error: ", err)
13 | }
14 | exPath := filepath.Dir(ex)
15 | return exPath
16 | }
17 |
--------------------------------------------------------------------------------
/utils/help.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "flag"
5 | "github.com/go-demo/version"
6 | "github.com/siddontang/go-log/log"
7 | "os"
8 | )
9 |
10 | type Help struct {
11 | printVersion bool
12 | ConfigFile *string
13 | LogLevel *string
14 | LogFile *string
15 | Daemon *bool
16 | HttpPort *uint
17 | }
18 |
19 | func InitHelp() (help *Help) {
20 | help = &Help{}
21 | help.ConfigFile = flag.String("config", "", "config file")
22 | help.LogLevel = flag.String("level", "info", "log level")
23 | help.LogFile = flag.String("log-file", "qin-cdc.log", "log file")
24 | help.Daemon = flag.Bool("daemon", false, "daemon run, must specify param 'log-file'")
25 | help.HttpPort = flag.Uint("http-port", 7716, "http monitor port, curl http://localhost:7716/metrics")
26 | flag.BoolVar(&help.printVersion, "version", false, "print program build version")
27 | flag.Parse()
28 | if help.printVersion {
29 | version.PrintVersion()
30 | os.Exit(0)
31 | }
32 | log.Infof("starting version: %s", version.Version)
33 | return help
34 | }
35 |
--------------------------------------------------------------------------------
/utils/http.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "github.com/prometheus/client_golang/prometheus/promhttp"
6 | "github.com/siddontang/go-log/log"
7 | "github.com/sqlpub/qin-cdc/api"
8 | "github.com/sqlpub/qin-cdc/metrics"
9 | "net/http"
10 | "time"
11 | )
12 |
13 | func StartHttp(inputParam *Help) {
14 | // Start prometheus http monitor
15 | go func() {
16 | metrics.OpsStartTime.Set(float64(time.Now().Unix()))
17 | log.Infof("starting http on port: %d", *inputParam.HttpPort)
18 | http.Handle("/metrics", promhttp.Handler())
19 | httpPortAddr := fmt.Sprintf(":%d", *inputParam.HttpPort)
20 | err := http.ListenAndServe(httpPortAddr, nil)
21 | if err != nil {
22 | log.Fatalf("starting http monitor error: %v", err)
23 | }
24 | }()
25 | }
26 |
27 | func InitHttpApi() {
28 | http.HandleFunc("/api/addRouter", api.AddRouter())
29 | http.HandleFunc("/api/delRule", api.DelRouter())
30 | http.HandleFunc("/api/getRule", api.GetRouter())
31 | http.HandleFunc("/api/pause", api.PauseRouter())
32 | http.HandleFunc("/api/resume", api.ResumeRouter())
33 | }
34 |
--------------------------------------------------------------------------------
/utils/input_param.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/siddontang/go-log/log"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | func InputParamHandle(inputParam *Help) {
10 | if *inputParam.ConfigFile == "" {
11 | log.Infof("-config param does not exist!")
12 | os.Exit(0)
13 | } else {
14 | abs, err := filepath.Abs(*inputParam.ConfigFile)
15 | if err != nil {
16 | log.Fatal("-config abs error: ", err.Error())
17 | }
18 | *inputParam.ConfigFile = abs
19 | }
20 | if *inputParam.Daemon {
21 | if *inputParam.LogFile == "" {
22 | log.Infof("daemon mode, must specify -log-file param!")
23 | os.Exit(0)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/utils/type_cast.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/juju/errors"
5 | "reflect"
6 | )
7 |
8 | func CastToSlice(arg interface{}) (out []interface{}, ok bool) {
9 | slice, success := TakeArg(arg, reflect.Slice)
10 | if !success {
11 | ok = false
12 | return
13 | }
14 | c := slice.Len()
15 | out = make([]interface{}, c)
16 | for i := 0; i < c; i++ {
17 | out[i] = slice.Index(i).Interface()
18 | }
19 | return out, true
20 | }
21 |
22 | func TakeArg(arg interface{}, kind reflect.Kind) (val reflect.Value, ok bool) {
23 | val = reflect.ValueOf(arg)
24 | if val.Kind() == kind {
25 | ok = true
26 | }
27 | return
28 | }
29 |
30 | func CastSliceInterfaceToSliceString(a []interface{}) ([]string, error) {
31 | aStrings := make([]string, len(a))
32 | for i, c := range a {
33 | name, ok := c.(string)
34 | if !ok {
35 | return nil, errors.Trace(errors.New("should be an array of string"))
36 | }
37 | aStrings[i] = name
38 | }
39 | return aStrings, nil
40 | }
41 |
--------------------------------------------------------------------------------