Skip to content

Commit f8385a2

Browse files
authored
Merge pull request #28 from KilikFr/slave-skip-transaction
slave skip transaction
2 parents 62532b8 + b86655a commit f8385a2

File tree

6 files changed

+149
-8
lines changed

6 files changed

+149
-8
lines changed

src/Controller/ServerController.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,46 @@ public function switchSlaveToNextMasterLogFileForChannel(Request $request, Serve
460460
return new JsonResponse(null, Response::HTTP_OK);
461461
}
462462

463+
/**
464+
* @Route("/{server}/slave/channel/{channel}/skip-transaction", name="server_slave_channel_skip_transaction_ajax", methods={"POST"})
465+
* @ParamConverter("server", options={"mapping": {"server" : "name"}})
466+
*
467+
* @return JsonResponse
468+
*/
469+
public function skiptransactionForChannel(Server $server, int $channel, SlaveService $slaveService): JsonResponse
470+
{
471+
$slave = $this->getDoctrine()->getRepository(Slave::class)->findOneBy(
472+
['server' => $server, 'channelName' => $channel]
473+
);
474+
475+
if ($slave === null) {
476+
$this->logger->error('slave for server '.$server->getName().' and channel '.$channel.' not found', [
477+
'server_id' => $server->getId(),
478+
'server_name' => $server->getName(),
479+
'channel' => $channel,
480+
]);
481+
return new JsonResponse(null, Response::HTTP_NOT_FOUND);
482+
}
483+
484+
try {
485+
if (null === $slaveService->getLastTransactionError($slave, $channel)) {
486+
return new JsonResponse(['message' => 'No error detected, aborting skip'], Response::HTTP_OK);
487+
}
488+
$slaveService->skipTransaction($slave, $channel);
489+
490+
return new JsonResponse(['message' => 'Transaction skipped'], Response::HTTP_OK);
491+
} catch (\Exception $e) {
492+
$this->logger->error('Error while skipping transaction file for channel '.$channel, [
493+
'server_id' => $server->getId(),
494+
'server_name' => $server->getName(),
495+
'channel' => $channel,
496+
'next_position' => 0,
497+
'exception' => $e->getMessage(),
498+
]);
499+
return new JsonResponse(null, Response::HTTP_INTERNAL_SERVER_ERROR);
500+
}
501+
}
502+
463503
/**
464504
* @Route("/{server}/processlist", name="server_processlist_ajax", methods={"GET"})
465505
* @ParamConverter("server", options={"mapping": {"server" : "name"}})

src/Services/SlaveService.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public function save(Slave $slave, bool $flush = false)
163163
}
164164
}
165165

166-
public function switchToNextMasterLogFile($slave, int $channel, string $nextMasterLogFile)
166+
public function switchToNextMasterLogFile(Slave $slave, int $channel, string $nextMasterLogFile)
167167
{
168168
$connection = $this->connectionService->getServerConnection($slave->getServer());
169169

@@ -181,4 +181,38 @@ public function switchToNextMasterLogFile($slave, int $channel, string $nextMast
181181
throw new \Exception($message);
182182
}
183183
}
184+
185+
public function getLastTransactionError(Slave $slave, int $channel): ?string
186+
{
187+
$connection = $this->connectionService->getServerConnection($slave->getServer());
188+
189+
$stmt = $connection->query(
190+
sprintf("SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE CHANNEL_NAME='%s';", $channel)
191+
);
192+
193+
if (false === $stmt) {
194+
$message = sprintf('error (%s): %s', $connection->errorCode(), $connection->errorInfo()[2]);
195+
throw new \Exception($message);
196+
}
197+
198+
return $stmt->fetch()['LAST_ERROR_MESSAGE'] ?: null;
199+
}
200+
201+
public function skipTransaction($slave, int $channel)
202+
{
203+
$connection = $this->connectionService->getServerConnection($slave->getServer());
204+
205+
$stmt = $connection->exec(
206+
sprintf("STOP SLAVE FOR CHANNEL '%s'; SELECT @gtid := (SELECT LAST_SEEN_TRANSACTION FROM performance_schema.replication_applier_status_by_worker WHERE CHANNEL_NAME='%s'); SET @@SESSION.GTID_NEXT=@gtid; BEGIN; COMMIT; SET @@SESSION.GTID_NEXT='AUTOMATIC'; START SLAVE FOR CHANNEL '%s';",
207+
$channel,
208+
$channel,
209+
$channel
210+
)
211+
);
212+
213+
if (false === $stmt) {
214+
$message = sprintf('error (%s): %s', $connection->errorCode(), $connection->errorInfo()[2]);
215+
throw new \Exception($message);
216+
}
217+
}
184218
}

src/Twig/AppExtension.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Twig;
44

5+
use Doctrine\SqlFormatter\SqlFormatter;
56
use Twig\Extension\AbstractExtension;
67
use Twig\TwigFilter;
78

@@ -11,6 +12,8 @@ public function getFilters(): array
1112
{
1213
return [
1314
new TwigFilter('format_duration', [$this, 'formatDuration']),
15+
new TwigFilter('format_sql', [$this, 'formatSQL']),
16+
new TwigFilter('highlight_sql', [$this, 'highlightSQL']),
1417
];
1518
}
1619

@@ -21,4 +24,13 @@ public function formatDuration(int $seconds): string
2124

2225
return $timeFrom->diff($timeTo)->format('%a days %h hours %i minutes %s seconds');
2326
}
27+
28+
public function formatSQL(string $query): string
29+
{
30+
return (new SqlFormatter())->format($query);
31+
}
32+
public function highlightSQL(string $query): string
33+
{
34+
return (new SqlFormatter())->highlight($query);
35+
}
2436
}

templates/server/channels_status_ajax.html.twig

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@
5050
<a href="#" class="btn btn-info btn-circle" data-toggle="modal" data-target="#slave-status-modal-{{ slave.channelName }}" title="Display complete slave status">
5151
<i class="fas fa-eye"></i>
5252
</a>
53-
<a href="#" class="btn btn-primary btn-circle" data-toggle="modal" data-target="#switch-next-master-log-file-modal-{{ slave.channelName }}" title="Switch to next position">
54-
<i class="fas fa-forward"></i>
53+
<a href="#" class="btn btn-primary btn-circle" data-toggle="modal" data-target="#skip-transaction-modal-{{ slave.channelName }}" title="Skip transaction">
54+
<i class="fas fa-step-forward"></i>
55+
</a>
56+
<a href="#" class="btn btn-primary btn-circle" data-toggle="modal" data-target="#switch-next-master-log-file-modal-{{ slave.channelName }}" title="Switch to next file">
57+
<i class="fas fa-fast-forward"></i>
5558
</a>
5659
</div>
5760
</div>
@@ -101,11 +104,9 @@
101104
<div class="modal-body">
102105
{% set next = (slave.masterLogFile | replace({'mysql-bin.': ""}) + 1) | format_number({min_integer_digit:'6', grouping_used:''}) %}
103106
{% set nextMasterLogFile = 'mysql-bin.'~next %}
104-
<pre>
105-
STOP SLAVE FOR CHANNEL '{{ slave.channelName }}';
106-
CHANGE MASTER TO MASTER_LOG_FILE='{{ nextMasterLogFile }}', MASTER_LOG_POS=0 FOR CHANNEL '{{ slave.channelName }}';
107-
START SLAVE FOR CHANNEL '{{ slave.channelName }}';
108-
</pre>
107+
{{ "STOP SLAVE FOR CHANNEL '#{ slave.channelName }';" | highlight_sql | raw }}
108+
{{ "CHANGE MASTER TO MASTER_LOG_FILE='#{ nextMasterLogFile }', MASTER_LOG_POS=0 FOR CHANNEL '#{ slave.channelName }';" | highlight_sql | raw }}
109+
{{ "START SLAVE FOR CHANNEL '#{ slave.channelName }';" | highlight_sql | raw }}
109110
</div>
110111
<div class="modal-footer">
111112
<button class="btn btn-outline btn-warning" type="button" data-dismiss="modal">{{ 'action.close' | trans | capitalize }}</button>
@@ -116,6 +117,33 @@ START SLAVE FOR CHANNEL '{{ slave.channelName }}';
116117
</div>
117118
</div>
118119
</div>
120+
<div class="modal fade" id="skip-transaction-modal-{{ slave.channelName }}" tabindex="-1" role="dialog" aria-hidden="true">
121+
<div class="modal-dialog modal-lg" role="document">
122+
<div class="modal-content">
123+
<div class="modal-header">
124+
<h5 class="modal-title">La requête suivante va être executée</h5>
125+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
126+
<span aria-hidden="true">&times;</span>
127+
</button>
128+
</div>
129+
<div class="modal-body">
130+
{{ "STOP SLAVE FOR CHANNEL '#{ slave.channelName }';" | highlight_sql | raw }}
131+
{{ "SELECT @gtid := (SELECT LAST_SEEN_TRANSACTION FROM performance_schema.replication_applier_status_by_worker WHERE CHANNEL_NAME='#{ slave.channelName }');" | format_sql | raw }}
132+
{{ "SET @@SESSION.GTID_NEXT=@gtid;" | highlight_sql | raw }}
133+
{{ "BEGIN;
134+
COMMIT;" | highlight_sql | raw }}
135+
{{ "SET @@SESSION.GTID_NEXT='AUTOMATIC';" | highlight_sql | raw }}
136+
{{ "START SLAVE FOR CHANNEL '#{ slave.channelName }';" | highlight_sql | raw }}
137+
</div>
138+
<div class="modal-footer">
139+
<button class="btn btn-outline btn-warning" type="button" data-dismiss="modal">{{ 'action.close' | trans | capitalize }}</button>
140+
<button type="button" onclick="skipTransaction({{ slave.channelName }})" class="btn btn-outline btn-success btn-skip-transaction" data-loading-text="LOADING..." id="btn-skip-transaction-{{ slave.channelName }}">
141+
<i class="fa fa-check"></i> {{ "action.validate" | trans | capitalize }}
142+
</button>
143+
</div>
144+
</div>
145+
</div>
146+
</div>
119147
{% endfor %}
120148
</ul>
121149
{% else %}

templates/server/view.html.twig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,31 @@
320320
});
321321
}
322322
323+
function skipTransaction(channel) {
324+
$('#btn-skip-transaction-' + channel).html('<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>In execution...').attr('disabled', true);
325+
326+
let url = "{{ path("server_slave_channel_skip_transaction_ajax", { 'server': server.name, 'channel': 'channel_name' }) }}";
327+
url = url.replace("channel_name", channel);
328+
$.ajax({
329+
type: 'post',
330+
url: url,
331+
success: function (data) {
332+
const $modal = $('#skip-transaction-modal-' + channel);
333+
$modal.modal('hide');
334+
$modal.on('hidden.bs.modal', function () {
335+
refreshSlavesStatus();
336+
refreshProcessList();
337+
$.notify(data.message, "success");
338+
})
339+
},
340+
error: function () {
341+
const $modal = $('#skip-transaction-modal-' + channel);
342+
$modal.modal('hide');
343+
$.notify("{{ 'message.skipTransaction.error' | trans | capitalize }}", "error");
344+
},
345+
});
346+
}
347+
323348
function copyChannelStatusToClipboard(channel) {
324349
navigator.clipboard.writeText($('#slave-status-modal-' + channel).find('.modal-body').text());
325350
$.notify("{{ 'message.copyToClipboard.success' | trans | capitalize }}", "success");

translations/messages.en.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ message:
9090
stopSlaves:
9191
success: Slaves successfully stopped
9292
error: Error while stopping slaves
93+
skipTransaction:
94+
error: Error while skipping transaction
9395
switchToNextMasterLogFile:
9496
success: Master Log File successfully switched to next file
9597
error: Error while switching to next Master Log File

0 commit comments

Comments
 (0)