Compare commits
2 Commits
master
...
no-lock-in
Author | SHA1 | Date | |
---|---|---|---|
|
97da64be8f | ||
|
19e00e5ddc |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -11,7 +11,7 @@ Related issue: https://github.com/github/gh-ost/issues/0123456789
|
||||
|
||||
### Description
|
||||
|
||||
This PR [briefly explain what it does]
|
||||
This PR [briefly explain what is does]
|
||||
|
||||
> In case this PR introduced Go code changes:
|
||||
|
||||
|
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@ -1,25 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Build
|
||||
run: script/cibuild
|
||||
|
||||
- name: Upload gh-ost binary artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: gh-ost
|
||||
path: bin/gh-ost
|
25
.github/workflows/codeql.yml
vendored
25
.github/workflows/codeql.yml
vendored
@ -1,25 +0,0 @@
|
||||
name: "CodeQL analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest # windows-latest and ubuntu-latest are supported. macos-latest is not supported at this time.
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
23
.github/workflows/golangci-lint.yml
vendored
23
.github/workflows/golangci-lint.yml
vendored
@ -1,23 +0,0 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.17
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.46.2
|
24
.github/workflows/replica-tests.yml
vendored
24
.github/workflows/replica-tests.yml
vendored
@ -1,24 +0,0 @@
|
||||
name: migration tests
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
version: [mysql-5.7.25,mysql-8.0.16,PerconaServer-8.0.21]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: migration tests
|
||||
env:
|
||||
TEST_MYSQL_VERSION: ${{ matrix.version }}
|
||||
run: script/cibuild-gh-ost-replica-tests
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,3 @@
|
||||
/bin/
|
||||
/libexec/
|
||||
/.vendor/
|
||||
.idea/
|
||||
|
@ -1,30 +0,0 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
linters:
|
||||
disable:
|
||||
- errcheck
|
||||
enable:
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- execinquery
|
||||
- gofmt
|
||||
- ifshort
|
||||
- misspell
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- wastedassign
|
||||
- whitespace
|
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@ -0,0 +1,20 @@
|
||||
# http://docs.travis-ci.com/user/languages/go/
|
||||
language: go
|
||||
|
||||
go: 1.9
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
env:
|
||||
- MYSQL_USER=root
|
||||
|
||||
before_install:
|
||||
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
|
||||
|
||||
install: true
|
||||
|
||||
script: script/cibuild
|
||||
|
||||
notifications:
|
||||
email: false
|
@ -1,20 +0,0 @@
|
||||
FROM golang:1.17
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y ruby ruby-dev rubygems build-essential
|
||||
RUN gem install --no-ri --no-rdoc fpm
|
||||
ENV GOPATH=/tmp/go
|
||||
|
||||
RUN apt-get install -y curl
|
||||
RUN apt-get install -y rsync
|
||||
RUN apt-get install -y gcc
|
||||
RUN apt-get install -y g++
|
||||
RUN apt-get install -y bash
|
||||
RUN apt-get install -y git
|
||||
RUN apt-get install -y tar
|
||||
RUN apt-get install -y rpm
|
||||
|
||||
RUN mkdir -p $GOPATH/src/github.com/github/gh-ost
|
||||
WORKDIR $GOPATH/src/github.com/github/gh-ost
|
||||
COPY . .
|
||||
RUN bash build.sh
|
@ -1,11 +0,0 @@
|
||||
FROM golang:1.17
|
||||
LABEL maintainer="github@github.com"
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y lsb-release
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . /go/src/github.com/github/gh-ost
|
||||
WORKDIR /go/src/github.com/github/gh-ost
|
||||
|
||||
CMD ["script/test"]
|
@ -1,6 +1,6 @@
|
||||
# gh-ost
|
||||
|
||||
[![ci](https://github.com/github/gh-ost/actions/workflows/ci.yml/badge.svg)](https://github.com/github/gh-ost/actions/workflows/ci.yml) [![replica-tests](https://github.com/github/gh-ost/actions/workflows/replica-tests.yml/badge.svg)](https://github.com/github/gh-ost/actions/workflows/replica-tests.yml) [![downloads](https://img.shields.io/github/downloads/github/gh-ost/total.svg)](https://github.com/github/gh-ost/releases) [![release](https://img.shields.io/github/release/github/gh-ost.svg)](https://github.com/github/gh-ost/releases)
|
||||
[![build status](https://travis-ci.org/github/gh-ost.svg)](https://travis-ci.org/github/gh-ost) [![downloads](https://img.shields.io/github/downloads/github/gh-ost/total.svg)](https://github.com/github/gh-ost/releases) [![release](https://img.shields.io/github/release/github/gh-ost.svg)](https://github.com/github/gh-ost/releases)
|
||||
|
||||
#### GitHub's online schema migration for MySQL <img src="doc/images/gh-ost-logo-light-160.png" align="right">
|
||||
|
||||
@ -65,7 +65,6 @@ Also see:
|
||||
- [the fine print](doc/the-fine-print.md)
|
||||
- [Community questions](https://github.com/github/gh-ost/issues?q=label%3Aquestion)
|
||||
- [Using `gh-ost` on AWS RDS](doc/rds.md)
|
||||
- [Using `gh-ost` on Azure Database for MySQL](doc/azure.md)
|
||||
|
||||
## What's in a name?
|
||||
|
||||
@ -95,7 +94,7 @@ Please see [Coding gh-ost](doc/coding-ghost.md) for a guide to getting started d
|
||||
|
||||
[Download latest release here](https://github.com/github/gh-ost/releases/latest)
|
||||
|
||||
`gh-ost` is a Go project; it is built with Go `1.15` and above. To build on your own, use either:
|
||||
`gh-ost` is a Go project; it is built with Go `1.8` (though `1.7` should work as well). To build on your own, use either:
|
||||
- [script/build](https://github.com/github/gh-ost/blob/master/script/build) - this is the same build script used by CI hence the authoritative; artifact is `./bin/gh-ost` binary.
|
||||
- [build.sh](https://github.com/github/gh-ost/blob/master/build.sh) for building `tar.gz` artifacts in `/tmp/gh-ost`
|
||||
|
||||
@ -108,6 +107,3 @@ Generally speaking, `master` branch is stable, but only [releases](https://githu
|
||||
- [@ggunson](https://github.com/ggunson)
|
||||
- [@tomkrouper](https://github.com/tomkrouper)
|
||||
- [@shlomi-noach](https://github.com/shlomi-noach)
|
||||
- [@jessbreckenridge](https://github.com/jessbreckenridge)
|
||||
- [@gtowey](https://github.com/gtowey)
|
||||
- [@timvaillancourt](https://github.com/timvaillancourt)
|
||||
|
@ -1 +1 @@
|
||||
1.1.2
|
||||
1.0.44
|
||||
|
91
build.sh
91
build.sh
@ -2,77 +2,40 @@
|
||||
#
|
||||
#
|
||||
|
||||
RELEASE_VERSION=
|
||||
buildpath=
|
||||
|
||||
function setuptree() {
|
||||
b=$( mktemp -d $buildpath/gh-ostXXXXXX ) || return 1
|
||||
mkdir -p $b/gh-ost
|
||||
mkdir -p $b/gh-ost/usr/bin
|
||||
echo $b
|
||||
}
|
||||
RELEASE_VERSION=$(cat RELEASE_VERSION)
|
||||
|
||||
function build {
|
||||
osname=$1
|
||||
osshort=$2
|
||||
GOOS=$3
|
||||
GOARCH=$4
|
||||
osname=$1
|
||||
osshort=$2
|
||||
GOOS=$3
|
||||
GOARCH=$4
|
||||
|
||||
if ! go version | egrep -q 'go1\.(1[5-9]|[2-9][0-9]{1})' ; then
|
||||
echo "go version must be 1.15 or above"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building ${osname}-${GOARCH} binary"
|
||||
export GOOS
|
||||
export GOARCH
|
||||
go build -ldflags "$ldflags" -o $buildpath/$target go/cmd/gh-ost/main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed for ${osname} ${GOARCH}."
|
||||
if [[ $(go version | egrep "go1[.][012345678]") ]]; then
|
||||
echo "go version is too low. Must use 1.9 or above"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
(cd $buildpath && tar cfz ./gh-ost-binary-${osshort}-${GOARCH}-${timestamp}.tar.gz $target)
|
||||
echo "Building ${osname} binary"
|
||||
export GOOS
|
||||
export GOARCH
|
||||
go build -ldflags "$ldflags" -o $buildpath/$target go/cmd/gh-ost/main.go
|
||||
|
||||
# build RPM and deb for Linux, x86-64 only
|
||||
if [ "$GOOS" == "linux" ] && [ "$GOARCH" == "amd64" ] ; then
|
||||
echo "Creating Distro full packages"
|
||||
builddir=$(setuptree)
|
||||
cp $buildpath/$target $builddir/gh-ost/usr/bin
|
||||
cd $buildpath
|
||||
fpm -v "${RELEASE_VERSION}" --epoch 1 -f -s dir -n gh-ost -m 'GitHub' --description "GitHub's Online Schema Migrations for MySQL " --url "https://github.com/github/gh-ost" --vendor "GitHub" --license "Apache 2.0" -C $builddir/gh-ost --prefix=/ -t rpm --rpm-rpmbuild-define "_build_id_links none" --rpm-os linux .
|
||||
fpm -v "${RELEASE_VERSION}" --epoch 1 -f -s dir -n gh-ost -m 'GitHub' --description "GitHub's Online Schema Migrations for MySQL " --url "https://github.com/github/gh-ost" --vendor "GitHub" --license "Apache 2.0" -C $builddir/gh-ost --prefix=/ -t deb --deb-no-default-config-files .
|
||||
cd -
|
||||
fi
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed for ${osname}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd $buildpath && tar cfz ./gh-ost-binary-${osshort}-${timestamp}.tar.gz $target)
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ -z "${RELEASE_VERSION}" ] ; then
|
||||
RELEASE_VERSION=$(git describe --abbrev=0 --tags | tr -d 'v')
|
||||
fi
|
||||
if [ -z "${RELEASE_VERSION}" ] ; then
|
||||
RELEASE_VERSION=$(cat RELEASE_VERSION)
|
||||
fi
|
||||
buildpath=/tmp/gh-ost
|
||||
target=gh-ost
|
||||
timestamp=$(date "+%Y%m%d%H%M%S")
|
||||
ldflags="-X main.AppVersion=${RELEASE_VERSION}"
|
||||
|
||||
mkdir -p ${buildpath}
|
||||
build macOS osx darwin amd64
|
||||
build GNU/Linux linux linux amd64
|
||||
|
||||
buildpath=/tmp/gh-ost-release
|
||||
target=gh-ost
|
||||
timestamp=$(date "+%Y%m%d%H%M%S")
|
||||
ldflags="-X main.AppVersion=${RELEASE_VERSION}"
|
||||
|
||||
mkdir -p ${buildpath}
|
||||
rm -rf ${buildpath:?}/*
|
||||
build GNU/Linux linux linux amd64
|
||||
build GNU/Linux linux linux arm64
|
||||
build macOS osx darwin amd64
|
||||
build macOS osx darwin arm64
|
||||
|
||||
echo "Binaries found in:"
|
||||
find $buildpath/gh-ost* -type f -maxdepth 1
|
||||
|
||||
echo "Checksums:"
|
||||
(cd $buildpath && shasum -a256 gh-ost* 2>/dev/null)
|
||||
}
|
||||
|
||||
main "$@"
|
||||
echo "Binaries found in:"
|
||||
ls -1 $buildpath/gh-ost-binary*${timestamp}.tar.gz
|
||||
|
26
doc/azure.md
26
doc/azure.md
@ -1,26 +0,0 @@
|
||||
`gh-ost` has been updated to work with Azure Database for MySQL however due to GitHub does not use it, this documentation is community driven so if you find a bug please [open an issue][new_issue]!
|
||||
|
||||
# Azure Database for MySQL
|
||||
|
||||
## Limitations
|
||||
|
||||
- `gh-ost` runs should be setup use [`--assume-rbr`][assume_rbr_docs] and use `binlog_row_image=FULL`.
|
||||
- Azure Database for MySQL does not use same user name suffix for master and replica, so master host, user and password need to be pointed out.
|
||||
|
||||
## Step
|
||||
1. Change the replica server's `binlog_row_image` from `MINIMAL` to `FULL`. See [guide](https://docs.microsoft.com/en-us/azure/mysql/howto-server-parameters) on Azure document.
|
||||
2. Use your `gh-ost` always with additional 5 parameter
|
||||
```{bash}
|
||||
gh-ost \
|
||||
--azure \
|
||||
--assume-master-host=master-server-dns-name \
|
||||
--master-user="master-user-name" \
|
||||
--master-password="master-password" \
|
||||
--assume-rbr \
|
||||
[-- other paramters you need]
|
||||
```
|
||||
|
||||
|
||||
[new_issue]: https://github.com/github/gh-ost/issues/new
|
||||
[assume_rbr_docs]: https://github.com/github/gh-ost/blob/master/doc/command-line-flags.md#assume-rbr
|
||||
[migrate_test_on_replica_docs]: https://github.com/github/gh-ost/blob/master/doc/cheatsheet.md#c-migratetest-on-replica
|
@ -5,7 +5,7 @@
|
||||
Getting started with gh-ost development is simple!
|
||||
|
||||
- First obtain the repository with `git clone` or `go get`.
|
||||
- From inside of the repository run `script/cibuild`.
|
||||
- From inside of the repository run `script/cibuild`
|
||||
- This will bootstrap the environment if needed, format the code, build the code, and then run the unit test.
|
||||
|
||||
## CI build workflow
|
||||
@ -14,12 +14,6 @@ Getting started with gh-ost development is simple!
|
||||
|
||||
If additional steps are needed, please add them into this workflow so that the workflow remains simple.
|
||||
|
||||
## `golang-ci` linter
|
||||
|
||||
To enfore best-practices, Pull Requests are automatically linted by [`golang-ci`](https://golangci-lint.run/). The linter config is located at [`.golangci.yml`](https://github.com/github/gh-ost/blob/master/.golangci.yml) and the `golangci-lint` GitHub Action is located at [`.github/workflows/golangci-lint.yml`](https://github.com/github/gh-ost/blob/master/.github/workflows/golangci-lint.yml).
|
||||
|
||||
To run the `golang-ci` linters locally _(recommended before push)_, use `script/lint`.
|
||||
|
||||
## Notes:
|
||||
|
||||
Currently, `script/ensure-go-installed` will install `go` for Mac OS X and Linux. We welcome PR's to add other platforms.
|
||||
|
@ -2,18 +2,6 @@
|
||||
|
||||
A more in-depth discussion of various `gh-ost` command line flags: implementation, implication, use cases.
|
||||
|
||||
### aliyun-rds
|
||||
|
||||
Add this flag when executing on Aliyun RDS.
|
||||
|
||||
### allow-zero-in-date
|
||||
|
||||
Allows the user to make schema changes that include a zero date or zero in date (e.g. adding a `datetime default '0000-00-00 00:00:00'` column), even if global `sql_mode` on MySQL has `NO_ZERO_IN_DATE,NO_ZERO_DATE`.
|
||||
|
||||
### azure
|
||||
|
||||
Add this flag when executing on Azure Database for MySQL.
|
||||
|
||||
### allow-master-master
|
||||
|
||||
See [`--assume-master-host`](#assume-master-host).
|
||||
@ -26,7 +14,7 @@ If, for some reason, you do not wish `gh-ost` to connect to a replica, you may c
|
||||
|
||||
### approve-renamed-columns
|
||||
|
||||
When your migration issues a column rename (`change column old_name new_name ...`) `gh-ost` analyzes the statement to try and associate the old column name with new column name. Otherwise, the new structure may also look like some column was dropped and another was added.
|
||||
When your migration issues a column rename (`change column old_name new_name ...`) `gh-ost` analyzes the statement to try an associate the old column name with new column name. Otherwise the new structure may also look like some column was dropped and another was added.
|
||||
|
||||
`gh-ost` will print out what it thinks the _rename_ implied, but will not issue the migration unless you provide with `--approve-renamed-columns`.
|
||||
|
||||
@ -36,7 +24,7 @@ If you think `gh-ost` is mistaken and that there's actually no _rename_ involved
|
||||
|
||||
`gh-ost` infers the identity of the master server by crawling up the replication topology. You may explicitly tell `gh-ost` the identity of the master host via `--assume-master-host=the.master.com`. This is useful in:
|
||||
|
||||
- _master-master_ topologies (together with [`--allow-master-master`](#allow-master-master)), where `gh-ost` can arbitrarily pick one of the co-masters, and you prefer that it picks a specific one
|
||||
- _master-master_ topologies (together with [`--allow-master-master`](#allow-master-master)), where `gh-ost` can arbitrarily pick one of the co-masters and you prefer that it picks a specific one
|
||||
- _tungsten replicator_ topologies (together with [`--tungsten`](#tungsten)), where `gh-ost` is unable to crawl and detect the master
|
||||
|
||||
### assume-rbr
|
||||
@ -45,25 +33,6 @@ If you happen to _know_ your servers use RBR (Row Based Replication, i.e. `binlo
|
||||
Skipping this step means `gh-ost` would not need the `SUPER` privilege in order to operate.
|
||||
You may want to use this on Amazon RDS.
|
||||
|
||||
### attempt-instant-ddl
|
||||
|
||||
MySQL 8.0 supports "instant DDL" for some operations. If an alter statement can be completed with instant DDL, only a metadata change is required internally. Instant operations include:
|
||||
|
||||
- Adding a column
|
||||
- Dropping a column
|
||||
- Dropping an index
|
||||
- Extending a varchar column
|
||||
- Adding a virtual generated column
|
||||
|
||||
It is not reliable to parse the `ALTER` statement to determine if it is instant or not. This is because the table might be in an older row format, or have some other incompatibility that is difficult to identify.
|
||||
|
||||
`--attempt-instant-ddl` is disabled by default, but the risks of enabling it are relatively minor: `gh-ost` may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions.
|
||||
|
||||
`gh-ost` will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful.
|
||||
|
||||
### binlogsyncer-max-reconnect-attempts
|
||||
`--binlogsyncer-max-reconnect-attempts=0`, the maximum number of attempts to re-establish a broken inspector connection for sync binlog. `0` or `negative number` means infinite retry, default `0`
|
||||
|
||||
### conf
|
||||
|
||||
`--conf=/path/to/my.cnf`: file where credentials are specified. Should be in (or contain) the following format:
|
||||
@ -84,13 +53,7 @@ Comma delimited status-name=threshold, same format as [`--max-load`](#max-load).
|
||||
|
||||
`--critical-load` defines a threshold that, when met, `gh-ost` panics and bails out. The default behavior is to bail out immediately when meeting this threshold.
|
||||
|
||||
This may sometimes lead to migrations bailing out on a very short spike, that, while in itself is impacting production and is worth investigating, isn't reason enough to kill a 10-hour migration.
|
||||
|
||||
### critical-load-hibernate-seconds
|
||||
|
||||
When `--critical-load-hibernate-seconds` is non-zero (e.g. `--critical-load-hibernate-seconds=300`), `critical-load` does not panic and bail out; instead, `gh-ost` goes into hibernation for the specified duration. It will not read/write anything from/to any server during this time. Execution then continues upon waking from hibernation.
|
||||
|
||||
If `critical-load` is met again, `gh-ost` will repeat this cycle, and never panic and bail out.
|
||||
This may sometimes lead to migrations bailing out on a very short spike, that, while in itself is impacting production and is worth investigating, isn't reason enough to kill a 10 hour migration.
|
||||
|
||||
### critical-load-interval-millis
|
||||
|
||||
@ -102,10 +65,6 @@ This is somewhat similar to a Nagios `n`-times test, where `n` in our case is al
|
||||
|
||||
Optional. Default is `safe`. See more discussion in [`cut-over`](cut-over.md)
|
||||
|
||||
### cut-over-lock-timeout-seconds
|
||||
|
||||
Default `3`. Max number of seconds to hold locks on tables while attempting to cut-over (retry attempted when lock exceeds timeout).
|
||||
|
||||
### discard-foreign-keys
|
||||
|
||||
**Danger**: this flag will _silently_ discard any foreign keys existing on your table.
|
||||
@ -127,7 +86,7 @@ Noteworthy is that setting `--dml-batch-size` to higher value _does not_ mean `g
|
||||
|
||||
### exact-rowcount
|
||||
|
||||
A `gh-ost` execution need to copy whatever rows you have in your existing table onto the ghost table. This can and often will be, a large number. Exactly what that number is?
|
||||
A `gh-ost` execution need to copy whatever rows you have in your existing table onto the ghost table. This can, and often be, a large number. Exactly what that number is?
|
||||
`gh-ost` initially estimates the number of rows in your table by issuing an `explain select * from your_table`. This will use statistics on your table and return with a rough estimate. How rough? It might go as low as half or as high as double the actual number of rows in your table. This is the same method as used in [`pt-online-schema-change`](https://www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html).
|
||||
|
||||
`gh-ost` also supports the `--exact-rowcount` flag. When this flag is given, two things happen:
|
||||
@ -144,30 +103,10 @@ While the ongoing estimated number of rows is still heuristic, it's almost exact
|
||||
|
||||
Without this parameter, migration is a _noop_: testing table creation and validity of migration, but not touching data.
|
||||
|
||||
### force-named-cut-over
|
||||
|
||||
If given, a `cut-over` command must name the migrated table, or else ignored.
|
||||
|
||||
### force-named-panic
|
||||
|
||||
If given, a `panic` command must name the migrated table, or else ignored.
|
||||
|
||||
### force-table-names
|
||||
|
||||
Table name prefix to be used on the temporary tables.
|
||||
|
||||
### gcp
|
||||
|
||||
Add this flag when executing on a 1st generation Google Cloud Platform (GCP).
|
||||
|
||||
### heartbeat-interval-millis
|
||||
|
||||
Default 100. See [`subsecond-lag`](subsecond-lag.md) for details.
|
||||
|
||||
### hooks-status-interval
|
||||
|
||||
Defaults to 60 seconds. Configures how often the `gh-ost-on-status` hook is called, see [`hooks`](hooks.md) for full details on how to use hooks.
|
||||
|
||||
### initially-drop-ghost-table
|
||||
|
||||
`gh-ost` maintains two tables while migrating: the _ghost_ table (which is synced from your original table and finally replaces it) and a changelog table, which is used internally for bookkeeping. By default, it panics and aborts if it sees those tables upon startup. Provide `--initially-drop-ghost-table` and `--initially-drop-old-table` to let `gh-ost` know it's OK to drop them beforehand.
|
||||
@ -178,10 +117,6 @@ We think `gh-ost` should not take chances or make assumptions about the user's t
|
||||
|
||||
See [`initially-drop-ghost-table`](#initially-drop-ghost-table)
|
||||
|
||||
### initially-drop-socket-file
|
||||
|
||||
Default False. Should `gh-ost` forcibly delete an existing socket file. Be careful: this might drop the socket file of a running migration!
|
||||
|
||||
### max-lag-millis
|
||||
|
||||
On a replication topology, this is perhaps the most important migration throttling factor: the maximum lag allowed for migration to work. If lag exceeds this value, migration throttles.
|
||||
@ -198,7 +133,7 @@ List of metrics and threshold values; topping the threshold of any will cause th
|
||||
|
||||
### migrate-on-replica
|
||||
|
||||
Typically `gh-ost` is used to migrate tables on a master. If you wish to only perform the migration in full on a replica, connect `gh-ost` to said replica and pass `--migrate-on-replica`. `gh-ost` will briefly connect to the master but otherwise will make no changes on the master. Migration will be fully executed on the replica, while making sure to maintain a small replication lag.
|
||||
Typically `gh-ost` is used to migrate tables on a master. If you wish to only perform the migration in full on a replica, connect `gh-ost` to said replica and pass `--migrate-on-replica`. `gh-ost` will briefly connect to the master but other issue no changes on the master. Migration will be fully executed on the replica, while making sure to maintain a small replication lag.
|
||||
|
||||
### postpone-cut-over-flag-file
|
||||
|
||||
@ -214,76 +149,25 @@ Optionally involve the process ID, for example: `--replica-server-id=$((10000000
|
||||
It's on you to choose a number that does not collide with another `gh-ost` or another running replica.
|
||||
See also: [`concurrent-migrations`](cheatsheet.md#concurrent-migrations) on the cheatsheet.
|
||||
|
||||
### serve-socket-file
|
||||
|
||||
Defaults to an auto-determined and advertised upon startup file. Defines Unix socket file to serve on.
|
||||
### skip-foreign-key-checks
|
||||
|
||||
By default `gh-ost` verifies no foreign keys exist on the migrated table. On servers with large number of tables this check can take a long time. If you're absolutely certain no foreign keys exist (table does not reference other table nor is referenced by other tables) and wish to save the check time, provide with `--skip-foreign-key-checks`.
|
||||
|
||||
### skip-strict-mode
|
||||
|
||||
By default `gh-ost` enforces STRICT_ALL_TABLES sql_mode as a safety measure. In some cases this changes the behaviour of other modes (namely ERROR_FOR_DIVISION_BY_ZERO, NO_ZERO_DATE, and NO_ZERO_IN_DATE) which may lead to errors during migration. Use `--skip-strict-mode` to explicitly tell `gh-ost` not to enforce this. **Danger** This may have some unexpected disastrous side effects.
|
||||
|
||||
### skip-renamed-columns
|
||||
|
||||
See [`approve-renamed-columns`](#approve-renamed-columns)
|
||||
|
||||
### ssl
|
||||
|
||||
By default `gh-ost` does not use ssl/tls connections to the database servers when performing migrations. This flag instructs `gh-ost` to use encrypted connections. If enabled, `gh-ost` will use the system's ca certificate pool for server certificate verification. If a different certificate is needed for server verification, see `--ssl-ca`. If you wish to skip server verification, but still use encrypted connections, use with `--ssl-allow-insecure`.
|
||||
|
||||
### ssl-allow-insecure
|
||||
|
||||
Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but without verifying the validity of the certificate provided by the server during the connection. Requires `--ssl`.
|
||||
|
||||
### ssl-ca
|
||||
|
||||
`--ssl-ca=/path/to/ca-cert.pem`: ca certificate file (in PEM format) to use for server certificate verification. If specified, the default system ca cert pool will not be used for verification, only the ca cert provided here. Requires `--ssl`.
|
||||
|
||||
### ssl-cert
|
||||
|
||||
`--ssl-cert=/path/to/ssl-cert.crt`: SSL public key certificate file (in PEM format).
|
||||
|
||||
### ssl-key
|
||||
|
||||
`--ssl-key=/path/to/ssl-key.key`: SSL private key file (in PEM format).
|
||||
|
||||
### storage-engine
|
||||
Default is `innodb`, and `rocksdb` support is currently experimental. InnoDB and RocksDB are both transactional engines, supporting both shared and exclusive row locks.
|
||||
|
||||
But RocksDB currently lacks a few features support compared to InnoDB:
|
||||
- Gap Locks
|
||||
- Foreign Key
|
||||
- Generated Columns
|
||||
- Spatial
|
||||
- Geometry
|
||||
|
||||
When `--storage-engine=rocksdb`, `gh-ost` will make some changes necessary (e.g. sets isolation level to `READ_COMMITTED`) to support RocksDB.
|
||||
|
||||
### test-on-replica
|
||||
|
||||
Issue the migration on a replica; do not modify data on master. Useful for validating, testing and benchmarking. See [`testing-on-replica`](testing-on-replica.md)
|
||||
|
||||
### test-on-replica-skip-replica-stop
|
||||
|
||||
Default `False`. When `--test-on-replica` is enabled, do not issue commands stop replication (requires `--test-on-replica`).
|
||||
|
||||
### throttle-control-replicas
|
||||
|
||||
Provide a command delimited list of replicas; `gh-ost` will throttle when any of the given replicas lag beyond [`--max-lag-millis`](#max-lag-millis). The list can be queried and updated dynamically via [interactive commands](interactive-commands.md)
|
||||
|
||||
### throttle-http
|
||||
|
||||
Provide an HTTP endpoint; `gh-ost` will issue `HEAD` requests on given URL and throttle whenever response status code is not `200`. The URL can be queried and updated dynamically via [interactive commands](interactive-commands.md). Empty URL disables the HTTP check.
|
||||
|
||||
### throttle-http-interval-millis
|
||||
|
||||
Defaults to 100. Configures the HTTP throttle check interval in milliseconds.
|
||||
|
||||
### throttle-http-timeout-millis
|
||||
|
||||
Defaults to 1000 (1 second). Configures the HTTP throttler check timeout in milliseconds.
|
||||
Provide a HTTP endpoint; `gh-ost` will issue `HEAD` requests on given URL and throttle whenever response status code is not `200`. The URL can be queried and updated dynamically via [interactive commands](interactive-commands.md). Empty URL disables the HTTP check.
|
||||
|
||||
### timestamp-old-table
|
||||
|
||||
|
@ -65,17 +65,10 @@ The following variables are available on all hooks:
|
||||
- `GH_OST_ELAPSED_COPY_SECONDS` - row-copy time (excluding startup, row-count and postpone time)
|
||||
- `GH_OST_ESTIMATED_ROWS` - estimated total rows in table
|
||||
- `GH_OST_COPIED_ROWS` - number of rows copied by `gh-ost`
|
||||
- `GH_OST_INSPECTED_LAG` - lag in seconds (floating point) of inspected server
|
||||
- `GH_OST_HEARTBEAT_LAG` - lag in seconds (floating point) of heartbeat
|
||||
- `GH_OST_PROGRESS` - progress pct ([0..100], floating point) of migration
|
||||
- `GH_OST_ETA_SECONDS` - estimated duration until migration finishes in seconds
|
||||
- `GH_OST_MIGRATED_HOST`
|
||||
- `GH_OST_INSPECTED_HOST`
|
||||
- `GH_OST_EXECUTING_HOST`
|
||||
- `GH_OST_HOOKS_HINT` - copy of `--hooks-hint` value
|
||||
- `GH_OST_HOOKS_HINT_OWNER` - copy of `--hooks-hint-owner` value
|
||||
- `GH_OST_HOOKS_HINT_TOKEN` - copy of `--hooks-hint-token` value
|
||||
- `GH_OST_DRY_RUN` - whether or not the `gh-ost` run is a dry run
|
||||
|
||||
The following variable are available on particular hooks:
|
||||
|
||||
|
@ -18,8 +18,6 @@ Both interfaces may serve at the same time. Both respond to simple text command,
|
||||
- `status`: returns a detailed status summary of migration progress and configuration
|
||||
- `sup`: returns a brief status summary of migration progress
|
||||
- `coordinates`: returns recent (though not exactly up to date) binary log coordinates of the inspected server
|
||||
- `applier`: returns the hostname of the applier
|
||||
- `inspector`: returns the hostname of the inspector
|
||||
- `chunk-size=<newsize>`: modify the `chunk-size`; applies on next running copy-iteration
|
||||
- `dml-batch-size=<newsize>`: modify the `dml-batch-size`; applies on next applying of binary log events
|
||||
- `max-lag-millis=<max-lag>`: modify the maximum replication lag threshold (milliseconds, minimum value is `100`, i.e. `0.1` second)
|
||||
|
@ -28,9 +28,3 @@ It is therefore unlikely that `gh-ost` will support this behavior.
|
||||
Yes. TL;DR if running all on same replica/master, make sure to provide `--replica-server-id`. [Read more](cheatsheet.md#concurrent-migrations)
|
||||
|
||||
# Why
|
||||
|
||||
### Why Is the "Connect to Replica" mode preferred?
|
||||
|
||||
To avoid placing extra load on the master. `gh-ost` connects as a replication client. Each additional replica adds some load to the master.
|
||||
|
||||
To monitor replication lag from a replica. This makes the replication lag throttle, `--max-lag-millis`, more representative of the lag experienced by other replicas following the master (perhaps N levels deep in a tree of replicas).
|
||||
|
12
doc/rds.md
12
doc/rds.md
@ -1,4 +1,4 @@
|
||||
`gh-ost` has been updated to work with Amazon RDS however due to GitHub not using AWS for databases, this documentation is community driven so if you find a bug please [open an issue][new_issue]!
|
||||
`gh-ost` has been updated to work with Amazon RDS however due to GitHub not relying using AWS for databases, this documentation is community driven so if you find a bug please [open an issue][new_issue]!
|
||||
|
||||
# Amazon RDS
|
||||
|
||||
@ -26,14 +26,6 @@ If you use `pt-table-checksum` as a part of your data integrity checks, you migh
|
||||
This tool requires binlog_format=STATEMENT, but the current binlog_format is set to ROW and an error occurred while attempting to change it. If running MySQL 5.1.29 or newer, setting binlog_format requires the SUPER privilege. You will need to manually set binlog_format to 'STATEMENT' before running this tool.
|
||||
```
|
||||
|
||||
#### Binlog filtering
|
||||
|
||||
In Aurora, the [binlog filtering feature][aws_replication_docs_bin_log_filtering] is enabled by default. This becomes an issue when gh-ost tries to do the cut-over, because gh-ost waits for an entry in the binlog to proceed but this entry will never end up in the binlog because it gets filtered out by the binlog filtering feature.
|
||||
You need to turn this feature off during the migration process.
|
||||
Set the `aurora_enable_repl_bin_log_filtering` parameter to 0 in the Parameter Group for your cluster.
|
||||
When the migration is done, set it back to 1 (default).
|
||||
|
||||
|
||||
#### Preflight checklist
|
||||
|
||||
Before trying to run any `gh-ost` migrations you will want to confirm the following:
|
||||
@ -43,7 +35,6 @@ Before trying to run any `gh-ost` migrations you will want to confirm the follow
|
||||
- [ ] Executing `SHOW SLAVE STATUS\G` on your replica cluster displays the correct master host, binlog position, etc.
|
||||
- [ ] Database backup retention is greater than 1 day to enable binlogs
|
||||
- [ ] You have setup [`hooks`][ghost_hooks] to issue RDS procedures for stopping and starting replication. (see [github/gh-ost#163][ghost_rds_issue_tracking] for examples)
|
||||
- [ ] The parameter `aurora_enable_repl_bin_log_filtering` is set to 0
|
||||
|
||||
[new_issue]: https://github.com/github/gh-ost/issues/new
|
||||
[assume_rbr_docs]: https://github.com/github/gh-ost/blob/master/doc/command-line-flags.md#assume-rbr
|
||||
@ -52,4 +43,3 @@ Before trying to run any `gh-ost` migrations you will want to confirm the follow
|
||||
[percona_toolkit_patch]: https://github.com/jacobbednarz/percona-toolkit/commit/0271ba6a094da446a5e5bb8d99b5c26f1777f2b9
|
||||
[ghost_hooks]: https://github.com/github/gh-ost/blob/master/doc/hooks.md
|
||||
[ghost_rds_issue_tracking]: https://github.com/github/gh-ost/issues/163
|
||||
[aws_replication_docs_bin_log_filtering]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Replication.html#AuroraMySQL.Replication.Performance
|
@ -2,8 +2,6 @@
|
||||
|
||||
### Requirements
|
||||
|
||||
- `gh-ost` currently requires MySQL versions 5.7 and greater.
|
||||
|
||||
- You will need to have one server serving Row Based Replication (RBR) format binary logs. Right now `FULL` row image is supported. `MINIMAL` to be supported in the near future. `gh-ost` prefers to work with replicas. You may [still have your master configured with Statement Based Replication](migrating-with-sbr.md) (SBR).
|
||||
|
||||
- If you are using a replica, the table must have an identical schema between the master and replica.
|
||||
@ -20,16 +18,18 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
|
||||
- Switching your `binlog_format` to `ROW`, in the case where it is _not_ `ROW` and you explicitly specified `--switch-to-rbr`
|
||||
- If your replication is already in RBR (`binlog_format=ROW`) you can specify `--assume-rbr` to avoid the `STOP SLAVE/START SLAVE` operations, hence no need for `SUPER`.
|
||||
|
||||
- `gh-ost` uses the `REPEATABLE_READ` transaction isolation level for all MySQL connections, regardless of the server default.
|
||||
|
||||
- Running `--test-on-replica`: before the cut-over phase, `gh-ost` stops replication so that you can compare the two tables and satisfy that the migration is sound.
|
||||
|
||||
### Limitations
|
||||
|
||||
- Foreign key constraints are not supported. They may be supported in the future, to some extent.
|
||||
- Foreign keys not supported. They may be supported in the future, to some extent.
|
||||
|
||||
- Triggers are not supported. They may be supported in the future.
|
||||
|
||||
- MySQL 5.7 generated columns are not supported. They may be supported in the future.
|
||||
|
||||
- MySQL 5.7 `POINT` column type is not supported.
|
||||
|
||||
- MySQL 5.7 `JSON` columns are supported but not as part of `PRIMARY KEY`
|
||||
|
||||
- The two _before_ & _after_ tables must share a `PRIMARY KEY` or other `UNIQUE KEY`. This key will be used by `gh-ost` to iterate through the table rows when copying. [Read more](shared-key.md)
|
||||
@ -42,18 +42,13 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
|
||||
- It is not allowed to migrate a table where another table exists with same name and different upper/lower case.
|
||||
- For example, you may not migrate `MyTable` if another table called `MYtable` exists in the same schema.
|
||||
|
||||
- Amazon RDS works, but has its own [limitations](rds.md).
|
||||
- Google Cloud SQL works, `--gcp` flag required.
|
||||
- Aliyun RDS works, `--aliyun-rds` flag required.
|
||||
- Azure Database for MySQL works, `--azure` flag required, and have detailed document about it. (azure.md)
|
||||
- Amazon RDS works, but has it's own [limitations](rds.md).
|
||||
- Google Cloud SQL is currently not supported
|
||||
|
||||
- Multisource is not supported when migrating via replica. It _should_ work (but never tested) when connecting directly to master (`--allow-on-master`)
|
||||
|
||||
- Master-master setup is only supported in active-passive setup. Active-active (where table is being written to on both masters concurrently) is unsupported. It may be supported in the future.
|
||||
|
||||
- If you have an `enum` field as part of your migration key (typically the `PRIMARY KEY`), migration performance will be degraded and potentially bad. [Read more](https://github.com/github/gh-ost/pull/277#issuecomment-254811520)
|
||||
- If you have en `enum` field as part of your migration key (typically the `PRIMARY KEY`), migration performance will be degraded and potentially bad. [Read more](https://github.com/github/gh-ost/pull/277#issuecomment-254811520)
|
||||
|
||||
- Migrating a `FEDERATED` table is unsupported and is irrelevant to the problem `gh-ost` tackles.
|
||||
|
||||
- [Encrypted binary logs](https://www.percona.com/blog/2018/03/08/binlog-encryption-percona-server-mysql/) are not supported.
|
||||
- `ALTER TABLE ... RENAME TO some_other_name` is not supported (and you shouldn't use `gh-ost` for such a trivial operation).
|
||||
|
@ -1,12 +1,12 @@
|
||||
# Shared key
|
||||
|
||||
gh-ost requires for every migration that both the _before_ and _after_ versions of the table share the same unique not-null key columns. This page illustrates this rule.
|
||||
A requirement for a migration to run is that the two _before_ and _after_ tables have a shared unique key. This is to elaborate and illustrate on the matter.
|
||||
|
||||
### Introduction
|
||||
|
||||
Consider a simple migration, with a normal table,
|
||||
Consider a classic, simple migration. The table is any normal:
|
||||
|
||||
```sql
|
||||
```
|
||||
CREATE TABLE tbl (
|
||||
id bigint unsigned not null auto_increment,
|
||||
data varchar(255),
|
||||
@ -15,72 +15,54 @@ CREATE TABLE tbl (
|
||||
)
|
||||
```
|
||||
|
||||
and the migration `add column ts timestamp`. The _after_ table version would be:
|
||||
And the migration is a simple `add column ts timestamp`.
|
||||
|
||||
In such migration there is no change in indexes, and in particular no change to any unique key, and specifically no change to the `PRIMARY KEY`. To run this migration, `gh-ost` would iterate the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` by order of `id`, and then apply binlog events onto `_tbl_gho`.
|
||||
|
||||
Applying the binlog events assumes the existence of a shared unique key. For example, an `UPDATE` statement in the binary log translate to a `REPLACE` statement which `gh-ost` applies to the _ghost_ table. Such statement expects to add or replace an existing row based on given row data. In particular, it would _replace_ an existing row if a unique key violation is met.
|
||||
|
||||
So `gh-ost` correlates `tbl` and `_tbl_gho` rows using a unique key. In the above example that would be the `PRIMARY KEY`.
|
||||
|
||||
### Rules
|
||||
|
||||
There must be a shared set of not-null columns for which there is a unique constraint in both the original table and the migration (_ghost_) table.
|
||||
|
||||
### Interpreting the rules
|
||||
|
||||
The same columns must be covered by a unique key in both tables. This doesn't have to be the `PRIMARY KEY`. This doesn't have to be a key of the same name.
|
||||
|
||||
Upon migration, `gh-ost` inspects both the original and _ghost_ table and attempts to find at least one such unique key (or rather, a set of columns) that is shared between the two. Typically this would just be the `PRIMARY KEY`, but sometimes you may change the `PRIMARY KEY` itself, in which case `gh-ost` will look for other options.
|
||||
|
||||
`gh-ost` expects unique keys where no `NULL` values are found, i.e. all columns covered by the unique key are defined as `NOT NULL`. This is implicitly true for `PRIMARY KEY`s. If no such key can be found, `gh-ost` bails out. In the event there is no such key, but you happen to _know_ your columns have no `NULL` values even though they're `NULL`-able, you may take responsibility and pass the `--allow-nullable-unique-key`. The migration will run well as long as no `NULL` values are found in the unique key's columns. Any actual `NULL`s may corrupt the migration.
|
||||
|
||||
### Examples: allowed and not allowed
|
||||
|
||||
```sql
|
||||
CREATE TABLE tbl (
|
||||
id bigint unsigned not null auto_increment,
|
||||
data varchar(255),
|
||||
more_data int,
|
||||
ts timestamp,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
```
|
||||
|
||||
(This is also the definition of the _ghost_ table, except that that table would be called `_tbl_gho`).
|
||||
|
||||
In this migration, the _before_ and _after_ versions contain the same unique not-null key (the PRIMARY KEY). To run this migration, `gh-ost` would iterate through the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` in primary key order, while also applying the binlog event writes from `tbl` onto `_tbl_gho`.
|
||||
|
||||
The applying of the binlog events is what requires the shared unique key. For example, an `UPDATE` statement to `tbl` translates to a `REPLACE` statement which `gh-ost` applies to `_tbl_gho`. A `REPLACE` statement expects to insert or replace an existing row based on its row's values and the table's unique key constraints. In particular, if inserting that row would result in a unique key violation (e.g., a row with that primary key already exists), it would _replace_ that existing row with the new values.
|
||||
|
||||
So `gh-ost` correlates `tbl` and `_tbl_gho` rows one to one using a unique key. In the above example that would be the `PRIMARY KEY`.
|
||||
|
||||
### Interpreting the rule
|
||||
|
||||
The _before_ and _after_ versions of the table share the same unique not-null key, but:
|
||||
- the key doesn't have to be the PRIMARY KEY
|
||||
- the key can have a different name between the _before_ and _after_ versions (e.g., renamed via DROP INDEX and ADD INDEX) so long as it contains the exact same column(s)
|
||||
|
||||
At the start of the migration, `gh-ost` inspects both the original and _ghost_ table it created, and attempts to find at least one such unique key (or rather, a set of columns) that is shared between the two. Typically this would just be the `PRIMARY KEY`, but some tables don't have primary keys, or sometimes it is the primary key that is being modified by the migration. In these cases `gh-ost` will look for other options.
|
||||
|
||||
`gh-ost` expects unique keys where no `NULL` values are found, i.e. all columns contained in the unique key are defined as `NOT NULL`. This is implicitly true for primary keys. If no such key can be found, `gh-ost` bails out.
|
||||
|
||||
If the table contains a unique key with nullable columns, but you know your columns contain no `NULL` values, use the `--allow-nullable-unique-key` option. The migration will run well as long as no `NULL` values are found in the unique key's columns. **Any actual `NULL`s may corrupt the migration.**
|
||||
|
||||
### Examples: Allowed and Not Allowed
|
||||
|
||||
```sql
|
||||
create table some_table (
|
||||
id int not null auto_increment,
|
||||
id int auto_increment,
|
||||
ts timestamp,
|
||||
name varchar(128) not null,
|
||||
owner_id int not null,
|
||||
loc_id int not null,
|
||||
loc_id int,
|
||||
primary key(id),
|
||||
unique key name_uidx(name)
|
||||
)
|
||||
```
|
||||
|
||||
Note the two unique, not-null indexes: the primary key and `name_uidx`.
|
||||
|
||||
Allowed migrations:
|
||||
Following are examples of migrations that are _good to run_:
|
||||
|
||||
- `add column i int`
|
||||
- `add key owner_idx (owner_id)`
|
||||
- `add unique key owner_name_idx (owner_id, name)` - **be careful not to write conflicting rows while this migration runs**
|
||||
- `add key owner_idx(owner_id)`
|
||||
- `add unique key owner_name_idx(owner_id, name)` - though you need to make sure to not write conflicting rows while this migration runs
|
||||
- `drop key name_uidx` - `primary key` is shared between the tables
|
||||
- `drop primary key, add primary key(owner_id, loc_id)` - `name_uidx` is shared between the tables
|
||||
- `change id bigint unsigned not null auto_increment` - the `primary key` changes datatype but not value, and can be used
|
||||
- `drop primary key, drop key name_uidx, add primary key(name), add unique key id_uidx(id)` - swapping the two keys. Either `id` or `name` could be used
|
||||
|
||||
Not allowed:
|
||||
|
||||
- `drop primary key, drop key name_uidx` - the _ghost_ table has no unique key
|
||||
- `drop primary key, drop key name_uidx, create primary key(name, owner_id)` - no shared columns to the unique keys on both tables. Even though `name` exists in the _ghost_ table's `primary key`, it is only part of the key and in itself does not guarantee uniqueness in the _ghost_ table.
|
||||
- `drop primary key, add primary key(owner_id, loc_id)` - `name_uidx` is shared between the tables and is used for migration
|
||||
- `change id bigint unsigned` - the `'primary key` is used. The change of type still makes the `primary key` workable.
|
||||
- `drop primary key, drop key name_uidx, create primary key(name), create unique key id_uidx(id)` - swapping the two keys. `gh-ost` is still happy because `id` is still unique in both tables. So is `name`.
|
||||
|
||||
|
||||
### Workarounds
|
||||
Following are examples of migrations that _cannot run_:
|
||||
|
||||
If you need to change your primary key or only not-null unique index to use different columns, you will want to do it as two separate migrations:
|
||||
1. `ADD UNIQUE KEY temp_pk (temp_pk_column,...)`
|
||||
1. `DROP PRIMARY KEY, DROP KEY temp_pk, ADD PRIMARY KEY (temp_pk_column,...)`
|
||||
- `drop primary key, drop key name_uidx` - no unique key to _ghost_ table, so clearly cannot run
|
||||
- `drop primary key, drop key name_uidx, create primary key(name, owner_id)` - no shared columns to both tables. Even though `name` exists in the _ghost_ table's `primary key`, it is only part of the key and in itself does not guarantee uniqueness in the _ghost_ table.
|
||||
|
||||
Also, you cannot run a migration on a table that doesn't have some form of `unique key` in the first place, such as `some_table (id int, ts timestamp)`
|
||||
|
@ -38,7 +38,7 @@ Note that you may dynamically change both `--max-lag-millis` and the `throttle-c
|
||||
|
||||
`--max-load='Threads_running=100,Threads_connected=500'`
|
||||
|
||||
Metrics must be valid, numeric [status variables](https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html)
|
||||
Metrics must be valid, numeric [status variables](http://dev.mysql.com/doc/refman/5.6/en/server-status-variables.html)
|
||||
|
||||
#### Throttle query
|
||||
|
||||
@ -46,14 +46,6 @@ Note that you may dynamically change both `--max-lag-millis` and the `throttle-c
|
||||
|
||||
An example query could be: `--throttle-query="select hour(now()) between 8 and 17"` which implies throttling auto-starts `8:00am` and migration auto-resumes at `18:00pm`.
|
||||
|
||||
#### HTTP Throttle
|
||||
|
||||
The `--throttle-http` flag allows for throttling via HTTP. Every 100ms `gh-ost` issues a `HEAD` request to the provided URL. If the response status code is not `200` throttling will kick in until a `200` response status code is returned.
|
||||
|
||||
If no URL is provided or the URL provided doesn't contain the scheme then the HTTP check will be disabled. For example `--throttle-http="http://1.2.3.4:6789/throttle"` will enable the HTTP check/throttling, but `--throttle-http="1.2.3.4:6789/throttle"` will not.
|
||||
|
||||
The URL can be queried and updated dynamically via [interactive interface](interactive-commands.md).
|
||||
|
||||
#### Manual control
|
||||
|
||||
In addition to the above, you are able to take control and throttle the operation any time you like.
|
||||
@ -97,7 +89,7 @@ Copy: 0/2915 0.0%; Applied: 0; Backlog: 0/100; Elapsed: 42s(copy), 42s(total); s
|
||||
|
||||
Throttling time is limited by the availability of the binary logs. When throttling begins, `gh-ost` suspends reading the binary logs, and expects to resume reading from same binary log where it paused.
|
||||
|
||||
Your availability of binary logs is typically determined by the [expire_logs_days](https://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html#sysvar_expire_logs_days) variable. If you have `expire_logs_days = 10` (or check `select @@global.expire_logs_days`), then you should be able to throttle for up to `10` days.
|
||||
Your availability of binary logs is typically determined by the [expire_logs_days](https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_expire_logs_days) variable. If you have `expire_logs_days = 10` (or check `select @@global.expire_logs_days`), then you should be able to throttle for up to `10` days.
|
||||
|
||||
Having said that, throttling for so long is far fetching, in that the `gh-ost` process itself must be kept alive during that time; and the amount of binary logs to process once it resumes will potentially take days to replay.
|
||||
|
||||
|
@ -112,7 +112,7 @@ It is also interesting to observe that `gh-ost` is the only application writing
|
||||
|
||||
When `gh-ost` pauses (throttles), it issues no writes on the ghost table. Because there are no triggers, write workload is decoupled from the `gh-ost` write workload. And because we're using an asynchronous approach, the algorithm already handles a time difference between a master write time and the ghost apply time. A difference of a few microseconds is no different from a difference of minutes or hours.
|
||||
|
||||
When `gh-ost` [throttles](throttle.md), either by replication lag, `max-load` setting or an explicit [interactive user command](interactive-commands.md), the master is back to normal. It sees no more writes on the ghost table.
|
||||
When `gh-ost` [throttles](throttle.md), either by replication lag, `max-load` setting or and explicit [interactive user command](interactive-commands.md), the master is back to normal. It sees no more writes on the ghost table.
|
||||
An exception is the ongoing heartbeat writes onto the changelog table, which we consider to be negligible.
|
||||
|
||||
#### Testability
|
||||
|
@ -7,7 +7,7 @@ Existing MySQL schema migration tools:
|
||||
- [LHM](https://github.com/soundcloud/lhm)
|
||||
- [oak-online-alter-table](https://github.com/shlomi-noach/openarkkit)
|
||||
|
||||
are all using [triggers](https://dev.mysql.com/doc/refman/5.7/en/triggers.html) to propagate live changes on your table onto a ghost/shadow table that is slowly being synchronized. The tools not all work the same: while most use a synchronous approach (all changes applied on the ghost table), the Facebook tool uses an asynchronous approach (changes are appended to a changelog table, later reviewed and applied on ghost table).
|
||||
are all using [triggers](http://dev.mysql.com/doc/refman/5.6/en/triggers.html) to propagate live changes on your table onto a ghost/shadow table that is slowly being synchronized. The tools not all work the same: while most use a synchronous approach (all changes applied on the ghost table), the Facebook tool uses an asynchronous approach (changes are appended to a changelog table, later reviewed and applied on ghost table).
|
||||
|
||||
Use of triggers simplifies a lot of the flow in doing a live table migration, but also poses some limitations or difficulties. Here are reasons why we choose to [design a triggerless solution](triggerless-design.md) to schema migrations.
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
version: "3.5"
|
||||
services:
|
||||
app:
|
||||
image: app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.test
|
27
go.mod
27
go.mod
@ -1,27 +0,0 @@
|
||||
module github.com/github/gh-ost
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/go-ini/ini v1.62.0
|
||||
github.com/go-mysql-org/go-mysql v1.3.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/openark/golib v0.0.0-20210531070646-355f37940af8
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7
|
||||
golang.org/x/text v0.3.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
)
|
136
go.sum
136
go.sum
@ -1,136 +0,0 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM=
|
||||
github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
|
||||
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
|
||||
github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-mysql-org/go-mysql v1.3.0 h1:lpNqkwdPzIrYSZGdqt8HIgAXZaK6VxBNfr8f7Z4FgGg=
|
||||
github.com/go-mysql-org/go-mysql v1.3.0/go.mod h1:3lFZKf7l95Qo70+3XB2WpiSf9wu2s3na3geLMaIIrqQ=
|
||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/openark/golib v0.0.0-20210531070646-355f37940af8 h1:9ciIHNuyFqRWi9NpMNw9sVLB6z1ItpP5ZhTY9Q1xVu4=
|
||||
github.com/openark/golib v0.0.0-20210531070646-355f37940af8/go.mod h1:1jj8x1eDVZxgc/Z4VyamX4qTbAdHPUQA6NeVtCd8Sl8=
|
||||
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg=
|
||||
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
|
||||
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pingcap/errors v0.11.5-0.20201029093017-5a7df2af2ac7/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI=
|
||||
github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 h1:LllgC9eGfqzkfubMgjKIDyZYaa609nNWAyNZtpy2B3M=
|
||||
github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI=
|
||||
github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
|
||||
github.com/pingcap/log v0.0.0-20210317133921-96f4fcab92a4/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
|
||||
github.com/pingcap/parser v0.0.0-20210415081931-48e7f467fd74/go.mod h1:xZC8I7bug4GJ5KtHhgAikjTfU4kBv1Sbo3Pf1MZ6lVw=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
|
||||
github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 h1:oI+RNwuC9jF2g2lP0u0cVEEZrc/AYBCuFdvwrLWM/6Q=
|
||||
github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07/go.mod h1:yFdBgwXP24JziuRl2NMUahT7nGLNOKi1SIiFxMttVD4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM=
|
||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -7,7 +7,6 @@ package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -15,13 +14,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/satori/go.uuid"
|
||||
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
"github.com/openark/golib/log"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
"gopkg.in/gcfg.v1"
|
||||
gcfgscanner "gopkg.in/gcfg.v1/scanner"
|
||||
)
|
||||
|
||||
// RowsEstimateMethod is the type of row number estimation
|
||||
@ -29,29 +28,28 @@ type RowsEstimateMethod string
|
||||
|
||||
const (
|
||||
TableStatusRowsEstimate RowsEstimateMethod = "TableStatusRowsEstimate"
|
||||
ExplainRowsEstimate RowsEstimateMethod = "ExplainRowsEstimate"
|
||||
CountRowsEstimate RowsEstimateMethod = "CountRowsEstimate"
|
||||
ExplainRowsEstimate = "ExplainRowsEstimate"
|
||||
CountRowsEstimate = "CountRowsEstimate"
|
||||
)
|
||||
|
||||
type CutOver int
|
||||
|
||||
const (
|
||||
CutOverAtomic CutOver = iota
|
||||
CutOverTwoStep
|
||||
CutOverAtomic CutOver = iota
|
||||
CutOverTwoStep = iota
|
||||
)
|
||||
|
||||
type ThrottleReasonHint string
|
||||
|
||||
const (
|
||||
NoThrottleReasonHint ThrottleReasonHint = "NoThrottleReasonHint"
|
||||
UserCommandThrottleReasonHint ThrottleReasonHint = "UserCommandThrottleReasonHint"
|
||||
LeavingHibernationThrottleReasonHint ThrottleReasonHint = "LeavingHibernationThrottleReasonHint"
|
||||
UserCommandThrottleReasonHint = "UserCommandThrottleReasonHint"
|
||||
LeavingHibernationThrottleReasonHint = "LeavingHibernationThrottleReasonHint"
|
||||
)
|
||||
|
||||
const (
|
||||
HTTPStatusOK = 200
|
||||
MaxEventsBatchSize = 1000
|
||||
ETAUnknown = math.MinInt64
|
||||
)
|
||||
|
||||
var (
|
||||
@ -77,13 +75,10 @@ func NewThrottleCheckResult(throttle bool, reason string, reasonHint ThrottleRea
|
||||
type MigrationContext struct {
|
||||
Uuid string
|
||||
|
||||
DatabaseName string
|
||||
OriginalTableName string
|
||||
AlterStatement string
|
||||
AlterStatementOptions string // anything following the 'ALTER TABLE [schema.]table' from AlterStatement
|
||||
DatabaseName string
|
||||
OriginalTableName string
|
||||
AlterStatement string
|
||||
|
||||
countMutex sync.Mutex
|
||||
countTableRowsCancelFunc func()
|
||||
CountTableRows bool
|
||||
ConcurrentCountTableRows bool
|
||||
AllowedRunningOnMaster bool
|
||||
@ -91,28 +86,17 @@ type MigrationContext struct {
|
||||
SwitchToRowBinlogFormat bool
|
||||
AssumeRBR bool
|
||||
SkipForeignKeyChecks bool
|
||||
SkipStrictMode bool
|
||||
AllowZeroInDate bool
|
||||
NullableUniqueKeyAllowed bool
|
||||
ApproveRenamedColumns bool
|
||||
SkipRenamedColumns bool
|
||||
IsTungsten bool
|
||||
DiscardForeignKeys bool
|
||||
AliyunRDS bool
|
||||
GoogleCloudPlatform bool
|
||||
AzureMySQL bool
|
||||
AttemptInstantDDL bool
|
||||
|
||||
config ContextConfig
|
||||
configMutex *sync.Mutex
|
||||
ConfigFile string
|
||||
CliUser string
|
||||
CliPassword string
|
||||
UseTLS bool
|
||||
TLSAllowInsecure bool
|
||||
TLSCACertificate string
|
||||
TLSCertificate string
|
||||
TLSKey string
|
||||
CliMasterUser string
|
||||
CliMasterPassword string
|
||||
|
||||
@ -126,7 +110,6 @@ type MigrationContext struct {
|
||||
ThrottleAdditionalFlagFile string
|
||||
throttleQuery string
|
||||
throttleHTTP string
|
||||
IgnoreHTTPErrors bool
|
||||
ThrottleCommandedByUser int64
|
||||
HibernateUntil int64
|
||||
maxLoad LoadMap
|
||||
@ -135,16 +118,10 @@ type MigrationContext struct {
|
||||
CriticalLoadHibernateSeconds int64
|
||||
PostponeCutOverFlagFile string
|
||||
CutOverLockTimeoutSeconds int64
|
||||
CutOverExponentialBackoff bool
|
||||
ExponentialBackoffMaxInterval int64
|
||||
ForceNamedCutOverCommand bool
|
||||
ForceNamedPanicCommand bool
|
||||
PanicFlagFile string
|
||||
HooksPath string
|
||||
HooksHintMessage string
|
||||
HooksHintOwner string
|
||||
HooksHintToken string
|
||||
HooksStatusIntervalSec int64
|
||||
|
||||
DropServeSocket bool
|
||||
ServeSocketFile string
|
||||
@ -183,14 +160,8 @@ type MigrationContext struct {
|
||||
RenameTablesEndTime time.Time
|
||||
pointOfInterestTime time.Time
|
||||
pointOfInterestTimeMutex *sync.Mutex
|
||||
lastHeartbeatOnChangelogTime time.Time
|
||||
lastHeartbeatOnChangelogMutex *sync.Mutex
|
||||
CurrentLag int64
|
||||
currentProgress uint64
|
||||
etaNanoseonds int64
|
||||
ThrottleHTTPIntervalMillis int64
|
||||
ThrottleHTTPStatusCode int64
|
||||
ThrottleHTTPTimeoutMillis int64
|
||||
controlReplicasLagResult mysql.ReplicationLagResult
|
||||
TotalRowsCopied int64
|
||||
TotalDMLEventsApplied int64
|
||||
@ -212,11 +183,8 @@ type MigrationContext struct {
|
||||
|
||||
OriginalTableColumnsOnApplier *sql.ColumnList
|
||||
OriginalTableColumns *sql.ColumnList
|
||||
OriginalTableVirtualColumns *sql.ColumnList
|
||||
OriginalTableUniqueKeys [](*sql.UniqueKey)
|
||||
OriginalTableAutoIncrement uint64
|
||||
GhostTableColumns *sql.ColumnList
|
||||
GhostTableVirtualColumns *sql.ColumnList
|
||||
GhostTableUniqueKeys [](*sql.UniqueKey)
|
||||
UniqueKey *sql.UniqueKey
|
||||
SharedColumns *sql.ColumnList
|
||||
@ -231,27 +199,6 @@ type MigrationContext struct {
|
||||
ForceTmpTableName string
|
||||
|
||||
recentBinlogCoordinates mysql.BinlogCoordinates
|
||||
|
||||
BinlogSyncerMaxReconnectAttempts int
|
||||
|
||||
Log Logger
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Warning(args ...interface{}) error
|
||||
Warningf(format string, args ...interface{}) error
|
||||
Error(args ...interface{}) error
|
||||
Errorf(format string, args ...interface{}) error
|
||||
Errore(err error) error
|
||||
Fatal(args ...interface{}) error
|
||||
Fatalf(format string, args ...interface{}) error
|
||||
Fatale(err error) error
|
||||
SetLevel(level log.LogLevel)
|
||||
SetPrintStackTrace(printStackTraceFlag bool)
|
||||
}
|
||||
|
||||
type ContextConfig struct {
|
||||
@ -277,7 +224,6 @@ func NewMigrationContext() *MigrationContext {
|
||||
MaxLagMillisecondsThrottleThreshold: 1500,
|
||||
CutOverLockTimeoutSeconds: 3,
|
||||
DMLBatchSize: 10,
|
||||
etaNanoseonds: ETAUnknown,
|
||||
maxLoad: NewLoadMap(),
|
||||
criticalLoad: NewLoadMap(),
|
||||
throttleMutex: &sync.Mutex{},
|
||||
@ -285,26 +231,11 @@ func NewMigrationContext() *MigrationContext {
|
||||
throttleControlReplicaKeys: mysql.NewInstanceKeyMap(),
|
||||
configMutex: &sync.Mutex{},
|
||||
pointOfInterestTimeMutex: &sync.Mutex{},
|
||||
lastHeartbeatOnChangelogMutex: &sync.Mutex{},
|
||||
ColumnRenameMap: make(map[string]string),
|
||||
PanicAbort: make(chan error),
|
||||
Log: NewDefaultLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetConnectionConfig(storageEngine string) error {
|
||||
var transactionIsolation string
|
||||
switch storageEngine {
|
||||
case "rocksdb":
|
||||
transactionIsolation = "READ-COMMITTED"
|
||||
default:
|
||||
transactionIsolation = "REPEATABLE-READ"
|
||||
}
|
||||
this.InspectorConnectionConfig.TransactionIsolation = transactionIsolation
|
||||
this.ApplierConnectionConfig.TransactionIsolation = transactionIsolation
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSafeTableName(baseName string, suffix string) string {
|
||||
name := fmt.Sprintf("_%s_%s", baseName, suffix)
|
||||
if len(name) <= mysql.MaxTableNameLength {
|
||||
@ -410,14 +341,6 @@ func (this *MigrationContext) SetCutOverLockTimeoutSeconds(timeoutSeconds int64)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetExponentialBackoffMaxInterval(intervalSeconds int64) error {
|
||||
if intervalSeconds < 2 {
|
||||
return fmt.Errorf("Minimal maximum interval is 2sec. Timeout remains at %d", this.ExponentialBackoffMaxInterval)
|
||||
}
|
||||
this.ExponentialBackoffMaxInterval = intervalSeconds
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetDefaultNumRetries(retries int64) {
|
||||
this.throttleMutex.Lock()
|
||||
defer this.throttleMutex.Unlock()
|
||||
@ -443,44 +366,10 @@ func (this *MigrationContext) IsTransactionalTable() bool {
|
||||
{
|
||||
return true
|
||||
}
|
||||
case "rocksdb":
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetCountTableRowsCancelFunc sets the cancel function for the CountTableRows query context
|
||||
func (this *MigrationContext) SetCountTableRowsCancelFunc(f func()) {
|
||||
this.countMutex.Lock()
|
||||
defer this.countMutex.Unlock()
|
||||
|
||||
this.countTableRowsCancelFunc = f
|
||||
}
|
||||
|
||||
// IsCountingTableRows returns true if the migration has a table count query running
|
||||
func (this *MigrationContext) IsCountingTableRows() bool {
|
||||
this.countMutex.Lock()
|
||||
defer this.countMutex.Unlock()
|
||||
|
||||
return this.countTableRowsCancelFunc != nil
|
||||
}
|
||||
|
||||
// CancelTableRowsCount cancels the CountTableRows query context. It is safe to
|
||||
// call function even when IsCountingTableRows is false.
|
||||
func (this *MigrationContext) CancelTableRowsCount() {
|
||||
this.countMutex.Lock()
|
||||
defer this.countMutex.Unlock()
|
||||
|
||||
if this.countTableRowsCancelFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
this.countTableRowsCancelFunc()
|
||||
this.countTableRowsCancelFunc = nil
|
||||
}
|
||||
|
||||
// ElapsedTime returns time since very beginning of the process
|
||||
func (this *MigrationContext) ElapsedTime() time.Duration {
|
||||
return time.Since(this.StartTime)
|
||||
@ -516,40 +405,6 @@ func (this *MigrationContext) MarkRowCopyEndTime() {
|
||||
this.RowCopyEndTime = time.Now()
|
||||
}
|
||||
|
||||
func (this *MigrationContext) TimeSinceLastHeartbeatOnChangelog() time.Duration {
|
||||
return time.Since(this.GetLastHeartbeatOnChangelogTime())
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetCurrentLagDuration() time.Duration {
|
||||
return time.Duration(atomic.LoadInt64(&this.CurrentLag))
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetProgressPct() float64 {
|
||||
return math.Float64frombits(atomic.LoadUint64(&this.currentProgress))
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetProgressPct(progressPct float64) {
|
||||
atomic.StoreUint64(&this.currentProgress, math.Float64bits(progressPct))
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetETADuration() time.Duration {
|
||||
return time.Duration(atomic.LoadInt64(&this.etaNanoseonds))
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetETADuration(etaDuration time.Duration) {
|
||||
atomic.StoreInt64(&this.etaNanoseonds, etaDuration.Nanoseconds())
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetETASeconds() int64 {
|
||||
nano := atomic.LoadInt64(&this.etaNanoseonds)
|
||||
if nano < 0 {
|
||||
return ETAUnknown
|
||||
}
|
||||
return nano / int64(time.Second)
|
||||
}
|
||||
|
||||
// math.Float64bits([f=0..100])
|
||||
|
||||
// GetTotalRowsCopied returns the accurate number of rows being copied (affected)
|
||||
// This is not exactly the same as the rows being iterated via chunks, but potentially close enough
|
||||
func (this *MigrationContext) GetTotalRowsCopied() int64 {
|
||||
@ -575,20 +430,6 @@ func (this *MigrationContext) TimeSincePointOfInterest() time.Duration {
|
||||
return time.Since(this.pointOfInterestTime)
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetLastHeartbeatOnChangelogTime(t time.Time) {
|
||||
this.lastHeartbeatOnChangelogMutex.Lock()
|
||||
defer this.lastHeartbeatOnChangelogMutex.Unlock()
|
||||
|
||||
this.lastHeartbeatOnChangelogTime = t
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetLastHeartbeatOnChangelogTime() time.Time {
|
||||
this.lastHeartbeatOnChangelogMutex.Lock()
|
||||
defer this.lastHeartbeatOnChangelogMutex.Unlock()
|
||||
|
||||
return this.lastHeartbeatOnChangelogTime
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetHeartbeatIntervalMilliseconds(heartbeatIntervalMilliseconds int64) {
|
||||
if heartbeatIntervalMilliseconds < 100 {
|
||||
heartbeatIntervalMilliseconds = 100
|
||||
@ -607,8 +448,8 @@ func (this *MigrationContext) SetMaxLagMillisecondsThrottleThreshold(maxLagMilli
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetChunkSize(chunkSize int64) {
|
||||
if chunkSize < 10 {
|
||||
chunkSize = 10
|
||||
if chunkSize < 100 {
|
||||
chunkSize = 100
|
||||
}
|
||||
if chunkSize > 100000 {
|
||||
chunkSize = 100000
|
||||
@ -694,13 +535,6 @@ func (this *MigrationContext) SetThrottleHTTP(throttleHTTP string) {
|
||||
this.throttleHTTP = throttleHTTP
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetIgnoreHTTPErrors(ignoreHTTPErrors bool) {
|
||||
this.throttleHTTPMutex.Lock()
|
||||
defer this.throttleHTTPMutex.Unlock()
|
||||
|
||||
this.IgnoreHTTPErrors = ignoreHTTPErrors
|
||||
}
|
||||
|
||||
func (this *MigrationContext) GetMaxLoad() LoadMap {
|
||||
this.throttleMutex.Lock()
|
||||
defer this.throttleMutex.Unlock()
|
||||
@ -847,13 +681,6 @@ func (this *MigrationContext) ApplyCredentials() {
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MigrationContext) SetupTLS() error {
|
||||
if this.UseTLS {
|
||||
return this.InspectorConnectionConfig.UseTLS(this.TLSCACertificate, this.TLSCertificate, this.TLSKey, this.TLSAllowInsecure)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadConfigFile attempts to read the config file, if it exists
|
||||
func (this *MigrationContext) ReadConfigFile() error {
|
||||
this.configMutex.Lock()
|
||||
@ -862,39 +689,10 @@ func (this *MigrationContext) ReadConfigFile() error {
|
||||
if this.ConfigFile == "" {
|
||||
return nil
|
||||
}
|
||||
cfg, err := ini.Load(this.ConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.Section("client").HasKey("user") {
|
||||
this.config.Client.User = cfg.Section("client").Key("user").String()
|
||||
}
|
||||
|
||||
if cfg.Section("client").HasKey("password") {
|
||||
this.config.Client.Password = cfg.Section("client").Key("password").String()
|
||||
}
|
||||
|
||||
if cfg.Section("osc").HasKey("chunk_size") {
|
||||
this.config.Osc.Chunk_Size, err = cfg.Section("osc").Key("chunk_size").Int64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to read osc chunk size: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Section("osc").HasKey("max_load") {
|
||||
this.config.Osc.Max_Load = cfg.Section("osc").Key("max_load").String()
|
||||
}
|
||||
|
||||
if cfg.Section("osc").HasKey("replication_lag_query") {
|
||||
this.config.Osc.Replication_Lag_Query = cfg.Section("osc").Key("replication_lag_query").String()
|
||||
}
|
||||
|
||||
if cfg.Section("osc").HasKey("max_lag_millis") {
|
||||
this.config.Osc.Max_Lag_Millis, err = cfg.Section("osc").Key("max_lag_millis").Int64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to read max lag millis: %w", err)
|
||||
}
|
||||
gcfg.RelaxedParserMode = true
|
||||
gcfgscanner.RelaxedScannerMode = true
|
||||
if err := gcfg.ReadFileInto(&this.config, this.ConfigFile); err != nil {
|
||||
return fmt.Errorf("Error reading config file %s. Details: %s", this.ConfigFile, err.Error())
|
||||
}
|
||||
|
||||
// We accept user & password in the form "${SOME_ENV_VARIABLE}" in which case we pull
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -58,65 +56,3 @@ func TestGetTableNames(t *testing.T) {
|
||||
test.S(t).ExpectEquals(context.GetChangelogTableName(), "_tmp_ghc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfigFile(t *testing.T) {
|
||||
{
|
||||
context := NewMigrationContext()
|
||||
context.ConfigFile = "/does/not/exist"
|
||||
if err := context.ReadConfigFile(); err == nil {
|
||||
t.Fatal("Expected .ReadConfigFile() to return an error, got nil")
|
||||
}
|
||||
}
|
||||
{
|
||||
f, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmp file: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
f.Write([]byte("[client]"))
|
||||
context := NewMigrationContext()
|
||||
context.ConfigFile = f.Name()
|
||||
if err := context.ReadConfigFile(); err != nil {
|
||||
t.Fatalf(".ReadConfigFile() failed: %v", err)
|
||||
}
|
||||
}
|
||||
{
|
||||
f, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmp file: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
f.Write([]byte("[client]\nuser=test\npassword=123456"))
|
||||
context := NewMigrationContext()
|
||||
context.ConfigFile = f.Name()
|
||||
if err := context.ReadConfigFile(); err != nil {
|
||||
t.Fatalf(".ReadConfigFile() failed: %v", err)
|
||||
}
|
||||
|
||||
if context.config.Client.User != "test" {
|
||||
t.Fatalf("Expected client user %q, got %q", "test", context.config.Client.User)
|
||||
} else if context.config.Client.Password != "123456" {
|
||||
t.Fatalf("Expected client password %q, got %q", "123456", context.config.Client.Password)
|
||||
}
|
||||
}
|
||||
{
|
||||
f, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmp file: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
f.Write([]byte("[osc]\nmax_load=10"))
|
||||
context := NewMigrationContext()
|
||||
context.ConfigFile = f.Name()
|
||||
if err := context.ReadConfigFile(); err != nil {
|
||||
t.Fatalf(".ReadConfigFile() failed: %v", err)
|
||||
}
|
||||
|
||||
if context.config.Osc.Max_Load != "10" {
|
||||
t.Fatalf("Expected osc 'max_load' %q, got %q", "10", context.config.Osc.Max_Load)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/openark/golib/log"
|
||||
)
|
||||
|
||||
type simpleLogger struct{}
|
||||
|
||||
func NewDefaultLogger() *simpleLogger {
|
||||
return &simpleLogger{}
|
||||
}
|
||||
|
||||
func (*simpleLogger) Debug(args ...interface{}) {
|
||||
log.Debug(args[0].(string), args[1:])
|
||||
}
|
||||
|
||||
func (*simpleLogger) Debugf(format string, args ...interface{}) {
|
||||
log.Debugf(format, args...)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Info(args ...interface{}) {
|
||||
log.Info(args[0].(string), args[1:])
|
||||
}
|
||||
|
||||
func (*simpleLogger) Infof(format string, args ...interface{}) {
|
||||
log.Infof(format, args...)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Warning(args ...interface{}) error {
|
||||
return log.Warning(args[0].(string), args[1:])
|
||||
}
|
||||
|
||||
func (*simpleLogger) Warningf(format string, args ...interface{}) error {
|
||||
return log.Warningf(format, args...)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Error(args ...interface{}) error {
|
||||
return log.Error(args[0].(string), args[1:])
|
||||
}
|
||||
|
||||
func (*simpleLogger) Errorf(format string, args ...interface{}) error {
|
||||
return log.Errorf(format, args...)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Errore(err error) error {
|
||||
return log.Errore(err)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Fatal(args ...interface{}) error {
|
||||
return log.Fatal(args[0].(string), args[1:])
|
||||
}
|
||||
|
||||
func (*simpleLogger) Fatalf(format string, args ...interface{}) error {
|
||||
return log.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
func (*simpleLogger) Fatale(err error) error {
|
||||
return log.Fatale(err)
|
||||
}
|
||||
|
||||
func (*simpleLogger) SetLevel(level log.LogLevel) {
|
||||
log.SetLevel(level)
|
||||
}
|
||||
|
||||
func (*simpleLogger) SetPrintStackTrace(printStackTraceFlag bool) {
|
||||
log.SetPrintStackTrace(printStackTraceFlag)
|
||||
}
|
@ -8,8 +8,8 @@ package base
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -13,8 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
gosql "database/sql"
|
||||
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/outbrain/golib/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -25,7 +25,9 @@ func PrettifyDurationOutput(d time.Duration) string {
|
||||
if d < time.Second {
|
||||
return "0s"
|
||||
}
|
||||
return prettifyDurationRegexp.ReplaceAllString(d.String(), "")
|
||||
result := fmt.Sprintf("%s", d)
|
||||
result = prettifyDurationRegexp.ReplaceAllString(result, "")
|
||||
return result
|
||||
}
|
||||
|
||||
func FileExists(fileName string) bool {
|
||||
@ -38,9 +40,10 @@ func FileExists(fileName string) bool {
|
||||
func TouchFile(fileName string) error {
|
||||
f, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
return (err)
|
||||
}
|
||||
return f.Close()
|
||||
defer f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringContainsAll returns true if `s` contains all non empty given `substrings`
|
||||
@ -61,31 +64,20 @@ func StringContainsAll(s string, substrings ...string) bool {
|
||||
return nonEmptyStringsFound
|
||||
}
|
||||
|
||||
func ValidateConnection(db *gosql.DB, connectionConfig *mysql.ConnectionConfig, migrationContext *MigrationContext, name string) (string, error) {
|
||||
versionQuery := `select @@global.version`
|
||||
func ValidateConnection(db *gosql.DB, connectionConfig *mysql.ConnectionConfig) (string, error) {
|
||||
query := `select @@global.port, @@global.version`
|
||||
var port, extraPort int
|
||||
var version string
|
||||
if err := db.QueryRow(versionQuery).Scan(&version); err != nil {
|
||||
if err := db.QueryRow(query).Scan(&port, &version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
extraPortQuery := `select @@global.extra_port`
|
||||
if err := db.QueryRow(extraPortQuery).Scan(&extraPort); err != nil { // nolint:staticcheck
|
||||
if err := db.QueryRow(extraPortQuery).Scan(&extraPort); err != nil {
|
||||
// swallow this error. not all servers support extra_port
|
||||
}
|
||||
// AliyunRDS set users port to "NULL", replace it by gh-ost param
|
||||
// GCP set users port to "NULL", replace it by gh-ost param
|
||||
// Azure MySQL set users port to a different value by design, replace it by gh-ost para
|
||||
if migrationContext.AliyunRDS || migrationContext.GoogleCloudPlatform || migrationContext.AzureMySQL {
|
||||
port = connectionConfig.Key.Port
|
||||
} else {
|
||||
portQuery := `select @@global.port`
|
||||
if err := db.QueryRow(portQuery).Scan(&port); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if connectionConfig.Key.Port == port || (extraPort > 0 && connectionConfig.Key.Port == extraPort) {
|
||||
migrationContext.Log.Infof("%s connection validated on %+v", name, connectionConfig.Key)
|
||||
log.Infof("connection validated on %+v", connectionConfig.Key)
|
||||
return version, nil
|
||||
} else if extraPort == 0 {
|
||||
return "", fmt.Errorf("Unexpected database port reported: %+v", port)
|
||||
|
@ -8,8 +8,8 @@ package base
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -7,18 +7,17 @@ package binlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EventDML string
|
||||
|
||||
const (
|
||||
NotDML EventDML = "NoDML"
|
||||
InsertDML EventDML = "Insert"
|
||||
UpdateDML EventDML = "Update"
|
||||
DeleteDML EventDML = "Delete"
|
||||
InsertDML = "Insert"
|
||||
UpdateDML = "Update"
|
||||
DeleteDML = "Delete"
|
||||
)
|
||||
|
||||
func ToEventDML(description string) EventDML {
|
||||
|
@ -26,7 +26,7 @@ func NewBinlogEntry(logFile string, logPos uint64) *BinlogEntry {
|
||||
return binlogEntry
|
||||
}
|
||||
|
||||
// NewBinlogEntryAt creates an empty, ready to go BinlogEntry object
|
||||
// NewBinlogEntry creates an empty, ready to go BinlogEntry object
|
||||
func NewBinlogEntryAt(coordinates mysql.BinlogCoordinates) *BinlogEntry {
|
||||
binlogEntry := &BinlogEntry{
|
||||
Coordinates: coordinates,
|
||||
@ -41,7 +41,7 @@ func (this *BinlogEntry) Duplicate() *BinlogEntry {
|
||||
return binlogEntry
|
||||
}
|
||||
|
||||
// String() returns a string representation of this binlog entry
|
||||
// Duplicate creates and returns a new binlog entry, with some of the attributes pre-assigned
|
||||
func (this *BinlogEntry) String() string {
|
||||
return fmt.Sprintf("[BinlogEntry at %+v; dml:%+v]", this.Coordinates, this.DmlEvent)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -13,13 +13,13 @@ import (
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
|
||||
gomysql "github.com/go-mysql-org/go-mysql/mysql"
|
||||
"github.com/go-mysql-org/go-mysql/replication"
|
||||
"github.com/outbrain/golib/log"
|
||||
gomysql "github.com/siddontang/go-mysql/mysql"
|
||||
"github.com/siddontang/go-mysql/replication"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type GoMySQLReader struct {
|
||||
migrationContext *base.MigrationContext
|
||||
connectionConfig *mysql.ConnectionConfig
|
||||
binlogSyncer *replication.BinlogSyncer
|
||||
binlogStreamer *replication.BinlogStreamer
|
||||
@ -28,40 +28,40 @@ type GoMySQLReader struct {
|
||||
LastAppliedRowsEventHint mysql.BinlogCoordinates
|
||||
}
|
||||
|
||||
func NewGoMySQLReader(migrationContext *base.MigrationContext) *GoMySQLReader {
|
||||
connectionConfig := migrationContext.InspectorConnectionConfig
|
||||
return &GoMySQLReader{
|
||||
migrationContext: migrationContext,
|
||||
connectionConfig: connectionConfig,
|
||||
func NewGoMySQLReader(migrationContext *base.MigrationContext) (binlogReader *GoMySQLReader, err error) {
|
||||
binlogReader = &GoMySQLReader{
|
||||
connectionConfig: migrationContext.InspectorConnectionConfig,
|
||||
currentCoordinates: mysql.BinlogCoordinates{},
|
||||
currentCoordinatesMutex: &sync.Mutex{},
|
||||
binlogSyncer: replication.NewBinlogSyncer(replication.BinlogSyncerConfig{
|
||||
ServerID: uint32(migrationContext.ReplicaServerId),
|
||||
Flavor: gomysql.MySQLFlavor,
|
||||
Host: connectionConfig.Key.Hostname,
|
||||
Port: uint16(connectionConfig.Key.Port),
|
||||
User: connectionConfig.User,
|
||||
Password: connectionConfig.Password,
|
||||
TLSConfig: connectionConfig.TLSConfig(),
|
||||
UseDecimal: true,
|
||||
MaxReconnectAttempts: migrationContext.BinlogSyncerMaxReconnectAttempts,
|
||||
}),
|
||||
binlogSyncer: nil,
|
||||
binlogStreamer: nil,
|
||||
}
|
||||
|
||||
serverId := uint32(migrationContext.ReplicaServerId)
|
||||
|
||||
binlogSyncerConfig := replication.BinlogSyncerConfig{
|
||||
ServerID: serverId,
|
||||
Flavor: "mysql",
|
||||
Host: binlogReader.connectionConfig.Key.Hostname,
|
||||
Port: uint16(binlogReader.connectionConfig.Key.Port),
|
||||
User: binlogReader.connectionConfig.User,
|
||||
Password: binlogReader.connectionConfig.Password,
|
||||
}
|
||||
binlogReader.binlogSyncer = replication.NewBinlogSyncer(binlogSyncerConfig)
|
||||
|
||||
return binlogReader, err
|
||||
}
|
||||
|
||||
// ConnectBinlogStreamer
|
||||
func (this *GoMySQLReader) ConnectBinlogStreamer(coordinates mysql.BinlogCoordinates) (err error) {
|
||||
if coordinates.IsEmpty() {
|
||||
return this.migrationContext.Log.Errorf("Empty coordinates at ConnectBinlogStreamer()")
|
||||
return log.Errorf("Empty coordinates at ConnectBinlogStreamer()")
|
||||
}
|
||||
|
||||
this.currentCoordinates = coordinates
|
||||
this.migrationContext.Log.Infof("Connecting binlog streamer at %+v", this.currentCoordinates)
|
||||
log.Infof("Connecting binlog streamer at %+v", this.currentCoordinates)
|
||||
// Start sync with specified binlog file and position
|
||||
this.binlogStreamer, err = this.binlogSyncer.StartSync(gomysql.Position{
|
||||
Name: this.currentCoordinates.LogFile,
|
||||
Pos: uint32(this.currentCoordinates.LogPos),
|
||||
})
|
||||
this.binlogStreamer, err = this.binlogSyncer.StartSync(gomysql.Position{this.currentCoordinates.LogFile, uint32(this.currentCoordinates.LogPos)})
|
||||
|
||||
return err
|
||||
}
|
||||
@ -76,7 +76,7 @@ func (this *GoMySQLReader) GetCurrentBinlogCoordinates() *mysql.BinlogCoordinate
|
||||
// StreamEvents
|
||||
func (this *GoMySQLReader) handleRowsEvent(ev *replication.BinlogEvent, rowsEvent *replication.RowsEvent, entriesChannel chan<- *BinlogEntry) error {
|
||||
if this.currentCoordinates.SmallerThanOrEquals(&this.LastAppliedRowsEventHint) {
|
||||
this.migrationContext.Log.Debugf("Skipping handled query at %+v", this.currentCoordinates)
|
||||
log.Debugf("Skipping handled query at %+v", this.currentCoordinates)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ func (this *GoMySQLReader) handleRowsEvent(ev *replication.BinlogEvent, rowsEven
|
||||
binlogEntry.DmlEvent.WhereColumnValues = sql.ToColumnValues(row)
|
||||
}
|
||||
}
|
||||
// The channel will do the throttling. Whoever is reading from the channel
|
||||
// The channel will do the throttling. Whoever is reding from the channel
|
||||
// decides whether action is taken synchronously (meaning we wait before
|
||||
// next iteration) or asynchronously (we keep pushing more events)
|
||||
// In reality, reads will be synchronous
|
||||
@ -139,22 +139,20 @@ func (this *GoMySQLReader) StreamEvents(canStopStreaming func() bool, entriesCha
|
||||
defer this.currentCoordinatesMutex.Unlock()
|
||||
this.currentCoordinates.LogPos = int64(ev.Header.LogPos)
|
||||
}()
|
||||
|
||||
switch binlogEvent := ev.Event.(type) {
|
||||
case *replication.RotateEvent:
|
||||
if rotateEvent, ok := ev.Event.(*replication.RotateEvent); ok {
|
||||
func() {
|
||||
this.currentCoordinatesMutex.Lock()
|
||||
defer this.currentCoordinatesMutex.Unlock()
|
||||
this.currentCoordinates.LogFile = string(binlogEvent.NextLogName)
|
||||
this.currentCoordinates.LogFile = string(rotateEvent.NextLogName)
|
||||
}()
|
||||
this.migrationContext.Log.Infof("rotate to next log from %s:%d to %s", this.currentCoordinates.LogFile, int64(ev.Header.LogPos), binlogEvent.NextLogName)
|
||||
case *replication.RowsEvent:
|
||||
if err := this.handleRowsEvent(ev, binlogEvent, entriesChannel); err != nil {
|
||||
log.Infof("rotate to next log name: %s", rotateEvent.NextLogName)
|
||||
} else if rowsEvent, ok := ev.Event.(*replication.RowsEvent); ok {
|
||||
if err := this.handleRowsEvent(ev, rowsEvent, entriesChannel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
this.migrationContext.Log.Debugf("done streaming events")
|
||||
log.Debugf("done streaming events")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -8,18 +8,15 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/github/gh-ost/go/logic"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/openark/golib/log"
|
||||
"github.com/outbrain/golib/log"
|
||||
|
||||
"golang.org/x/term"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var AppVersion string
|
||||
@ -33,7 +30,7 @@ func acceptSignals(migrationContext *base.MigrationContext) {
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
migrationContext.Log.Infof("Received SIGHUP. Reloading configuration")
|
||||
log.Infof("Received SIGHUP. Reloading configuration")
|
||||
if err := migrationContext.ReadConfigFile(); err != nil {
|
||||
log.Errore(err)
|
||||
} else {
|
||||
@ -47,10 +44,10 @@ func acceptSignals(migrationContext *base.MigrationContext) {
|
||||
// main is the application's entry point. It will either spawn a CLI or HTTP interfaces.
|
||||
func main() {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
|
||||
flag.StringVar(&migrationContext.InspectorConnectionConfig.Key.Hostname, "host", "127.0.0.1", "MySQL hostname (preferably a replica, not the master)")
|
||||
flag.StringVar(&migrationContext.AssumeMasterHostname, "assume-master-host", "", "(optional) explicitly tell gh-ost the identity of the master. Format: some.host.com[:port] This is useful in master-master setups where you wish to pick an explicit master, or in a tungsten-replicator where gh-ost is unable to determine the master")
|
||||
flag.IntVar(&migrationContext.InspectorConnectionConfig.Key.Port, "port", 3306, "MySQL port (preferably a replica, not the master)")
|
||||
flag.Float64Var(&migrationContext.InspectorConnectionConfig.Timeout, "mysql-timeout", 0.0, "Connect, read and write timeout for MySQL")
|
||||
flag.StringVar(&migrationContext.CliUser, "user", "", "MySQL user")
|
||||
flag.StringVar(&migrationContext.CliPassword, "password", "", "MySQL password")
|
||||
flag.StringVar(&migrationContext.CliMasterUser, "master-user", "", "MySQL user on master, if different from that on replica. Requires --assume-master-host")
|
||||
@ -58,18 +55,9 @@ func main() {
|
||||
flag.StringVar(&migrationContext.ConfigFile, "conf", "", "Config file")
|
||||
askPass := flag.Bool("ask-pass", false, "prompt for MySQL password")
|
||||
|
||||
flag.BoolVar(&migrationContext.UseTLS, "ssl", false, "Enable SSL encrypted connections to MySQL hosts")
|
||||
flag.StringVar(&migrationContext.TLSCACertificate, "ssl-ca", "", "CA certificate in PEM format for TLS connections to MySQL hosts. Requires --ssl")
|
||||
flag.StringVar(&migrationContext.TLSCertificate, "ssl-cert", "", "Certificate in PEM format for TLS connections to MySQL hosts. Requires --ssl")
|
||||
flag.StringVar(&migrationContext.TLSKey, "ssl-key", "", "Key in PEM format for TLS connections to MySQL hosts. Requires --ssl")
|
||||
flag.BoolVar(&migrationContext.TLSAllowInsecure, "ssl-allow-insecure", false, "Skips verification of MySQL hosts' certificate chain and host name. Requires --ssl")
|
||||
|
||||
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
|
||||
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
|
||||
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
|
||||
flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", false, "Attempt to use instant DDL for this migration first")
|
||||
storageEngine := flag.String("storage-engine", "innodb", "Specify table storage engine (default: 'innodb'). When 'rocksdb': the session transaction isolation level is changed from REPEATABLE_READ to READ_COMMITTED.")
|
||||
|
||||
flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)")
|
||||
flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy")
|
||||
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")
|
||||
@ -80,11 +68,6 @@ func main() {
|
||||
flag.BoolVar(&migrationContext.IsTungsten, "tungsten", false, "explicitly let gh-ost know that you are running on a tungsten-replication based topology (you are likely to also provide --assume-master-host)")
|
||||
flag.BoolVar(&migrationContext.DiscardForeignKeys, "discard-foreign-keys", false, "DANGER! This flag will migrate a table that has foreign keys and will NOT create foreign keys on the ghost table, thus your altered table will have NO foreign keys. This is useful for intentional dropping of foreign keys")
|
||||
flag.BoolVar(&migrationContext.SkipForeignKeyChecks, "skip-foreign-key-checks", false, "set to 'true' when you know for certain there are no foreign keys on your table, and wish to skip the time it takes for gh-ost to verify that")
|
||||
flag.BoolVar(&migrationContext.SkipStrictMode, "skip-strict-mode", false, "explicitly tell gh-ost binlog applier not to enforce strict sql mode")
|
||||
flag.BoolVar(&migrationContext.AllowZeroInDate, "allow-zero-in-date", false, "explicitly tell gh-ost binlog applier to ignore NO_ZERO_IN_DATE,NO_ZERO_DATE in sql_mode")
|
||||
flag.BoolVar(&migrationContext.AliyunRDS, "aliyun-rds", false, "set to 'true' when you execute on Aliyun RDS.")
|
||||
flag.BoolVar(&migrationContext.GoogleCloudPlatform, "gcp", false, "set to 'true' when you execute on a 1st generation Google Cloud Platform (GCP).")
|
||||
flag.BoolVar(&migrationContext.AzureMySQL, "azure", false, "set to 'true' when you execute on Azure Database on MySQL.")
|
||||
|
||||
executeFlag := flag.Bool("execute", false, "actually execute the alter & migrate the table. Default is noop: do some tests and exit")
|
||||
flag.BoolVar(&migrationContext.TestOnReplica, "test-on-replica", false, "Have the migration run on a replica, not on the master. At the end of migration replication is stopped, and tables are swapped and immediately swap-revert. Replication remains stopped and you can compare the two tables for building trust")
|
||||
@ -97,13 +80,10 @@ func main() {
|
||||
flag.BoolVar(&migrationContext.TimestampOldTable, "timestamp-old-table", false, "Use a timestamp in old table name. This makes old table names unique and non conflicting cross migrations")
|
||||
cutOver := flag.String("cut-over", "atomic", "choose cut-over type (default|atomic, two-step)")
|
||||
flag.BoolVar(&migrationContext.ForceNamedCutOverCommand, "force-named-cut-over", false, "When true, the 'unpostpone|cut-over' interactive command must name the migrated table")
|
||||
flag.BoolVar(&migrationContext.ForceNamedPanicCommand, "force-named-panic", false, "When true, the 'panic' interactive command must name the migrated table")
|
||||
|
||||
flag.BoolVar(&migrationContext.SwitchToRowBinlogFormat, "switch-to-rbr", false, "let this tool automatically switch binary log format to 'ROW' on the replica, if needed. The format will NOT be switched back. I'm too scared to do that, and wish to protect you if you happen to execute another migration while this one is running")
|
||||
flag.BoolVar(&migrationContext.AssumeRBR, "assume-rbr", false, "set to 'true' when you know for certain your server uses 'ROW' binlog_format. gh-ost is unable to tell, event after reading binlog_format, whether the replication process does indeed use 'ROW', and restarts replication to be certain RBR setting is applied. Such operation requires SUPER privileges which you might not have. Setting this flag avoids restarting replication and you can proceed to use gh-ost without SUPER privileges")
|
||||
flag.BoolVar(&migrationContext.CutOverExponentialBackoff, "cut-over-exponential-backoff", false, "Wait exponentially longer intervals between failed cut-over attempts. Wait intervals obey a maximum configurable with 'exponential-backoff-max-interval').")
|
||||
exponentialBackoffMaxInterval := flag.Int64("exponential-backoff-max-interval", 64, "Maximum number of seconds to wait between attempts when performing various operations with exponential backoff.")
|
||||
chunkSize := flag.Int64("chunk-size", 1000, "amount of rows to handle in each iteration (allowed range: 10-100,000)")
|
||||
chunkSize := flag.Int64("chunk-size", 1000, "amount of rows to handle in each iteration (allowed range: 100-100,000)")
|
||||
dmlBatchSize := flag.Int64("dml-batch-size", 10, "batch size for DML events to apply in a single transaction (range 1-100)")
|
||||
defaultRetries := flag.Int64("default-retries", 60, "Default number of retries for various operations before panicking")
|
||||
cutOverLockTimeoutSeconds := flag.Int64("cut-over-lock-timeout-seconds", 3, "Max number of seconds to hold locks on tables while attempting to cut-over (retry attempted when lock exceeds timeout)")
|
||||
@ -114,9 +94,6 @@ func main() {
|
||||
throttleControlReplicas := flag.String("throttle-control-replicas", "", "List of replicas on which to check for lag; comma delimited. Example: myhost1.com:3306,myhost2.com,myhost3.com:3307")
|
||||
throttleQuery := flag.String("throttle-query", "", "when given, issued (every second) to check if operation should throttle. Expecting to return zero for no-throttle, >0 for throttle. Query is issued on the migrated server. Make sure this query is lightweight")
|
||||
throttleHTTP := flag.String("throttle-http", "", "when given, gh-ost checks given URL via HEAD request; any response code other than 200 (OK) causes throttling; make sure it has low latency response")
|
||||
flag.Int64Var(&migrationContext.ThrottleHTTPIntervalMillis, "throttle-http-interval-millis", 100, "Number of milliseconds to wait before triggering another HTTP throttle check")
|
||||
flag.Int64Var(&migrationContext.ThrottleHTTPTimeoutMillis, "throttle-http-timeout-millis", 1000, "Number of milliseconds to use as an HTTP throttle check timeout")
|
||||
ignoreHTTPErrors := flag.Bool("ignore-http-errors", false, "ignore HTTP connection errors during throttle check")
|
||||
heartbeatIntervalMillis := flag.Int64("heartbeat-interval-millis", 100, "how frequently would gh-ost inject a heartbeat value")
|
||||
flag.StringVar(&migrationContext.ThrottleFlagFile, "throttle-flag-file", "", "operation pauses when this file exists; hint: use a file that is specific to the table being altered")
|
||||
flag.StringVar(&migrationContext.ThrottleAdditionalFlagFile, "throttle-additional-flag-file", "/tmp/gh-ost.throttle", "operation pauses when this file exists; hint: keep default, use for throttling multiple gh-ost operations")
|
||||
@ -129,17 +106,13 @@ func main() {
|
||||
|
||||
flag.StringVar(&migrationContext.HooksPath, "hooks-path", "", "directory where hook files are found (default: empty, ie. hooks disabled). Hook files found on this path, and conforming to hook naming conventions will be executed")
|
||||
flag.StringVar(&migrationContext.HooksHintMessage, "hooks-hint", "", "arbitrary message to be injected to hooks via GH_OST_HOOKS_HINT, for your convenience")
|
||||
flag.StringVar(&migrationContext.HooksHintOwner, "hooks-hint-owner", "", "arbitrary name of owner to be injected to hooks via GH_OST_HOOKS_HINT_OWNER, for your convenience")
|
||||
flag.StringVar(&migrationContext.HooksHintToken, "hooks-hint-token", "", "arbitrary token to be injected to hooks via GH_OST_HOOKS_HINT_TOKEN, for your convenience")
|
||||
flag.Int64Var(&migrationContext.HooksStatusIntervalSec, "hooks-status-interval", 60, "how many seconds to wait between calling onStatus hook")
|
||||
|
||||
flag.UintVar(&migrationContext.ReplicaServerId, "replica-server-id", 99999, "server id used by gh-ost process. Default: 99999")
|
||||
flag.IntVar(&migrationContext.BinlogSyncerMaxReconnectAttempts, "binlogsyncer-max-reconnect-attempts", 0, "when master node fails, the maximum number of binlog synchronization attempts to reconnect. 0 is unlimited")
|
||||
|
||||
maxLoad := flag.String("max-load", "", "Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'. When status exceeds threshold, app throttles writes")
|
||||
criticalLoad := flag.String("critical-load", "", "Comma delimited status-name=threshold, same format as --max-load. When status exceeds threshold, app panics and quits")
|
||||
flag.Int64Var(&migrationContext.CriticalLoadIntervalMilliseconds, "critical-load-interval-millis", 0, "When 0, migration immediately bails out upon meeting critical-load. When non-zero, a second check is done after given interval, and migration only bails out if 2nd check still meets critical load")
|
||||
flag.Int64Var(&migrationContext.CriticalLoadHibernateSeconds, "critical-load-hibernate-seconds", 0, "When non-zero, critical-load does not panic and bail out; instead, gh-ost goes into hibernation for the specified duration. It will not read/write anything from/to any server")
|
||||
flag.Int64Var(&migrationContext.CriticalLoadHibernateSeconds, "critical-load-hibernate-seconds", 0, "When nonzero, critical-load does not panic and bail out; instead, gh-ost goes into hibernate for the specified duration. It will not read/write anything to from/to any server")
|
||||
quiet := flag.Bool("quiet", false, "quiet")
|
||||
verbose := flag.Bool("verbose", false, "verbose")
|
||||
debug := flag.Bool("debug", false, "debug mode (very verbose)")
|
||||
@ -169,92 +142,57 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
migrationContext.Log.SetLevel(log.ERROR)
|
||||
log.SetLevel(log.ERROR)
|
||||
if *verbose {
|
||||
migrationContext.Log.SetLevel(log.INFO)
|
||||
log.SetLevel(log.INFO)
|
||||
}
|
||||
if *debug {
|
||||
migrationContext.Log.SetLevel(log.DEBUG)
|
||||
log.SetLevel(log.DEBUG)
|
||||
}
|
||||
if *stack {
|
||||
migrationContext.Log.SetPrintStackTrace(*stack)
|
||||
log.SetPrintStackTrace(*stack)
|
||||
}
|
||||
if *quiet {
|
||||
// Override!!
|
||||
migrationContext.Log.SetLevel(log.ERROR)
|
||||
log.SetLevel(log.ERROR)
|
||||
}
|
||||
|
||||
if err := migrationContext.SetConnectionConfig(*storageEngine); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
}
|
||||
|
||||
if migrationContext.AlterStatement == "" {
|
||||
log.Fatal("--alter must be provided and statement must not be empty")
|
||||
}
|
||||
parser := sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
|
||||
migrationContext.AlterStatementOptions = parser.GetAlterStatementOptions()
|
||||
|
||||
if migrationContext.DatabaseName == "" {
|
||||
if parser.HasExplicitSchema() {
|
||||
migrationContext.DatabaseName = parser.GetExplicitSchema()
|
||||
} else {
|
||||
log.Fatal("--database must be provided and database name must not be empty, or --alter must specify database name")
|
||||
}
|
||||
log.Fatalf("--database must be provided and database name must not be empty")
|
||||
}
|
||||
|
||||
if err := flag.Set("database", url.QueryEscape(migrationContext.DatabaseName)); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
}
|
||||
|
||||
if migrationContext.OriginalTableName == "" {
|
||||
if parser.HasExplicitTable() {
|
||||
migrationContext.OriginalTableName = parser.GetExplicitTable()
|
||||
} else {
|
||||
log.Fatal("--table must be provided and table name must not be empty, or --alter must specify table name")
|
||||
}
|
||||
log.Fatalf("--table must be provided and table name must not be empty")
|
||||
}
|
||||
if migrationContext.AlterStatement == "" {
|
||||
log.Fatalf("--alter must be provided and statement must not be empty")
|
||||
}
|
||||
migrationContext.Noop = !(*executeFlag)
|
||||
if migrationContext.AllowedRunningOnMaster && migrationContext.TestOnReplica {
|
||||
migrationContext.Log.Fatal("--allow-on-master and --test-on-replica are mutually exclusive")
|
||||
log.Fatalf("--allow-on-master and --test-on-replica are mutually exclusive")
|
||||
}
|
||||
if migrationContext.AllowedRunningOnMaster && migrationContext.MigrateOnReplica {
|
||||
migrationContext.Log.Fatal("--allow-on-master and --migrate-on-replica are mutually exclusive")
|
||||
log.Fatalf("--allow-on-master and --migrate-on-replica are mutually exclusive")
|
||||
}
|
||||
if migrationContext.MigrateOnReplica && migrationContext.TestOnReplica {
|
||||
migrationContext.Log.Fatal("--migrate-on-replica and --test-on-replica are mutually exclusive")
|
||||
log.Fatalf("--migrate-on-replica and --test-on-replica are mutually exclusive")
|
||||
}
|
||||
if migrationContext.SwitchToRowBinlogFormat && migrationContext.AssumeRBR {
|
||||
migrationContext.Log.Fatal("--switch-to-rbr and --assume-rbr are mutually exclusive")
|
||||
log.Fatalf("--switch-to-rbr and --assume-rbr are mutually exclusive")
|
||||
}
|
||||
if migrationContext.TestOnReplicaSkipReplicaStop {
|
||||
if !migrationContext.TestOnReplica {
|
||||
migrationContext.Log.Fatal("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
|
||||
log.Fatalf("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
|
||||
}
|
||||
migrationContext.Log.Warning("--test-on-replica-skip-replica-stop enabled. We will not stop replication before cut-over. Ensure you have a plugin that does this.")
|
||||
log.Warning("--test-on-replica-skip-replica-stop enabled. We will not stop replication before cut-over. Ensure you have a plugin that does this.")
|
||||
}
|
||||
if migrationContext.CliMasterUser != "" && migrationContext.AssumeMasterHostname == "" {
|
||||
migrationContext.Log.Fatal("--master-user requires --assume-master-host")
|
||||
log.Fatalf("--master-user requires --assume-master-host")
|
||||
}
|
||||
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
|
||||
migrationContext.Log.Fatal("--master-password requires --assume-master-host")
|
||||
}
|
||||
if migrationContext.TLSCACertificate != "" && !migrationContext.UseTLS {
|
||||
migrationContext.Log.Fatal("--ssl-ca requires --ssl")
|
||||
}
|
||||
if migrationContext.TLSCertificate != "" && !migrationContext.UseTLS {
|
||||
migrationContext.Log.Fatal("--ssl-cert requires --ssl")
|
||||
}
|
||||
if migrationContext.TLSKey != "" && !migrationContext.UseTLS {
|
||||
migrationContext.Log.Fatal("--ssl-key requires --ssl")
|
||||
}
|
||||
if migrationContext.TLSAllowInsecure && !migrationContext.UseTLS {
|
||||
migrationContext.Log.Fatal("--ssl-allow-insecure requires --ssl")
|
||||
log.Fatalf("--master-password requires --assume-master-host")
|
||||
}
|
||||
if *replicationLagQuery != "" {
|
||||
migrationContext.Log.Warning("--replication-lag-query is deprecated")
|
||||
}
|
||||
if *storageEngine == "rocksdb" {
|
||||
migrationContext.Log.Warning("RocksDB storage engine support is experimental")
|
||||
log.Warningf("--replication-lag-query is deprecated")
|
||||
}
|
||||
|
||||
switch *cutOver {
|
||||
@ -263,28 +201,28 @@ func main() {
|
||||
case "two-step":
|
||||
migrationContext.CutOverType = base.CutOverTwoStep
|
||||
default:
|
||||
migrationContext.Log.Fatalf("Unknown cut-over: %s", *cutOver)
|
||||
log.Fatalf("Unknown cut-over: %s", *cutOver)
|
||||
}
|
||||
if err := migrationContext.ReadConfigFile(); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
if err := migrationContext.ReadThrottleControlReplicaKeys(*throttleControlReplicas); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
if err := migrationContext.ReadMaxLoad(*maxLoad); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
if err := migrationContext.ReadCriticalLoad(*criticalLoad); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
if migrationContext.ServeSocketFile == "" {
|
||||
migrationContext.ServeSocketFile = fmt.Sprintf("/tmp/gh-ost.%s.%s.sock", migrationContext.DatabaseName, migrationContext.OriginalTableName)
|
||||
}
|
||||
if *askPass {
|
||||
fmt.Println("Password:")
|
||||
bytePassword, err := term.ReadPassword(syscall.Stdin)
|
||||
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
migrationContext.CliPassword = string(bytePassword)
|
||||
}
|
||||
@ -295,26 +233,20 @@ func main() {
|
||||
migrationContext.SetMaxLagMillisecondsThrottleThreshold(*maxLagMillis)
|
||||
migrationContext.SetThrottleQuery(*throttleQuery)
|
||||
migrationContext.SetThrottleHTTP(*throttleHTTP)
|
||||
migrationContext.SetIgnoreHTTPErrors(*ignoreHTTPErrors)
|
||||
migrationContext.SetDefaultNumRetries(*defaultRetries)
|
||||
migrationContext.ApplyCredentials()
|
||||
if err := migrationContext.SetupTLS(); err != nil {
|
||||
migrationContext.Log.Fatale(err)
|
||||
}
|
||||
if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil {
|
||||
migrationContext.Log.Errore(err)
|
||||
}
|
||||
if err := migrationContext.SetExponentialBackoffMaxInterval(*exponentialBackoffMaxInterval); err != nil {
|
||||
migrationContext.Log.Errore(err)
|
||||
log.Errore(err)
|
||||
}
|
||||
|
||||
log.Infof("starting gh-ost %+v", AppVersion)
|
||||
acceptSignals(migrationContext)
|
||||
|
||||
migrator := logic.NewMigrator(migrationContext, AppVersion)
|
||||
if err := migrator.Migrate(); err != nil {
|
||||
migrator := logic.NewMigrator(migrationContext)
|
||||
err := migrator.Migrate()
|
||||
if err != nil {
|
||||
migrator.ExecOnFailureHook()
|
||||
migrationContext.Log.Fatale(err)
|
||||
log.Fatale(err)
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "# Done")
|
||||
fmt.Fprintf(os.Stdout, "# Done\n")
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -8,7 +8,6 @@ package logic
|
||||
import (
|
||||
gosql "database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -17,13 +16,12 @@ import (
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
"github.com/openark/golib/sqlutils"
|
||||
"github.com/outbrain/golib/log"
|
||||
"github.com/outbrain/golib/sqlutils"
|
||||
)
|
||||
|
||||
const (
|
||||
GhostChangelogTableComment = "gh-ost changelog"
|
||||
atomicCutOverMagicHint = "ghost-cut-over-sentry"
|
||||
atomicCutOverMagicHint = "ghost-cut-over-sentry"
|
||||
)
|
||||
|
||||
type dmlBuildResult struct {
|
||||
@ -48,7 +46,7 @@ func newDmlBuildResultError(err error) *dmlBuildResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Applier connects and writes the applier-server, which is the server where migration
|
||||
// Applier connects and writes the the applier-server, which is the server where migration
|
||||
// happens. This is typically the master, but could be a replica when `--test-on-replica` or
|
||||
// `--execute-on-replica` are given.
|
||||
// Applier is the one to actually write row data and apply binlog events onto the ghost table.
|
||||
@ -59,7 +57,6 @@ type Applier struct {
|
||||
singletonDB *gosql.DB
|
||||
migrationContext *base.MigrationContext
|
||||
finishedMigrating int64
|
||||
name string
|
||||
}
|
||||
|
||||
func NewApplier(migrationContext *base.MigrationContext) *Applier {
|
||||
@ -67,42 +64,40 @@ func NewApplier(migrationContext *base.MigrationContext) *Applier {
|
||||
connectionConfig: migrationContext.ApplierConnectionConfig,
|
||||
migrationContext: migrationContext,
|
||||
finishedMigrating: 0,
|
||||
name: "applier",
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Applier) InitDBConnections() (err error) {
|
||||
|
||||
applierUri := this.connectionConfig.GetDBUri(this.migrationContext.DatabaseName)
|
||||
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, applierUri); err != nil {
|
||||
return err
|
||||
}
|
||||
singletonApplierUri := fmt.Sprintf("%s&timeout=0", applierUri)
|
||||
singletonApplierUri := fmt.Sprintf("%s?timeout=0", applierUri)
|
||||
if this.singletonDB, _, err = mysql.GetDB(this.migrationContext.Uuid, singletonApplierUri); err != nil {
|
||||
return err
|
||||
}
|
||||
this.singletonDB.SetMaxOpenConns(1)
|
||||
version, err := base.ValidateConnection(this.db, this.connectionConfig, this.migrationContext, this.name)
|
||||
version, err := base.ValidateConnection(this.db, this.connectionConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := base.ValidateConnection(this.singletonDB, this.connectionConfig, this.migrationContext, this.name); err != nil {
|
||||
if _, err := base.ValidateConnection(this.singletonDB, this.connectionConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.ApplierMySQLVersion = version
|
||||
if err := this.validateAndReadTimeZone(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !this.migrationContext.AliyunRDS && !this.migrationContext.GoogleCloudPlatform && !this.migrationContext.AzureMySQL {
|
||||
if impliedKey, err := mysql.GetInstanceKey(this.db); err != nil {
|
||||
return err
|
||||
} else {
|
||||
this.connectionConfig.ImpliedKey = impliedKey
|
||||
}
|
||||
if impliedKey, err := mysql.GetInstanceKey(this.db); err != nil {
|
||||
return err
|
||||
} else {
|
||||
this.connectionConfig.ImpliedKey = impliedKey
|
||||
}
|
||||
if err := this.readTableColumns(); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Applier initiated on %+v, version %+v", this.connectionConfig.ImpliedKey, this.migrationContext.ApplierMySQLVersion)
|
||||
log.Infof("Applier initiated on %+v, version %+v", this.connectionConfig.ImpliedKey, this.migrationContext.ApplierMySQLVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -113,41 +108,14 @@ func (this *Applier) validateAndReadTimeZone() error {
|
||||
return err
|
||||
}
|
||||
|
||||
this.migrationContext.Log.Infof("will use time_zone='%s' on applier", this.migrationContext.ApplierTimeZone)
|
||||
log.Infof("will use time_zone='%s' on applier", this.migrationContext.ApplierTimeZone)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSqlModeQuery return a `sql_mode = ...` query, to be wrapped with a `set session` or `set global`,
|
||||
// based on gh-ost configuration:
|
||||
// - User may skip strict mode
|
||||
// - User may allow zero dats or zero in dates
|
||||
func (this *Applier) generateSqlModeQuery() string {
|
||||
sqlModeAddendum := []string{`NO_AUTO_VALUE_ON_ZERO`}
|
||||
if !this.migrationContext.SkipStrictMode {
|
||||
sqlModeAddendum = append(sqlModeAddendum, `STRICT_ALL_TABLES`)
|
||||
}
|
||||
sqlModeQuery := fmt.Sprintf("CONCAT(@@session.sql_mode, ',%s')", strings.Join(sqlModeAddendum, ","))
|
||||
if this.migrationContext.AllowZeroInDate {
|
||||
sqlModeQuery = fmt.Sprintf("REPLACE(REPLACE(%s, 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '')", sqlModeQuery)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("sql_mode = %s", sqlModeQuery)
|
||||
}
|
||||
|
||||
// generateInstantDDLQuery returns the SQL for this ALTER operation
|
||||
// with an INSTANT assertion (requires MySQL 8.0+)
|
||||
func (this *Applier) generateInstantDDLQuery() string {
|
||||
return fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`,
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
this.migrationContext.AlterStatementOptions,
|
||||
)
|
||||
}
|
||||
|
||||
// readTableColumns reads table columns on applier
|
||||
func (this *Applier) readTableColumns() (err error) {
|
||||
this.migrationContext.Log.Infof("Examining table structure on applier")
|
||||
this.migrationContext.OriginalTableColumnsOnApplier, _, err = mysql.GetTableColumns(this.db, this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName)
|
||||
log.Infof("Examining table structure on applier")
|
||||
this.migrationContext.OriginalTableColumnsOnApplier, err = mysql.GetTableColumns(this.db, this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -156,6 +124,7 @@ func (this *Applier) readTableColumns() (err error) {
|
||||
|
||||
// showTableStatus returns the output of `show table status like '...'` command
|
||||
func (this *Applier) showTableStatus(tableName string) (rowMap sqlutils.RowMap) {
|
||||
rowMap = nil
|
||||
query := fmt.Sprintf(`show /* gh-ost */ table status from %s like '%s'`, sql.EscapeName(this.migrationContext.DatabaseName), tableName)
|
||||
sqlutils.QueryRowsMap(this.db, query, func(m sqlutils.RowMap) error {
|
||||
rowMap = m
|
||||
@ -187,7 +156,7 @@ func (this *Applier) ValidateOrDropExistingTables() error {
|
||||
}
|
||||
}
|
||||
if len(this.migrationContext.GetOldTableName()) > mysql.MaxTableNameLength {
|
||||
this.migrationContext.Log.Fatalf("--timestamp-old-table defined, but resulting table name (%s) is too long (only %d characters allowed)", this.migrationContext.GetOldTableName(), mysql.MaxTableNameLength)
|
||||
log.Fatalf("--timestamp-old-table defined, but resulting table name (%s) is too long (only %d characters allowed)", this.migrationContext.GetOldTableName(), mysql.MaxTableNameLength)
|
||||
}
|
||||
|
||||
if this.tableExists(this.migrationContext.GetOldTableName()) {
|
||||
@ -197,27 +166,6 @@ func (this *Applier) ValidateOrDropExistingTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others).
|
||||
// If successful, the operation is only a meta-data change so a lot of time is saved!
|
||||
// The risk of attempting to instant DDL when not supported is that a metadata lock may be acquired.
|
||||
// This is minor, since gh-ost will eventually require a metadata lock anyway, but at the cut-over stage.
|
||||
// Instant operations include:
|
||||
// - Adding a column
|
||||
// - Dropping a column
|
||||
// - Dropping an index
|
||||
// - Extending a VARCHAR column
|
||||
// - Adding a virtual generated column
|
||||
// It is not reliable to parse the `alter` statement to determine if it is instant or not.
|
||||
// This is because the table might be in an older row format, or have some other incompatibility
|
||||
// that is difficult to identify.
|
||||
func (this *Applier) AttemptInstantDDL() error {
|
||||
query := this.generateInstantDDLQuery()
|
||||
this.migrationContext.Log.Infof("INSTANT DDL query is: %s", query)
|
||||
// We don't need a trx, because for instant DDL the SQL mode doesn't matter.
|
||||
_, err := this.db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateGhostTable creates the ghost table on the applier host
|
||||
func (this *Applier) CreateGhostTable() error {
|
||||
query := fmt.Sprintf(`create /* gh-ost */ table %s.%s like %s.%s`,
|
||||
@ -226,37 +174,15 @@ func (this *Applier) CreateGhostTable() error {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Creating ghost table %s.%s",
|
||||
log.Infof("Creating ghost table %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
)
|
||||
|
||||
err := func() error {
|
||||
tx, err := this.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
|
||||
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
|
||||
|
||||
if _, err := tx.Exec(sessionQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Ghost table created")
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
|
||||
// there's no need to commit; but let's do this the legit way anyway.
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
return err
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Ghost table created")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlterGhost applies `alter` statement on ghost table
|
||||
@ -264,58 +190,17 @@ func (this *Applier) AlterGhost() error {
|
||||
query := fmt.Sprintf(`alter /* gh-ost */ table %s.%s %s`,
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
this.migrationContext.AlterStatementOptions,
|
||||
this.migrationContext.AlterStatement,
|
||||
)
|
||||
this.migrationContext.Log.Infof("Altering ghost table %s.%s",
|
||||
log.Infof("Altering ghost table %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
)
|
||||
this.migrationContext.Log.Debugf("ALTER statement: %s", query)
|
||||
|
||||
err := func() error {
|
||||
tx, err := this.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
|
||||
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
|
||||
|
||||
if _, err := tx.Exec(sessionQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Ghost table altered")
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
|
||||
// there's no need to commit; but let's do this the legit way anyway.
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// AlterGhost applies `alter` statement on ghost table
|
||||
func (this *Applier) AlterGhostAutoIncrement() error {
|
||||
query := fmt.Sprintf(`alter /* gh-ost */ table %s.%s AUTO_INCREMENT=%d`,
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
this.migrationContext.OriginalTableAutoIncrement,
|
||||
)
|
||||
this.migrationContext.Log.Infof("Altering ghost table AUTO_INCREMENT value %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
)
|
||||
this.migrationContext.Log.Debugf("AUTO_INCREMENT ALTER statement: %s", query)
|
||||
log.Debugf("ALTER statement: %s", query)
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Ghost table AUTO_INCREMENT altered")
|
||||
log.Infof("Ghost table altered")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -325,25 +210,25 @@ func (this *Applier) CreateChangelogTable() error {
|
||||
return err
|
||||
}
|
||||
query := fmt.Sprintf(`create /* gh-ost */ table %s.%s (
|
||||
id bigint unsigned auto_increment,
|
||||
id bigint auto_increment,
|
||||
last_update timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
hint varchar(64) charset ascii not null,
|
||||
value varchar(4096) charset ascii not null,
|
||||
primary key(id),
|
||||
unique key hint_uidx(hint)
|
||||
) auto_increment=256 comment='%s'`,
|
||||
) auto_increment=256
|
||||
`,
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetChangelogTableName()),
|
||||
GhostChangelogTableComment,
|
||||
)
|
||||
this.migrationContext.Log.Infof("Creating changelog table %s.%s",
|
||||
log.Infof("Creating changelog table %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetChangelogTableName()),
|
||||
)
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Changelog table created")
|
||||
log.Infof("Changelog table created")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -353,14 +238,14 @@ func (this *Applier) dropTable(tableName string) error {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(tableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Dropping table %s.%s",
|
||||
log.Infof("Dropping table %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(tableName),
|
||||
)
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Table dropped")
|
||||
log.Infof("Table dropped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -403,7 +288,7 @@ func (this *Applier) WriteChangelog(hint, value string) (string, error) {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetChangelogTableName()),
|
||||
)
|
||||
_, err := sqlutils.ExecNoPrepare(this.db, query, explicitId, hint, value)
|
||||
_, err := sqlutils.Exec(this.db, query, explicitId, hint, value)
|
||||
return hint, err
|
||||
}
|
||||
|
||||
@ -427,7 +312,7 @@ func (this *Applier) InitiateHeartbeat() {
|
||||
if _, err := this.WriteChangelog("heartbeat", time.Now().Format(time.RFC3339Nano)); err != nil {
|
||||
numSuccessiveFailures++
|
||||
if numSuccessiveFailures > this.migrationContext.MaxRetries() {
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
}
|
||||
} else {
|
||||
numSuccessiveFailures = 0
|
||||
@ -436,9 +321,8 @@ func (this *Applier) InitiateHeartbeat() {
|
||||
}
|
||||
injectHeartbeat()
|
||||
|
||||
ticker := time.NewTicker(time.Duration(this.migrationContext.HeartbeatIntervalMilliseconds) * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
heartbeatTick := time.Tick(time.Duration(this.migrationContext.HeartbeatIntervalMilliseconds) * time.Millisecond)
|
||||
for range heartbeatTick {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
}
|
||||
@ -463,97 +347,62 @@ func (this *Applier) ExecuteThrottleQuery() (int64, error) {
|
||||
}
|
||||
var result int64
|
||||
if err := this.db.QueryRow(throttleQuery).Scan(&result); err != nil {
|
||||
return 0, this.migrationContext.Log.Errore(err)
|
||||
return 0, log.Errore(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// readMigrationMinValues returns the minimum values to be iterated on rowcopy
|
||||
func (this *Applier) readMigrationMinValues(tx *gosql.Tx, uniqueKey *sql.UniqueKey) error {
|
||||
this.migrationContext.Log.Debugf("Reading migration range according to key: %s", uniqueKey.Name)
|
||||
// ReadMigrationMinValues returns the minimum values to be iterated on rowcopy
|
||||
func (this *Applier) ReadMigrationMinValues(uniqueKey *sql.UniqueKey) error {
|
||||
log.Debugf("Reading migration range according to key: %s", uniqueKey.Name)
|
||||
query, err := sql.BuildUniqueKeyMinValuesPreparedQuery(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, &uniqueKey.Columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := tx.Query(query)
|
||||
rows, err := this.db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
this.migrationContext.MigrationRangeMinValues = sql.NewColumnValues(uniqueKey.Len())
|
||||
if err = rows.Scan(this.migrationContext.MigrationRangeMinValues.ValuesPointers...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
this.migrationContext.Log.Infof("Migration min values: [%s]", this.migrationContext.MigrationRangeMinValues)
|
||||
|
||||
return rows.Err()
|
||||
log.Infof("Migration min values: [%s]", this.migrationContext.MigrationRangeMinValues)
|
||||
return err
|
||||
}
|
||||
|
||||
// readMigrationMaxValues returns the maximum values to be iterated on rowcopy
|
||||
func (this *Applier) readMigrationMaxValues(tx *gosql.Tx, uniqueKey *sql.UniqueKey) error {
|
||||
this.migrationContext.Log.Debugf("Reading migration range according to key: %s", uniqueKey.Name)
|
||||
// ReadMigrationMaxValues returns the maximum values to be iterated on rowcopy
|
||||
func (this *Applier) ReadMigrationMaxValues(uniqueKey *sql.UniqueKey) error {
|
||||
log.Debugf("Reading migration range according to key: %s", uniqueKey.Name)
|
||||
query, err := sql.BuildUniqueKeyMaxValuesPreparedQuery(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, &uniqueKey.Columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := tx.Query(query)
|
||||
rows, err := this.db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
this.migrationContext.MigrationRangeMaxValues = sql.NewColumnValues(uniqueKey.Len())
|
||||
if err = rows.Scan(this.migrationContext.MigrationRangeMaxValues.ValuesPointers...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
this.migrationContext.Log.Infof("Migration max values: [%s]", this.migrationContext.MigrationRangeMaxValues)
|
||||
|
||||
return rows.Err()
|
||||
log.Infof("Migration max values: [%s]", this.migrationContext.MigrationRangeMaxValues)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadMigrationRangeValues reads min/max values that will be used for rowcopy.
|
||||
// Before read min/max, write a changelog state into the ghc table to avoid lost data in mysql two-phase commit.
|
||||
/*
|
||||
Detail description of the lost data in mysql two-phase commit issue by @Fanduzi:
|
||||
When using semi-sync and setting rpl_semi_sync_master_wait_point=AFTER_SYNC,
|
||||
if an INSERT statement is being committed but blocks due to an unmet ack count,
|
||||
the data inserted by the transaction is not visible to ReadMigrationRangeValues,
|
||||
so the copy of the existing data in the table does not include the new row inserted by the transaction.
|
||||
However, the binlog event for the transaction is already written to the binlog,
|
||||
so the addDMLEventsListener only captures the binlog event after the transaction,
|
||||
and thus the transaction's binlog event is not captured, resulting in data loss.
|
||||
|
||||
If write a changelog into ghc table before ReadMigrationRangeValues, and the transaction commit blocks
|
||||
because the ack is not met, then the changelog will not be able to write, so the ReadMigrationRangeValues
|
||||
will not be run. When the changelog writes successfully, the ReadMigrationRangeValues will read the
|
||||
newly inserted data, thus Avoiding data loss due to the above problem.
|
||||
*/
|
||||
// ReadMigrationRangeValues reads min/max values that will be used for rowcopy
|
||||
func (this *Applier) ReadMigrationRangeValues() error {
|
||||
if _, err := this.WriteChangelogState(string(ReadMigrationRangeValues)); err != nil {
|
||||
if err := this.ReadMigrationMinValues(this.migrationContext.UniqueKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := this.db.Begin()
|
||||
if err != nil {
|
||||
if err := this.ReadMigrationMaxValues(this.migrationContext.UniqueKey); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := this.readMigrationMinValues(tx, this.migrationContext.UniqueKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := this.readMigrationMaxValues(tx, this.migrationContext.UniqueKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalculateNextIterationRangeEndValues reads the next-iteration-range-end unique key values,
|
||||
@ -583,13 +432,10 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
|
||||
if err != nil {
|
||||
return hasFurtherRange, err
|
||||
}
|
||||
|
||||
rows, err := this.db.Query(query, explodedArgs...)
|
||||
if err != nil {
|
||||
return hasFurtherRange, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
iterationRangeMaxValues := sql.NewColumnValues(this.migrationContext.UniqueKey.Len())
|
||||
for rows.Next() {
|
||||
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
|
||||
@ -597,15 +443,12 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
|
||||
}
|
||||
hasFurtherRange = true
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return hasFurtherRange, err
|
||||
}
|
||||
if hasFurtherRange {
|
||||
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
|
||||
return hasFurtherRange, nil
|
||||
}
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Iteration complete: no further range to iterate")
|
||||
log.Debugf("Iteration complete: no further range to iterate")
|
||||
return hasFurtherRange, nil
|
||||
}
|
||||
|
||||
@ -637,11 +480,10 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
|
||||
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
|
||||
|
||||
sessionQuery := fmt.Sprintf(`SET
|
||||
SESSION time_zone = '%s',
|
||||
sql_mode = CONCAT(@@session.sql_mode, ',STRICT_ALL_TABLES')
|
||||
`, this.migrationContext.ApplierTimeZone)
|
||||
if _, err := tx.Exec(sessionQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -660,7 +502,7 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
|
||||
}
|
||||
rowsAffected, _ = sqlResult.RowsAffected()
|
||||
duration = time.Since(startTime)
|
||||
this.migrationContext.Log.Debugf(
|
||||
log.Debugf(
|
||||
"Issued INSERT on range: [%s]..[%s]; iteration: %d; chunk-size: %d",
|
||||
this.migrationContext.MigrationIterationRangeMinValues,
|
||||
this.migrationContext.MigrationIterationRangeMaxValues,
|
||||
@ -675,7 +517,7 @@ func (this *Applier) LockOriginalTable() error {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Locking %s.%s",
|
||||
log.Infof("Locking %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
@ -683,18 +525,18 @@ func (this *Applier) LockOriginalTable() error {
|
||||
if _, err := sqlutils.ExecNoPrepare(this.singletonDB, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Table locked")
|
||||
log.Infof("Table locked")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlockTables makes tea. No wait, it unlocks tables.
|
||||
func (this *Applier) UnlockTables() error {
|
||||
query := `unlock /* gh-ost */ tables`
|
||||
this.migrationContext.Log.Infof("Unlocking tables")
|
||||
log.Infof("Unlocking tables")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.singletonDB, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Tables unlocked")
|
||||
log.Infof("Tables unlocked")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -708,7 +550,7 @@ func (this *Applier) SwapTablesQuickAndBumpy() error {
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
sql.EscapeName(this.migrationContext.GetOldTableName()),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Renaming original table")
|
||||
log.Infof("Renaming original table")
|
||||
this.migrationContext.RenameTablesStartTime = time.Now()
|
||||
if _, err := sqlutils.ExecNoPrepare(this.singletonDB, query); err != nil {
|
||||
return err
|
||||
@ -718,13 +560,13 @@ func (this *Applier) SwapTablesQuickAndBumpy() error {
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Renaming ghost table")
|
||||
log.Infof("Renaming ghost table")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.RenameTablesEndTime = time.Now()
|
||||
|
||||
this.migrationContext.Log.Infof("Tables renamed")
|
||||
log.Infof("Tables renamed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -743,7 +585,7 @@ func (this *Applier) RenameTablesRollback() (renameError error) {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Renaming back both tables")
|
||||
log.Infof("Renaming back both tables")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err == nil {
|
||||
return nil
|
||||
}
|
||||
@ -754,7 +596,7 @@ func (this *Applier) RenameTablesRollback() (renameError error) {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Renaming back to ghost table")
|
||||
log.Infof("Renaming back to ghost table")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
renameError = err
|
||||
}
|
||||
@ -764,11 +606,11 @@ func (this *Applier) RenameTablesRollback() (renameError error) {
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Renaming back to original table")
|
||||
log.Infof("Renaming back to original table")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
renameError = err
|
||||
}
|
||||
return this.migrationContext.Log.Errore(renameError)
|
||||
return log.Errore(renameError)
|
||||
}
|
||||
|
||||
// StopSlaveIOThread is applicable with --test-on-replica; it stops the IO thread, duh.
|
||||
@ -776,44 +618,44 @@ func (this *Applier) RenameTablesRollback() (renameError error) {
|
||||
// and have them written to the binary log, so that we can then read them via streamer.
|
||||
func (this *Applier) StopSlaveIOThread() error {
|
||||
query := `stop /* gh-ost */ slave io_thread`
|
||||
this.migrationContext.Log.Infof("Stopping replication IO thread")
|
||||
log.Infof("Stopping replication IO thread")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Replication IO thread stopped")
|
||||
log.Infof("Replication IO thread stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSlaveIOThread is applicable with --test-on-replica
|
||||
func (this *Applier) StartSlaveIOThread() error {
|
||||
query := `start /* gh-ost */ slave io_thread`
|
||||
this.migrationContext.Log.Infof("Starting replication IO thread")
|
||||
log.Infof("Starting replication IO thread")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Replication IO thread started")
|
||||
log.Infof("Replication IO thread started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSlaveSQLThread is applicable with --test-on-replica
|
||||
func (this *Applier) StopSlaveSQLThread() error {
|
||||
query := `stop /* gh-ost */ slave sql_thread`
|
||||
this.migrationContext.Log.Infof("Verifying SQL thread is stopped")
|
||||
log.Infof("Verifying SQL thread is stopped")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("SQL thread stopped")
|
||||
log.Infof("SQL thread stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSlaveSQLThread is applicable with --test-on-replica
|
||||
func (this *Applier) StartSlaveSQLThread() error {
|
||||
query := `start /* gh-ost */ slave sql_thread`
|
||||
this.migrationContext.Log.Infof("Verifying SQL thread is running")
|
||||
log.Infof("Verifying SQL thread is running")
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("SQL thread started")
|
||||
log.Infof("SQL thread started")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -830,7 +672,7 @@ func (this *Applier) StopReplication() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Replication IO thread at %+v. SQL thread is at %+v", *readBinlogCoordinates, *executeBinlogCoordinates)
|
||||
log.Infof("Replication IO thread at %+v. SQL thread is at %+v", *readBinlogCoordinates, *executeBinlogCoordinates)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -842,7 +684,7 @@ func (this *Applier) StartReplication() error {
|
||||
if err := this.StartSlaveSQLThread(); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Replication started")
|
||||
log.Infof("Replication started")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -856,7 +698,7 @@ func (this *Applier) ExpectUsedLock(sessionId int64) error {
|
||||
var result int64
|
||||
query := `select is_used_lock(?)`
|
||||
lockName := this.GetSessionLockName(sessionId)
|
||||
this.migrationContext.Log.Infof("Checking session lock: %s", lockName)
|
||||
log.Infof("Checking session lock: %s", lockName)
|
||||
if err := this.db.QueryRow(query, lockName).Scan(&result); err != nil || result != sessionId {
|
||||
return fmt.Errorf("Session lock %s expected to be found but wasn't", lockName)
|
||||
}
|
||||
@ -891,7 +733,7 @@ func (this *Applier) ExpectProcess(sessionId int64, stateHint, infoHint string)
|
||||
// DropAtomicCutOverSentryTableIfExists checks if the "old" table name
|
||||
// happens to be a cut-over magic table; if so, it drops it.
|
||||
func (this *Applier) DropAtomicCutOverSentryTableIfExists() error {
|
||||
this.migrationContext.Log.Infof("Looking for magic cut-over table")
|
||||
log.Infof("Looking for magic cut-over table")
|
||||
tableName := this.migrationContext.GetOldTableName()
|
||||
rowMap := this.showTableStatus(tableName)
|
||||
if rowMap == nil {
|
||||
@ -901,7 +743,7 @@ func (this *Applier) DropAtomicCutOverSentryTableIfExists() error {
|
||||
if rowMap["Comment"].String != atomicCutOverMagicHint {
|
||||
return fmt.Errorf("Expected magic comment on %s, did not find it", tableName)
|
||||
}
|
||||
this.migrationContext.Log.Infof("Dropping magic cut-over table")
|
||||
log.Infof("Dropping magic cut-over table")
|
||||
return this.dropTable(tableName)
|
||||
}
|
||||
|
||||
@ -921,14 +763,14 @@ func (this *Applier) CreateAtomicCutOverSentryTable() error {
|
||||
this.migrationContext.TableEngine,
|
||||
atomicCutOverMagicHint,
|
||||
)
|
||||
this.migrationContext.Log.Infof("Creating magic cut-over table %s.%s",
|
||||
log.Infof("Creating magic cut-over table %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(tableName),
|
||||
)
|
||||
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Magic cut-over table created")
|
||||
log.Infof("Magic cut-over table created")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -945,7 +787,6 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
tableLocked <- fmt.Errorf("Unexpected error in AtomicCutOverMagicLock(), injected to release blocking channel reads")
|
||||
tableUnlocked <- fmt.Errorf("Unexpected error in AtomicCutOverMagicLock(), injected to release blocking channel reads")
|
||||
tx.Rollback()
|
||||
this.DropAtomicCutOverSentryTableIfExists()
|
||||
}()
|
||||
|
||||
var sessionId int64
|
||||
@ -958,7 +799,7 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
lockResult := 0
|
||||
query := `select get_lock(?, 0)`
|
||||
lockName := this.GetSessionLockName(sessionId)
|
||||
this.migrationContext.Log.Infof("Grabbing voluntary lock: %s", lockName)
|
||||
log.Infof("Grabbing voluntary lock: %s", lockName)
|
||||
if err := tx.QueryRow(query, lockName).Scan(&lockResult); err != nil || lockResult != 1 {
|
||||
err := fmt.Errorf("Unable to acquire lock %s", lockName)
|
||||
tableLocked <- err
|
||||
@ -966,7 +807,7 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
}
|
||||
|
||||
tableLockTimeoutSeconds := this.migrationContext.CutOverLockTimeoutSeconds * 2
|
||||
this.migrationContext.Log.Infof("Setting LOCK timeout as %d seconds", tableLockTimeoutSeconds)
|
||||
log.Infof("Setting LOCK timeout as %d seconds", tableLockTimeoutSeconds)
|
||||
query = fmt.Sprintf(`set session lock_wait_timeout:=%d`, tableLockTimeoutSeconds)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
tableLocked <- err
|
||||
@ -984,7 +825,7 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetOldTableName()),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Locking %s.%s, %s.%s",
|
||||
log.Infof("Locking %s.%s, %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
@ -995,7 +836,7 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
tableLocked <- err
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Tables locked")
|
||||
log.Infof("Tables locked")
|
||||
tableLocked <- nil // No error.
|
||||
|
||||
// From this point on, we are committed to UNLOCK TABLES. No matter what happens,
|
||||
@ -1004,23 +845,22 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
// The cut-over phase will proceed to apply remaining backlog onto ghost table,
|
||||
// and issue RENAME. We wait here until told to proceed.
|
||||
<-okToUnlockTable
|
||||
this.migrationContext.Log.Infof("Will now proceed to drop magic table and unlock tables")
|
||||
log.Infof("Will now proceed to drop magic table and unlock tables")
|
||||
|
||||
// The magic table is here because we locked it. And we are the only ones allowed to drop it.
|
||||
// And in fact, we will:
|
||||
this.migrationContext.Log.Infof("Dropping magic cut-over table")
|
||||
log.Infof("Dropping magic cut-over table")
|
||||
query = fmt.Sprintf(`drop /* gh-ost */ table if exists %s.%s`,
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.GetOldTableName()),
|
||||
)
|
||||
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
this.migrationContext.Log.Errore(err)
|
||||
log.Errore(err)
|
||||
// We DO NOT return here because we must `UNLOCK TABLES`!
|
||||
}
|
||||
|
||||
// Tables still locked
|
||||
this.migrationContext.Log.Infof("Releasing lock from %s.%s, %s.%s",
|
||||
log.Infof("Releasing lock from %s.%s, %s.%s",
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
@ -1029,9 +869,9 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
|
||||
query = `unlock tables`
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
tableUnlocked <- err
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
}
|
||||
this.migrationContext.Log.Infof("Tables unlocked")
|
||||
log.Infof("Tables unlocked")
|
||||
tableUnlocked <- nil
|
||||
return nil
|
||||
}
|
||||
@ -1053,7 +893,7 @@ func (this *Applier) AtomicCutoverRename(sessionIdChan chan int64, tablesRenamed
|
||||
}
|
||||
sessionIdChan <- sessionId
|
||||
|
||||
this.migrationContext.Log.Infof("Setting RENAME timeout as %d seconds", this.migrationContext.CutOverLockTimeoutSeconds)
|
||||
log.Infof("Setting RENAME timeout as %d seconds", this.migrationContext.CutOverLockTimeoutSeconds)
|
||||
query := fmt.Sprintf(`set session lock_wait_timeout:=%d`, this.migrationContext.CutOverLockTimeoutSeconds)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
return err
|
||||
@ -1069,13 +909,13 @@ func (this *Applier) AtomicCutoverRename(sessionIdChan chan int64, tablesRenamed
|
||||
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||
)
|
||||
this.migrationContext.Log.Infof("Issuing and expecting this to block: %s", query)
|
||||
log.Infof("Issuing and expecting this to block: %s", query)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
tablesRenamed <- err
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
}
|
||||
tablesRenamed <- nil
|
||||
this.migrationContext.Log.Infof("Tables renamed")
|
||||
log.Infof("Tables renamed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1135,8 +975,62 @@ func (this *Applier) buildDMLEventQuery(dmlEvent *binlog.BinlogDMLEvent) (result
|
||||
return append(results, newDmlBuildResultError(fmt.Errorf("Unknown dml event type: %+v", dmlEvent.DML)))
|
||||
}
|
||||
|
||||
// ApplyDMLEventQuery writes an entry to the ghost table, in response to an intercepted
|
||||
// original-table binlog event
|
||||
func (this *Applier) ApplyDMLEventQuery(dmlEvent *binlog.BinlogDMLEvent) error {
|
||||
for _, buildResult := range this.buildDMLEventQuery(dmlEvent) {
|
||||
if buildResult.err != nil {
|
||||
return buildResult.err
|
||||
}
|
||||
// TODO The below is in preparation for transactional writes on the ghost tables.
|
||||
// Such writes would be, for example:
|
||||
// - prepended with sql_mode setup
|
||||
// - prepended with time zone setup
|
||||
// - prepended with SET SQL_LOG_BIN=0
|
||||
// - prepended with SET FK_CHECKS=0
|
||||
// etc.
|
||||
//
|
||||
// a known problem: https://github.com/golang/go/issues/9373 -- bitint unsigned values, not supported in database/sql
|
||||
// is solved by silently converting unsigned bigints to string values.
|
||||
//
|
||||
|
||||
err := func() error {
|
||||
tx, err := this.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionQuery := `SET
|
||||
SESSION time_zone = '+00:00',
|
||||
sql_mode = CONCAT(@@session.sql_mode, ',STRICT_ALL_TABLES')
|
||||
`
|
||||
if _, err := tx.Exec(sessionQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(buildResult.query, buildResult.args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s; query=%s; args=%+v", err.Error(), buildResult.query, buildResult.args)
|
||||
return log.Errore(err)
|
||||
}
|
||||
// no error
|
||||
atomic.AddInt64(&this.migrationContext.TotalDMLEventsApplied, 1)
|
||||
if this.migrationContext.CountTableRows {
|
||||
atomic.AddInt64(&this.migrationContext.RowsDeltaEstimate, buildResult.rowsDelta)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyDMLEventQueries applies multiple DML queries onto the _ghost_ table
|
||||
func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent)) error {
|
||||
|
||||
var totalDelta int64
|
||||
|
||||
err := func() error {
|
||||
@ -1150,9 +1044,10 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
|
||||
return err
|
||||
}
|
||||
|
||||
sessionQuery := "SET SESSION time_zone = '+00:00'"
|
||||
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
|
||||
|
||||
sessionQuery := `SET
|
||||
SESSION time_zone = '+00:00',
|
||||
sql_mode = CONCAT(@@session.sql_mode, ',STRICT_ALL_TABLES')
|
||||
`
|
||||
if _, err := tx.Exec(sessionQuery); err != nil {
|
||||
return rollback(err)
|
||||
}
|
||||
@ -1161,20 +1056,11 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
|
||||
if buildResult.err != nil {
|
||||
return rollback(buildResult.err)
|
||||
}
|
||||
result, err := tx.Exec(buildResult.query, buildResult.args...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w; query=%s; args=%+v", err, buildResult.query, buildResult.args)
|
||||
if _, err := tx.Exec(buildResult.query, buildResult.args...); err != nil {
|
||||
err = fmt.Errorf("%s; query=%s; args=%+v", err.Error(), buildResult.query, buildResult.args)
|
||||
return rollback(err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Warningf("error getting rows affected from DML event query: %s. i'm going to assume that the DML affected a single row, but this may result in inaccurate statistics", err)
|
||||
rowsAffected = 1
|
||||
}
|
||||
// each DML is either a single insert (delta +1), update (delta +0) or delete (delta -1).
|
||||
// multiplying by the rows actually affected (either 0 or 1) will give an accurate row delta for this DML event
|
||||
totalDelta += buildResult.rowsDelta * rowsAffected
|
||||
totalDelta += buildResult.rowsDelta
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@ -1184,19 +1070,19 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
}
|
||||
// no error
|
||||
atomic.AddInt64(&this.migrationContext.TotalDMLEventsApplied, int64(len(dmlEvents)))
|
||||
if this.migrationContext.CountTableRows {
|
||||
atomic.AddInt64(&this.migrationContext.RowsDeltaEstimate, totalDelta)
|
||||
}
|
||||
this.migrationContext.Log.Debugf("ApplyDMLEventQueries() applied %d events in one transaction", len(dmlEvents))
|
||||
log.Debugf("ApplyDMLEventQueries() applied %d events in one transaction", len(dmlEvents))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Applier) Teardown() {
|
||||
this.migrationContext.Log.Debugf("Tearing down...")
|
||||
log.Debugf("Tearing down...")
|
||||
this.db.Close()
|
||||
this.singletonDB.Close()
|
||||
atomic.StoreInt64(&this.finishedMigrating, 1)
|
||||
|
@ -1,185 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
test "github.com/openark/golib/tests"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/github/gh-ost/go/binlog"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
)
|
||||
|
||||
func TestApplierGenerateSqlModeQuery(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
applier := NewApplier(migrationContext)
|
||||
|
||||
{
|
||||
test.S(t).ExpectEquals(
|
||||
applier.generateSqlModeQuery(),
|
||||
`sql_mode = CONCAT(@@session.sql_mode, ',NO_AUTO_VALUE_ON_ZERO,STRICT_ALL_TABLES')`,
|
||||
)
|
||||
}
|
||||
{
|
||||
migrationContext.SkipStrictMode = true
|
||||
migrationContext.AllowZeroInDate = false
|
||||
test.S(t).ExpectEquals(
|
||||
applier.generateSqlModeQuery(),
|
||||
`sql_mode = CONCAT(@@session.sql_mode, ',NO_AUTO_VALUE_ON_ZERO')`,
|
||||
)
|
||||
}
|
||||
{
|
||||
migrationContext.SkipStrictMode = false
|
||||
migrationContext.AllowZeroInDate = true
|
||||
test.S(t).ExpectEquals(
|
||||
applier.generateSqlModeQuery(),
|
||||
`sql_mode = REPLACE(REPLACE(CONCAT(@@session.sql_mode, ',NO_AUTO_VALUE_ON_ZERO,STRICT_ALL_TABLES'), 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '')`,
|
||||
)
|
||||
}
|
||||
{
|
||||
migrationContext.SkipStrictMode = true
|
||||
migrationContext.AllowZeroInDate = true
|
||||
test.S(t).ExpectEquals(
|
||||
applier.generateSqlModeQuery(),
|
||||
`sql_mode = REPLACE(REPLACE(CONCAT(@@session.sql_mode, ',NO_AUTO_VALUE_ON_ZERO'), 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '')`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplierUpdateModifiesUniqueKeyColumns(t *testing.T) {
|
||||
columns := sql.NewColumnList([]string{"id", "item_id"})
|
||||
columnValues := sql.ToColumnValues([]interface{}{123456, 42})
|
||||
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrationContext.OriginalTableColumns = columns
|
||||
migrationContext.UniqueKey = &sql.UniqueKey{
|
||||
Name: t.Name(),
|
||||
Columns: *columns,
|
||||
}
|
||||
|
||||
applier := NewApplier(migrationContext)
|
||||
|
||||
t.Run("unmodified", func(t *testing.T) {
|
||||
modifiedColumn, isModified := applier.updateModifiesUniqueKeyColumns(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.UpdateDML,
|
||||
NewColumnValues: columnValues,
|
||||
WhereColumnValues: columnValues,
|
||||
})
|
||||
test.S(t).ExpectEquals(modifiedColumn, "")
|
||||
test.S(t).ExpectFalse(isModified)
|
||||
})
|
||||
|
||||
t.Run("modified", func(t *testing.T) {
|
||||
modifiedColumn, isModified := applier.updateModifiesUniqueKeyColumns(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.UpdateDML,
|
||||
NewColumnValues: sql.ToColumnValues([]interface{}{123456, 24}),
|
||||
WhereColumnValues: columnValues,
|
||||
})
|
||||
test.S(t).ExpectEquals(modifiedColumn, "item_id")
|
||||
test.S(t).ExpectTrue(isModified)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplierBuildDMLEventQuery(t *testing.T) {
|
||||
columns := sql.NewColumnList([]string{"id", "item_id"})
|
||||
columnValues := sql.ToColumnValues([]interface{}{123456, 42})
|
||||
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrationContext.OriginalTableName = "test"
|
||||
migrationContext.OriginalTableColumns = columns
|
||||
migrationContext.SharedColumns = columns
|
||||
migrationContext.MappedSharedColumns = columns
|
||||
migrationContext.UniqueKey = &sql.UniqueKey{
|
||||
Name: t.Name(),
|
||||
Columns: *columns,
|
||||
}
|
||||
|
||||
applier := NewApplier(migrationContext)
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
binlogEvent := &binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.DeleteDML,
|
||||
WhereColumnValues: columnValues,
|
||||
}
|
||||
|
||||
res := applier.buildDMLEventQuery(binlogEvent)
|
||||
test.S(t).ExpectEquals(len(res), 1)
|
||||
test.S(t).ExpectNil(res[0].err)
|
||||
test.S(t).ExpectEquals(strings.TrimSpace(res[0].query),
|
||||
`delete /* gh-ost `+"`test`.`_test_gho`"+` */
|
||||
from
|
||||
`+"`test`.`_test_gho`"+`
|
||||
where
|
||||
((`+"`id`"+` = ?) and (`+"`item_id`"+` = ?))`)
|
||||
|
||||
test.S(t).ExpectEquals(len(res[0].args), 2)
|
||||
test.S(t).ExpectEquals(res[0].args[0], 123456)
|
||||
test.S(t).ExpectEquals(res[0].args[1], 42)
|
||||
})
|
||||
|
||||
t.Run("insert", func(t *testing.T) {
|
||||
binlogEvent := &binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}
|
||||
res := applier.buildDMLEventQuery(binlogEvent)
|
||||
test.S(t).ExpectEquals(len(res), 1)
|
||||
test.S(t).ExpectNil(res[0].err)
|
||||
test.S(t).ExpectEquals(strings.TrimSpace(res[0].query),
|
||||
`replace /* gh-ost `+"`test`.`_test_gho`"+` */ into
|
||||
`+"`test`.`_test_gho`"+`
|
||||
`+"(`id`, `item_id`)"+`
|
||||
values
|
||||
(?, ?)`)
|
||||
test.S(t).ExpectEquals(len(res[0].args), 2)
|
||||
test.S(t).ExpectEquals(res[0].args[0], 123456)
|
||||
test.S(t).ExpectEquals(res[0].args[1], 42)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
binlogEvent := &binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.UpdateDML,
|
||||
NewColumnValues: columnValues,
|
||||
WhereColumnValues: columnValues,
|
||||
}
|
||||
res := applier.buildDMLEventQuery(binlogEvent)
|
||||
test.S(t).ExpectEquals(len(res), 1)
|
||||
test.S(t).ExpectNil(res[0].err)
|
||||
test.S(t).ExpectEquals(strings.TrimSpace(res[0].query),
|
||||
`update /* gh-ost `+"`test`.`_test_gho`"+` */
|
||||
`+"`test`.`_test_gho`"+`
|
||||
set
|
||||
`+"`id`"+`=?, `+"`item_id`"+`=?
|
||||
where
|
||||
((`+"`id`"+` = ?) and (`+"`item_id`"+` = ?))`)
|
||||
test.S(t).ExpectEquals(len(res[0].args), 4)
|
||||
test.S(t).ExpectEquals(res[0].args[0], 123456)
|
||||
test.S(t).ExpectEquals(res[0].args[1], 42)
|
||||
test.S(t).ExpectEquals(res[0].args[2], 123456)
|
||||
test.S(t).ExpectEquals(res[0].args[3], 42)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplierInstantDDL(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrationContext.DatabaseName = "test"
|
||||
migrationContext.OriginalTableName = "mytable"
|
||||
migrationContext.AlterStatementOptions = "ADD INDEX (foo)"
|
||||
applier := NewApplier(migrationContext)
|
||||
|
||||
t.Run("instantDDLstmt", func(t *testing.T) {
|
||||
stmt := applier.generateInstantDDLQuery()
|
||||
test.S(t).ExpectEquals(stmt, "ALTER /* gh-ost */ TABLE `test`.`mytable` ADD INDEX (foo), ALGORITHM=INSTANT")
|
||||
})
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
/*
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -7,14 +8,13 @@ package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/openark/golib/log"
|
||||
"github.com/outbrain/golib/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -35,16 +35,18 @@ const (
|
||||
|
||||
type HooksExecutor struct {
|
||||
migrationContext *base.MigrationContext
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewHooksExecutor(migrationContext *base.MigrationContext) *HooksExecutor {
|
||||
return &HooksExecutor{
|
||||
migrationContext: migrationContext,
|
||||
writer: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *HooksExecutor) initHooks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) []string {
|
||||
env := os.Environ()
|
||||
env = append(env, fmt.Sprintf("GH_OST_DATABASE_NAME=%s", this.migrationContext.DatabaseName))
|
||||
@ -61,27 +63,22 @@ func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) [
|
||||
env = append(env, fmt.Sprintf("GH_OST_MIGRATED_HOST=%s", this.migrationContext.GetApplierHostname()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_INSPECTED_HOST=%s", this.migrationContext.GetInspectorHostname()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_EXECUTING_HOST=%s", this.migrationContext.Hostname))
|
||||
env = append(env, fmt.Sprintf("GH_OST_INSPECTED_LAG=%f", this.migrationContext.GetCurrentLagDuration().Seconds()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_HEARTBEAT_LAG=%f", this.migrationContext.TimeSinceLastHeartbeatOnChangelog().Seconds()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_PROGRESS=%f", this.migrationContext.GetProgressPct()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_ETA_SECONDS=%d", this.migrationContext.GetETASeconds()))
|
||||
env = append(env, fmt.Sprintf("GH_OST_HOOKS_HINT=%s", this.migrationContext.HooksHintMessage))
|
||||
env = append(env, fmt.Sprintf("GH_OST_HOOKS_HINT_OWNER=%s", this.migrationContext.HooksHintOwner))
|
||||
env = append(env, fmt.Sprintf("GH_OST_HOOKS_HINT_TOKEN=%s", this.migrationContext.HooksHintToken))
|
||||
env = append(env, fmt.Sprintf("GH_OST_DRY_RUN=%t", this.migrationContext.Noop))
|
||||
|
||||
env = append(env, extraVariables...)
|
||||
for _, variable := range extraVariables {
|
||||
env = append(env, variable)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// executeHook executes a command, and sets relevant environment variables
|
||||
// combined output & error are printed to the configured writer.
|
||||
// combined output & error are printed to gh-ost's standard error.
|
||||
func (this *HooksExecutor) executeHook(hook string, extraVariables ...string) error {
|
||||
cmd := exec.Command(hook)
|
||||
cmd.Env = this.applyEnvironmentVariables(extraVariables...)
|
||||
|
||||
combinedOutput, err := cmd.CombinedOutput()
|
||||
fmt.Fprintln(this.writer, string(combinedOutput))
|
||||
fmt.Fprintln(os.Stderr, string(combinedOutput))
|
||||
return log.Errore(err)
|
||||
}
|
||||
|
||||
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/openark/golib/tests"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
)
|
||||
|
||||
func TestHooksExecutorExecuteHooks(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrationContext.AlterStatement = "ENGINE=InnoDB"
|
||||
migrationContext.DatabaseName = "test"
|
||||
migrationContext.Hostname = "test.example.com"
|
||||
migrationContext.OriginalTableName = "tablename"
|
||||
migrationContext.RowsDeltaEstimate = 1
|
||||
migrationContext.RowsEstimate = 122
|
||||
migrationContext.TotalRowsCopied = 123456
|
||||
migrationContext.SetETADuration(time.Minute)
|
||||
migrationContext.SetProgressPct(50)
|
||||
hooksExecutor := NewHooksExecutor(migrationContext)
|
||||
|
||||
writeTmpHookFunc := func(testName, hookName, script string) (path string, err error) {
|
||||
if path, err = os.MkdirTemp("", testName); err != nil {
|
||||
return path, err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(path, hookName), []byte(script), 0777)
|
||||
return path, err
|
||||
}
|
||||
|
||||
t.Run("does-not-exist", func(t *testing.T) {
|
||||
migrationContext.HooksPath = "/does/not/exist"
|
||||
tests.S(t).ExpectNil(hooksExecutor.executeHooks("test-hook"))
|
||||
})
|
||||
|
||||
t.Run("failed", func(t *testing.T) {
|
||||
var err error
|
||||
if migrationContext.HooksPath, err = writeTmpHookFunc(
|
||||
"TestHooksExecutorExecuteHooks-failed",
|
||||
"failed-hook",
|
||||
"#!/bin/sh\nexit 1",
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(migrationContext.HooksPath)
|
||||
tests.S(t).ExpectNotNil(hooksExecutor.executeHooks("failed-hook"))
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var err error
|
||||
if migrationContext.HooksPath, err = writeTmpHookFunc(
|
||||
"TestHooksExecutorExecuteHooks-success",
|
||||
"success-hook",
|
||||
"#!/bin/sh\nenv",
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(migrationContext.HooksPath)
|
||||
|
||||
var buf bytes.Buffer
|
||||
hooksExecutor.writer = &buf
|
||||
tests.S(t).ExpectNil(hooksExecutor.executeHooks("success-hook", "TEST="+t.Name()))
|
||||
|
||||
scanner := bufio.NewScanner(&buf)
|
||||
for scanner.Scan() {
|
||||
split := strings.SplitN(scanner.Text(), "=", 2)
|
||||
switch split[0] {
|
||||
case "GH_OST_COPIED_ROWS":
|
||||
copiedRows, _ := strconv.ParseInt(split[1], 10, 64)
|
||||
tests.S(t).ExpectEquals(copiedRows, migrationContext.TotalRowsCopied)
|
||||
case "GH_OST_DATABASE_NAME":
|
||||
tests.S(t).ExpectEquals(split[1], migrationContext.DatabaseName)
|
||||
case "GH_OST_DDL":
|
||||
tests.S(t).ExpectEquals(split[1], migrationContext.AlterStatement)
|
||||
case "GH_OST_DRY_RUN":
|
||||
tests.S(t).ExpectEquals(split[1], "false")
|
||||
case "GH_OST_ESTIMATED_ROWS":
|
||||
estimatedRows, _ := strconv.ParseInt(split[1], 10, 64)
|
||||
tests.S(t).ExpectEquals(estimatedRows, int64(123))
|
||||
case "GH_OST_ETA_SECONDS":
|
||||
etaSeconds, _ := strconv.ParseInt(split[1], 10, 64)
|
||||
tests.S(t).ExpectEquals(etaSeconds, int64(60))
|
||||
case "GH_OST_EXECUTING_HOST":
|
||||
tests.S(t).ExpectEquals(split[1], migrationContext.Hostname)
|
||||
case "GH_OST_GHOST_TABLE_NAME":
|
||||
tests.S(t).ExpectEquals(split[1], fmt.Sprintf("_%s_gho", migrationContext.OriginalTableName))
|
||||
case "GH_OST_OLD_TABLE_NAME":
|
||||
tests.S(t).ExpectEquals(split[1], fmt.Sprintf("_%s_del", migrationContext.OriginalTableName))
|
||||
case "GH_OST_PROGRESS":
|
||||
progress, _ := strconv.ParseFloat(split[1], 64)
|
||||
tests.S(t).ExpectEquals(progress, 50.0)
|
||||
case "GH_OST_TABLE_NAME":
|
||||
tests.S(t).ExpectEquals(split[1], migrationContext.OriginalTableName)
|
||||
case "TEST":
|
||||
tests.S(t).ExpectEquals(split[1], t.Name())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
gosql "database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@ -19,7 +17,8 @@ import (
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
|
||||
"github.com/openark/golib/sqlutils"
|
||||
"github.com/outbrain/golib/log"
|
||||
"github.com/outbrain/golib/sqlutils"
|
||||
)
|
||||
|
||||
const startSlavePostWaitMilliseconds = 500 * time.Millisecond
|
||||
@ -31,14 +30,12 @@ type Inspector struct {
|
||||
db *gosql.DB
|
||||
informationSchemaDb *gosql.DB
|
||||
migrationContext *base.MigrationContext
|
||||
name string
|
||||
}
|
||||
|
||||
func NewInspector(migrationContext *base.MigrationContext) *Inspector {
|
||||
return &Inspector{
|
||||
connectionConfig: migrationContext.InspectorConnectionConfig,
|
||||
migrationContext: migrationContext,
|
||||
name: "inspector",
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,12 +53,10 @@ func (this *Inspector) InitDBConnections() (err error) {
|
||||
if err := this.validateConnection(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !this.migrationContext.AliyunRDS && !this.migrationContext.GoogleCloudPlatform && !this.migrationContext.AzureMySQL {
|
||||
if impliedKey, err := mysql.GetInstanceKey(this.db); err != nil {
|
||||
return err
|
||||
} else {
|
||||
this.connectionConfig.ImpliedKey = impliedKey
|
||||
}
|
||||
if impliedKey, err := mysql.GetInstanceKey(this.db); err != nil {
|
||||
return err
|
||||
} else {
|
||||
this.connectionConfig.ImpliedKey = impliedKey
|
||||
}
|
||||
if err := this.validateGrants(); err != nil {
|
||||
return err
|
||||
@ -72,7 +67,7 @@ func (this *Inspector) InitDBConnections() (err error) {
|
||||
if err := this.applyBinlogFormat(); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Inspector initiated on %+v, version %+v", this.connectionConfig.ImpliedKey, this.migrationContext.InspectorMySQLVersion)
|
||||
log.Infof("Inspector initiated on %+v, version %+v", this.connectionConfig.ImpliedKey, this.migrationContext.InspectorMySQLVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -92,28 +87,24 @@ func (this *Inspector) ValidateOriginalTable() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Inspector) InspectTableColumnsAndUniqueKeys(tableName string) (columns *sql.ColumnList, virtualColumns *sql.ColumnList, uniqueKeys [](*sql.UniqueKey), err error) {
|
||||
func (this *Inspector) InspectTableColumnsAndUniqueKeys(tableName string) (columns *sql.ColumnList, uniqueKeys [](*sql.UniqueKey), err error) {
|
||||
uniqueKeys, err = this.getCandidateUniqueKeys(tableName)
|
||||
if err != nil {
|
||||
return columns, virtualColumns, uniqueKeys, err
|
||||
return columns, uniqueKeys, err
|
||||
}
|
||||
if len(uniqueKeys) == 0 {
|
||||
return columns, virtualColumns, uniqueKeys, fmt.Errorf("No PRIMARY nor UNIQUE key found in table! Bailing out")
|
||||
return columns, uniqueKeys, fmt.Errorf("No PRIMARY nor UNIQUE key found in table! Bailing out")
|
||||
}
|
||||
columns, virtualColumns, err = mysql.GetTableColumns(this.db, this.migrationContext.DatabaseName, tableName)
|
||||
columns, err = mysql.GetTableColumns(this.db, this.migrationContext.DatabaseName, tableName)
|
||||
if err != nil {
|
||||
return columns, virtualColumns, uniqueKeys, err
|
||||
return columns, uniqueKeys, err
|
||||
}
|
||||
|
||||
return columns, virtualColumns, uniqueKeys, nil
|
||||
return columns, uniqueKeys, nil
|
||||
}
|
||||
|
||||
func (this *Inspector) InspectOriginalTable() (err error) {
|
||||
this.migrationContext.OriginalTableColumns, this.migrationContext.OriginalTableVirtualColumns, this.migrationContext.OriginalTableUniqueKeys, err = this.InspectTableColumnsAndUniqueKeys(this.migrationContext.OriginalTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.OriginalTableAutoIncrement, err = this.getAutoIncrementValue(this.migrationContext.OriginalTableName)
|
||||
this.migrationContext.OriginalTableColumns, this.migrationContext.OriginalTableUniqueKeys, err = this.InspectTableColumnsAndUniqueKeys(this.migrationContext.OriginalTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -129,11 +120,14 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
|
||||
return fmt.Errorf("It seems like table structure is not identical between master and replica. This scenario is not supported.")
|
||||
}
|
||||
|
||||
this.migrationContext.GhostTableColumns, this.migrationContext.GhostTableVirtualColumns, this.migrationContext.GhostTableUniqueKeys, err = this.InspectTableColumnsAndUniqueKeys(this.migrationContext.GetGhostTableName())
|
||||
this.migrationContext.GhostTableColumns, this.migrationContext.GhostTableUniqueKeys, err = this.InspectTableColumnsAndUniqueKeys(this.migrationContext.GetGhostTableName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sharedUniqueKeys, err := this.getSharedUniqueKeys(this.migrationContext.OriginalTableUniqueKeys, this.migrationContext.GhostTableUniqueKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sharedUniqueKeys := this.getSharedUniqueKeys(this.migrationContext.OriginalTableUniqueKeys, this.migrationContext.GhostTableUniqueKeys)
|
||||
for i, sharedUniqueKey := range sharedUniqueKeys {
|
||||
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, &sharedUniqueKey.Columns)
|
||||
uniqueKeyIsValid := true
|
||||
@ -141,14 +135,14 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
|
||||
switch column.Type {
|
||||
case sql.FloatColumnType:
|
||||
{
|
||||
this.migrationContext.Log.Warning("Will not use %+v as shared key due to FLOAT data type", sharedUniqueKey.Name)
|
||||
log.Warning("Will not use %+v as shared key due to FLOAT data type", sharedUniqueKey.Name)
|
||||
uniqueKeyIsValid = false
|
||||
}
|
||||
case sql.JSONColumnType:
|
||||
{
|
||||
// Noteworthy that at this time MySQL does not allow JSON indexing anyhow, but this code
|
||||
// will remain in place to potentially handle the future case where JSON is supported in indexes.
|
||||
this.migrationContext.Log.Warning("Will not use %+v as shared key due to JSON data type", sharedUniqueKey.Name)
|
||||
log.Warning("Will not use %+v as shared key due to JSON data type", sharedUniqueKey.Name)
|
||||
uniqueKeyIsValid = false
|
||||
}
|
||||
}
|
||||
@ -161,23 +155,29 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
|
||||
if this.migrationContext.UniqueKey == nil {
|
||||
return fmt.Errorf("No shared unique key can be found after ALTER! Bailing out")
|
||||
}
|
||||
this.migrationContext.Log.Infof("Chosen shared unique key is %s", this.migrationContext.UniqueKey.Name)
|
||||
log.Infof("Chosen shared unique key is %s", this.migrationContext.UniqueKey.Name)
|
||||
if this.migrationContext.UniqueKey.HasNullable {
|
||||
if this.migrationContext.NullableUniqueKeyAllowed {
|
||||
this.migrationContext.Log.Warningf("Chosen key (%s) has nullable columns. You have supplied with --allow-nullable-unique-key and so this migration proceeds. As long as there aren't NULL values in this key's column, migration should be fine. NULL values will corrupt migration's data", this.migrationContext.UniqueKey)
|
||||
log.Warningf("Chosen key (%s) has nullable columns. You have supplied with --allow-nullable-unique-key and so this migration proceeds. As long as there aren't NULL values in this key's column, migration should be fine. NULL values will corrupt migration's data", this.migrationContext.UniqueKey)
|
||||
} else {
|
||||
return fmt.Errorf("Chosen key (%s) has nullable columns. Bailing out. To force this operation to continue, supply --allow-nullable-unique-key flag. Only do so if you are certain there are no actual NULL values in this key. As long as there aren't, migration should be fine. NULL values in columns of this key will corrupt migration's data", this.migrationContext.UniqueKey)
|
||||
}
|
||||
}
|
||||
if !this.migrationContext.UniqueKey.IsPrimary() {
|
||||
if this.migrationContext.OriginalBinlogRowImage != "FULL" {
|
||||
return fmt.Errorf("binlog_row_image is '%s' and chosen key is %s, which is not the primary key. This operation cannot proceed. You may `set global binlog_row_image='full'` and try again", this.migrationContext.OriginalBinlogRowImage, this.migrationContext.UniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
this.migrationContext.SharedColumns, this.migrationContext.MappedSharedColumns = this.getSharedColumns(this.migrationContext.OriginalTableColumns, this.migrationContext.GhostTableColumns, this.migrationContext.OriginalTableVirtualColumns, this.migrationContext.GhostTableVirtualColumns, this.migrationContext.ColumnRenameMap)
|
||||
this.migrationContext.Log.Infof("Shared columns are %s", this.migrationContext.SharedColumns)
|
||||
this.migrationContext.SharedColumns, this.migrationContext.MappedSharedColumns = this.getSharedColumns(this.migrationContext.OriginalTableColumns, this.migrationContext.GhostTableColumns, this.migrationContext.ColumnRenameMap)
|
||||
log.Infof("Shared columns are %s", this.migrationContext.SharedColumns)
|
||||
// By fact that a non-empty unique key exists we also know the shared columns are non-empty
|
||||
|
||||
// This additional step looks at which columns are unsigned. We could have merged this within
|
||||
// the `getTableColumns()` function, but it's a later patch and introduces some complexity; I feel
|
||||
// comfortable in doing this as a separate step.
|
||||
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, this.migrationContext.OriginalTableColumns, this.migrationContext.SharedColumns, &this.migrationContext.UniqueKey.Columns)
|
||||
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, this.migrationContext.OriginalTableColumns, this.migrationContext.SharedColumns)
|
||||
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, &this.migrationContext.UniqueKey.Columns)
|
||||
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.GetGhostTableName(), this.migrationContext.GhostTableColumns, this.migrationContext.MappedSharedColumns)
|
||||
|
||||
for i := range this.migrationContext.SharedColumns.Columns() {
|
||||
@ -186,20 +186,9 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
|
||||
if column.Name == mappedColumn.Name && column.Type == sql.DateTimeColumnType && mappedColumn.Type == sql.TimestampColumnType {
|
||||
this.migrationContext.MappedSharedColumns.SetConvertDatetimeToTimestamp(column.Name, this.migrationContext.ApplierTimeZone)
|
||||
}
|
||||
if column.Name == mappedColumn.Name && column.Type == sql.EnumColumnType && mappedColumn.Charset != "" {
|
||||
this.migrationContext.MappedSharedColumns.SetEnumToTextConversion(column.Name)
|
||||
this.migrationContext.MappedSharedColumns.SetEnumValues(column.Name, column.EnumValues)
|
||||
}
|
||||
if column.Name == mappedColumn.Name && column.Charset != mappedColumn.Charset {
|
||||
this.migrationContext.SharedColumns.SetCharsetConversion(column.Name, column.Charset, mappedColumn.Charset)
|
||||
}
|
||||
}
|
||||
|
||||
for _, column := range this.migrationContext.UniqueKey.Columns.Columns() {
|
||||
if this.migrationContext.GhostTableVirtualColumns.GetColumn(column.Name) != nil {
|
||||
// this is a virtual column
|
||||
continue
|
||||
}
|
||||
if this.migrationContext.MappedSharedColumns.HasTimezoneConversion(column.Name) {
|
||||
return fmt.Errorf("No support at this time for converting a column from DATETIME to TIMESTAMP that is also part of the chosen unique key. Column: %s, key: %s", column.Name, this.migrationContext.UniqueKey.Name)
|
||||
}
|
||||
@ -214,7 +203,7 @@ func (this *Inspector) validateConnection() error {
|
||||
return fmt.Errorf("MySQL replication length limited to 32 characters. See https://dev.mysql.com/doc/refman/5.7/en/assigning-passwords.html")
|
||||
}
|
||||
|
||||
version, err := base.ValidateConnection(this.db, this.connectionConfig, this.migrationContext, this.name)
|
||||
version, err := base.ValidateConnection(this.db, this.connectionConfig)
|
||||
this.migrationContext.InspectorMySQLVersion = version
|
||||
return err
|
||||
}
|
||||
@ -247,9 +236,6 @@ func (this *Inspector) validateGrants() error {
|
||||
if strings.Contains(grant, fmt.Sprintf("GRANT ALL PRIVILEGES ON `%s`.*", this.migrationContext.DatabaseName)) {
|
||||
foundDBAll = true
|
||||
}
|
||||
if strings.Contains(grant, fmt.Sprintf("GRANT ALL PRIVILEGES ON `%s`.*", strings.Replace(this.migrationContext.DatabaseName, "_", "\\_", -1))) {
|
||||
foundDBAll = true
|
||||
}
|
||||
if base.StringContainsAll(grant, `ALTER`, `CREATE`, `DELETE`, `DROP`, `INDEX`, `INSERT`, `LOCK TABLES`, `SELECT`, `TRIGGER`, `UPDATE`, ` ON *.*`) {
|
||||
foundDBAll = true
|
||||
}
|
||||
@ -265,19 +251,19 @@ func (this *Inspector) validateGrants() error {
|
||||
this.migrationContext.HasSuperPrivilege = foundSuper
|
||||
|
||||
if foundAll {
|
||||
this.migrationContext.Log.Infof("User has ALL privileges")
|
||||
log.Infof("User has ALL privileges")
|
||||
return nil
|
||||
}
|
||||
if foundSuper && foundReplicationSlave && foundDBAll {
|
||||
this.migrationContext.Log.Infof("User has SUPER, REPLICATION SLAVE privileges, and has ALL privileges on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
log.Infof("User has SUPER, REPLICATION SLAVE privileges, and has ALL privileges on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
return nil
|
||||
}
|
||||
if foundReplicationClient && foundReplicationSlave && foundDBAll {
|
||||
this.migrationContext.Log.Infof("User has REPLICATION CLIENT, REPLICATION SLAVE privileges, and has ALL privileges on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
log.Infof("User has REPLICATION CLIENT, REPLICATION SLAVE privileges, and has ALL privileges on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
return nil
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Privileges: Super: %t, REPLICATION CLIENT: %t, REPLICATION SLAVE: %t, ALL on *.*: %t, ALL on %s.*: %t", foundSuper, foundReplicationClient, foundReplicationSlave, foundAll, sql.EscapeName(this.migrationContext.DatabaseName), foundDBAll)
|
||||
return this.migrationContext.Log.Errorf("User has insufficient privileges for migration. Needed: SUPER|REPLICATION CLIENT, REPLICATION SLAVE and ALL on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
log.Debugf("Privileges: Super: %t, REPLICATION CLIENT: %t, REPLICATION SLAVE: %t, ALL on *.*: %t, ALL on %s.*: %t", foundSuper, foundReplicationClient, foundReplicationSlave, foundAll, sql.EscapeName(this.migrationContext.DatabaseName), foundDBAll)
|
||||
return log.Errorf("User has insufficient privileges for migration. Needed: SUPER|REPLICATION CLIENT, REPLICATION SLAVE and ALL on %s.*", sql.EscapeName(this.migrationContext.DatabaseName))
|
||||
}
|
||||
|
||||
// restartReplication is required so that we are _certain_ the binlog format and
|
||||
@ -285,7 +271,7 @@ func (this *Inspector) validateGrants() error {
|
||||
// It is entirely possible, for example, that the replication is using 'STATEMENT'
|
||||
// binlog format even as the variable says 'ROW'
|
||||
func (this *Inspector) restartReplication() error {
|
||||
this.migrationContext.Log.Infof("Restarting replication on %s to make sure binlog settings apply to replication thread", this.connectionConfig.Key.String())
|
||||
log.Infof("Restarting replication on %s:%d to make sure binlog settings apply to replication thread", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
|
||||
masterKey, _ := mysql.GetMasterKeyFromSlaveStatus(this.connectionConfig)
|
||||
if masterKey == nil {
|
||||
@ -304,7 +290,7 @@ func (this *Inspector) restartReplication() error {
|
||||
}
|
||||
time.Sleep(startSlavePostWaitMilliseconds)
|
||||
|
||||
this.migrationContext.Log.Debugf("Replication restarted")
|
||||
log.Debugf("Replication restarted")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -324,7 +310,7 @@ func (this *Inspector) applyBinlogFormat() error {
|
||||
if err := this.restartReplication(); err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Debugf("'ROW' binlog format applied")
|
||||
log.Debugf("'ROW' binlog format applied")
|
||||
return nil
|
||||
}
|
||||
// We already have RBR, no explicit switch
|
||||
@ -344,13 +330,13 @@ func (this *Inspector) validateBinlogs() error {
|
||||
return err
|
||||
}
|
||||
if !hasBinaryLogs {
|
||||
return fmt.Errorf("%s must have binary logs enabled", this.connectionConfig.Key.String())
|
||||
return fmt.Errorf("%s:%d must have binary logs enabled", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
}
|
||||
if this.migrationContext.RequiresBinlogFormatChange() {
|
||||
if !this.migrationContext.SwitchToRowBinlogFormat {
|
||||
return fmt.Errorf("You must be using ROW binlog format. I can switch it for you, provided --switch-to-rbr and that %s doesn't have replicas", this.connectionConfig.Key.String())
|
||||
return fmt.Errorf("You must be using ROW binlog format. I can switch it for you, provided --switch-to-rbr and that %s:%d doesn't have replicas", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
}
|
||||
query := `show /* gh-ost */ slave hosts`
|
||||
query := fmt.Sprintf(`show /* gh-ost */ slave hosts`)
|
||||
countReplicas := 0
|
||||
err := sqlutils.QueryRowsMap(this.db, query, func(rowMap sqlutils.RowMap) error {
|
||||
countReplicas++
|
||||
@ -360,20 +346,18 @@ func (this *Inspector) validateBinlogs() error {
|
||||
return err
|
||||
}
|
||||
if countReplicas > 0 {
|
||||
return fmt.Errorf("%s has %s binlog_format, but I'm too scared to change it to ROW because it has replicas. Bailing out", this.connectionConfig.Key.String(), this.migrationContext.OriginalBinlogFormat)
|
||||
return fmt.Errorf("%s:%d has %s binlog_format, but I'm too scared to change it to ROW because it has replicas. Bailing out", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat)
|
||||
}
|
||||
this.migrationContext.Log.Infof("%s has %s binlog_format. I will change it to ROW, and will NOT change it back, even in the event of failure.", this.connectionConfig.Key.String(), this.migrationContext.OriginalBinlogFormat)
|
||||
log.Infof("%s:%d has %s binlog_format. I will change it to ROW, and will NOT change it back, even in the event of failure.", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat)
|
||||
}
|
||||
query = `select @@global.binlog_row_image`
|
||||
if err := this.db.QueryRow(query).Scan(&this.migrationContext.OriginalBinlogRowImage); err != nil {
|
||||
return err
|
||||
// Only as of 5.6. We wish to support 5.5 as well
|
||||
this.migrationContext.OriginalBinlogRowImage = "FULL"
|
||||
}
|
||||
this.migrationContext.OriginalBinlogRowImage = strings.ToUpper(this.migrationContext.OriginalBinlogRowImage)
|
||||
if this.migrationContext.OriginalBinlogRowImage != "FULL" {
|
||||
return fmt.Errorf("%s has '%s' binlog_row_image, and only 'FULL' is supported. This operation cannot proceed. You may `set global binlog_row_image='full'` and try again", this.connectionConfig.Key.String(), this.migrationContext.OriginalBinlogRowImage)
|
||||
}
|
||||
|
||||
this.migrationContext.Log.Infof("binary logs validated on %s", this.connectionConfig.Key.String())
|
||||
log.Infof("binary logs validated on %s:%d", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -386,25 +370,25 @@ func (this *Inspector) validateLogSlaveUpdates() error {
|
||||
}
|
||||
|
||||
if logSlaveUpdates {
|
||||
this.migrationContext.Log.Infof("log_slave_updates validated on %s", this.connectionConfig.Key.String())
|
||||
log.Infof("log_slave_updates validated on %s:%d", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
if this.migrationContext.IsTungsten {
|
||||
this.migrationContext.Log.Warningf("log_slave_updates not found on %s, but --tungsten provided, so I'm proceeding", this.connectionConfig.Key.String())
|
||||
log.Warningf("log_slave_updates not found on %s:%d, but --tungsten provided, so I'm proceeding", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
if this.migrationContext.TestOnReplica || this.migrationContext.MigrateOnReplica {
|
||||
return fmt.Errorf("%s must have log_slave_updates enabled for testing/migrating on replica", this.connectionConfig.Key.String())
|
||||
return fmt.Errorf("%s:%d must have log_slave_updates enabled for testing/migrating on replica", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
}
|
||||
|
||||
if this.migrationContext.InspectorIsAlsoApplier() {
|
||||
this.migrationContext.Log.Warningf("log_slave_updates not found on %s, but executing directly on master, so I'm proceeding", this.connectionConfig.Key.String())
|
||||
log.Warningf("log_slave_updates not found on %s:%d, but executing directly on master, so I'm proceeding", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s must have log_slave_updates enabled for executing migration", this.connectionConfig.Key.String())
|
||||
return fmt.Errorf("%s:%d must have log_slave_updates enabled for executing migration", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)
|
||||
}
|
||||
|
||||
// validateTable makes sure the table we need to operate on actually exists
|
||||
@ -427,17 +411,17 @@ func (this *Inspector) validateTable() error {
|
||||
return err
|
||||
}
|
||||
if !tableFound {
|
||||
return this.migrationContext.Log.Errorf("Cannot find table %s.%s!", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
return log.Errorf("Cannot find table %s.%s!", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
}
|
||||
this.migrationContext.Log.Infof("Table found. Engine=%s", this.migrationContext.TableEngine)
|
||||
this.migrationContext.Log.Debugf("Estimated number of rows via STATUS: %d", this.migrationContext.RowsEstimate)
|
||||
log.Infof("Table found. Engine=%s", this.migrationContext.TableEngine)
|
||||
log.Debugf("Estimated number of rows via STATUS: %d", this.migrationContext.RowsEstimate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTableForeignKeys makes sure no foreign keys exist on the migrated table
|
||||
func (this *Inspector) validateTableForeignKeys(allowChildForeignKeys bool) error {
|
||||
if this.migrationContext.SkipForeignKeyChecks {
|
||||
this.migrationContext.Log.Warning("--skip-foreign-key-checks provided: will not check for foreign keys")
|
||||
log.Warning("--skip-foreign-key-checks provided: will not check for foreign keys")
|
||||
return nil
|
||||
}
|
||||
query := `
|
||||
@ -471,16 +455,16 @@ func (this *Inspector) validateTableForeignKeys(allowChildForeignKeys bool) erro
|
||||
return err
|
||||
}
|
||||
if numParentForeignKeys > 0 {
|
||||
return this.migrationContext.Log.Errorf("Found %d parent-side foreign keys on %s.%s. Parent-side foreign keys are not supported. Bailing out", numParentForeignKeys, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
return log.Errorf("Found %d parent-side foreign keys on %s.%s. Parent-side foreign keys are not supported. Bailing out", numParentForeignKeys, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
}
|
||||
if numChildForeignKeys > 0 {
|
||||
if allowChildForeignKeys {
|
||||
this.migrationContext.Log.Debugf("Foreign keys found and will be dropped, as per given --discard-foreign-keys flag")
|
||||
log.Debugf("Foreign keys found and will be dropped, as per given --discard-foreign-keys flag")
|
||||
return nil
|
||||
}
|
||||
return this.migrationContext.Log.Errorf("Found %d child-side foreign keys on %s.%s. Child-side foreign keys are not supported. Bailing out", numChildForeignKeys, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
return log.Errorf("Found %d child-side foreign keys on %s.%s. Child-side foreign keys are not supported. Bailing out", numChildForeignKeys, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Validated no foreign keys exist on table")
|
||||
log.Debugf("Validated no foreign keys exist on table")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -506,9 +490,9 @@ func (this *Inspector) validateTableTriggers() error {
|
||||
return err
|
||||
}
|
||||
if numTriggers > 0 {
|
||||
return this.migrationContext.Log.Errorf("Found triggers on %s.%s. Triggers are not supported at this time. Bailing out", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
return log.Errorf("Found triggers on %s.%s. Triggers are not supported at this time. Bailing out", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Validated no triggers exist on table")
|
||||
log.Debugf("Validated no triggers exist on table")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -528,48 +512,28 @@ func (this *Inspector) estimateTableRowsViaExplain() error {
|
||||
return err
|
||||
}
|
||||
if !outputFound {
|
||||
return this.migrationContext.Log.Errorf("Cannot run EXPLAIN on %s.%s!", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
return log.Errorf("Cannot run EXPLAIN on %s.%s!", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
}
|
||||
this.migrationContext.Log.Infof("Estimated number of rows via EXPLAIN: %d", this.migrationContext.RowsEstimate)
|
||||
log.Infof("Estimated number of rows via EXPLAIN: %d", this.migrationContext.RowsEstimate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountTableRows counts exact number of rows on the original table
|
||||
func (this *Inspector) CountTableRows(ctx context.Context) error {
|
||||
func (this *Inspector) CountTableRows() error {
|
||||
atomic.StoreInt64(&this.migrationContext.CountingRowsFlag, 1)
|
||||
defer atomic.StoreInt64(&this.migrationContext.CountingRowsFlag, 0)
|
||||
|
||||
this.migrationContext.Log.Infof("As instructed, I'm issuing a SELECT COUNT(*) on the table. This may take a while")
|
||||
log.Infof("As instructed, I'm issuing a SELECT COUNT(*) on the table. This may take a while")
|
||||
|
||||
conn, err := this.db.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var connectionID string
|
||||
if err := conn.QueryRowContext(ctx, `SELECT /* gh-ost */ CONNECTION_ID()`).Scan(&connectionID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`select /* gh-ost */ count(*) as count_rows from %s.%s`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
query := fmt.Sprintf(`select /* gh-ost */ count(*) as rows from %s.%s`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
|
||||
var rowsEstimate int64
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&rowsEstimate); err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
this.migrationContext.Log.Infof("exact row count cancelled (%s), likely because I'm about to cut over. I'm going to kill that query.", ctx.Err())
|
||||
return mysql.Kill(this.db, connectionID)
|
||||
}
|
||||
if err := this.db.QueryRow(query).Scan(&rowsEstimate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// row count query finished. nil out the cancel func, so the main migration thread
|
||||
// doesn't bother calling it after row copy is done.
|
||||
this.migrationContext.SetCountTableRowsCancelFunc(nil)
|
||||
|
||||
atomic.StoreInt64(&this.migrationContext.RowsEstimate, rowsEstimate)
|
||||
this.migrationContext.UsedRowsEstimateMethod = base.CountRowsEstimate
|
||||
|
||||
this.migrationContext.Log.Infof("Exact number of rows via COUNT: %d", rowsEstimate)
|
||||
log.Infof("Exact number of rows via COUNT: %d", rowsEstimate)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -588,41 +552,44 @@ func (this *Inspector) applyColumnTypes(databaseName, tableName string, columnsL
|
||||
err := sqlutils.QueryRowsMap(this.db, query, func(m sqlutils.RowMap) error {
|
||||
columnName := m.GetString("COLUMN_NAME")
|
||||
columnType := m.GetString("COLUMN_TYPE")
|
||||
columnOctetLength := m.GetUint("CHARACTER_OCTET_LENGTH")
|
||||
for _, columnsList := range columnsLists {
|
||||
column := columnsList.GetColumn(columnName)
|
||||
if column == nil {
|
||||
continue
|
||||
if strings.Contains(columnType, "unsigned") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.SetUnsigned(columnName)
|
||||
}
|
||||
|
||||
if strings.Contains(columnType, "unsigned") {
|
||||
column.IsUnsigned = true
|
||||
}
|
||||
if strings.Contains(columnType, "mediumint") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.MediumIntColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "mediumint") {
|
||||
column.Type = sql.MediumIntColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "timestamp") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.TimestampColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "timestamp") {
|
||||
column.Type = sql.TimestampColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "datetime") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.DateTimeColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "datetime") {
|
||||
column.Type = sql.DateTimeColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "json") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.JSONColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "json") {
|
||||
column.Type = sql.JSONColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "float") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.FloatColumnType
|
||||
}
|
||||
if strings.Contains(columnType, "float") {
|
||||
column.Type = sql.FloatColumnType
|
||||
}
|
||||
if strings.HasPrefix(columnType, "enum") {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.GetColumn(columnName).Type = sql.EnumColumnType
|
||||
}
|
||||
if strings.HasPrefix(columnType, "enum") {
|
||||
column.Type = sql.EnumColumnType
|
||||
column.EnumValues = sql.ParseEnumValues(m.GetString("COLUMN_TYPE"))
|
||||
}
|
||||
if strings.HasPrefix(columnType, "binary") {
|
||||
column.Type = sql.BinaryColumnType
|
||||
column.BinaryOctetLength = columnOctetLength
|
||||
}
|
||||
if charset := m.GetString("CHARACTER_SET_NAME"); charset != "" {
|
||||
column.Charset = charset
|
||||
}
|
||||
if charset := m.GetString("CHARACTER_SET_NAME"); charset != "" {
|
||||
for _, columnsList := range columnsLists {
|
||||
columnsList.SetCharset(columnName, charset)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -630,24 +597,6 @@ func (this *Inspector) applyColumnTypes(databaseName, tableName string, columnsL
|
||||
return err
|
||||
}
|
||||
|
||||
// getAutoIncrementValue get's the original table's AUTO_INCREMENT value, if exists (0 value if not exists)
|
||||
func (this *Inspector) getAutoIncrementValue(tableName string) (autoIncrement uint64, err error) {
|
||||
query := `
|
||||
SELECT
|
||||
AUTO_INCREMENT
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE
|
||||
TABLES.TABLE_SCHEMA = ?
|
||||
AND TABLES.TABLE_NAME = ?
|
||||
AND AUTO_INCREMENT IS NOT NULL
|
||||
`
|
||||
err = sqlutils.QueryRowsMap(this.db, query, func(m sqlutils.RowMap) error {
|
||||
autoIncrement = m.GetUint64("AUTO_INCREMENT")
|
||||
return nil
|
||||
}, this.migrationContext.DatabaseName, tableName)
|
||||
return autoIncrement, err
|
||||
}
|
||||
|
||||
// getCandidateUniqueKeys investigates a table and returns the list of unique keys
|
||||
// candidate for chunking
|
||||
func (this *Inspector) getCandidateUniqueKeys(tableName string) (uniqueKeys [](*sql.UniqueKey), err error) {
|
||||
@ -680,6 +629,8 @@ func (this *Inspector) getCandidateUniqueKeys(tableName string) (uniqueKeys [](*
|
||||
GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
|
||||
) AS UNIQUES
|
||||
ON (
|
||||
COLUMNS.TABLE_SCHEMA = UNIQUES.TABLE_SCHEMA AND
|
||||
COLUMNS.TABLE_NAME = UNIQUES.TABLE_NAME AND
|
||||
COLUMNS.COLUMN_NAME = UNIQUES.FIRST_COLUMN_NAME
|
||||
)
|
||||
WHERE
|
||||
@ -721,13 +672,13 @@ func (this *Inspector) getCandidateUniqueKeys(tableName string) (uniqueKeys [](*
|
||||
if err != nil {
|
||||
return uniqueKeys, err
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Potential unique keys in %+v: %+v", tableName, uniqueKeys)
|
||||
log.Debugf("Potential unique keys in %+v: %+v", tableName, uniqueKeys)
|
||||
return uniqueKeys, nil
|
||||
}
|
||||
|
||||
// getSharedUniqueKeys returns the intersection of two given unique keys,
|
||||
// testing by list of columns
|
||||
func (this *Inspector) getSharedUniqueKeys(originalUniqueKeys, ghostUniqueKeys []*sql.UniqueKey) (uniqueKeys []*sql.UniqueKey) {
|
||||
func (this *Inspector) getSharedUniqueKeys(originalUniqueKeys, ghostUniqueKeys [](*sql.UniqueKey)) (uniqueKeys [](*sql.UniqueKey), err error) {
|
||||
// We actually do NOT rely on key name, just on the set of columns. This is because maybe
|
||||
// the ALTER is on the name itself...
|
||||
for _, originalUniqueKey := range originalUniqueKeys {
|
||||
@ -737,38 +688,25 @@ func (this *Inspector) getSharedUniqueKeys(originalUniqueKeys, ghostUniqueKeys [
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueKeys
|
||||
return uniqueKeys, nil
|
||||
}
|
||||
|
||||
// getSharedColumns returns the intersection of two lists of columns in same order as the first list
|
||||
func (this *Inspector) getSharedColumns(originalColumns, ghostColumns *sql.ColumnList, originalVirtualColumns, ghostVirtualColumns *sql.ColumnList, columnRenameMap map[string]string) (*sql.ColumnList, *sql.ColumnList) {
|
||||
func (this *Inspector) getSharedColumns(originalColumns, ghostColumns *sql.ColumnList, columnRenameMap map[string]string) (*sql.ColumnList, *sql.ColumnList) {
|
||||
sharedColumnNames := []string{}
|
||||
for _, originalColumn := range originalColumns.Names() {
|
||||
isSharedColumn := false
|
||||
for _, ghostColumn := range ghostColumns.Names() {
|
||||
if strings.EqualFold(originalColumn, ghostColumn) {
|
||||
isSharedColumn = true
|
||||
break
|
||||
}
|
||||
if strings.EqualFold(columnRenameMap[originalColumn], ghostColumn) {
|
||||
isSharedColumn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for droppedColumn := range this.migrationContext.DroppedColumnsMap {
|
||||
if strings.EqualFold(originalColumn, droppedColumn) {
|
||||
isSharedColumn = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, virtualColumn := range originalVirtualColumns.Names() {
|
||||
if strings.EqualFold(originalColumn, virtualColumn) {
|
||||
isSharedColumn = false
|
||||
}
|
||||
}
|
||||
for _, virtualColumn := range ghostVirtualColumns.Names() {
|
||||
if strings.EqualFold(originalColumn, virtualColumn) {
|
||||
isSharedColumn = false
|
||||
}
|
||||
}
|
||||
if isSharedColumn {
|
||||
@ -811,14 +749,15 @@ func (this *Inspector) readChangelogState(hint string) (string, error) {
|
||||
}
|
||||
|
||||
func (this *Inspector) getMasterConnectionConfig() (applierConfig *mysql.ConnectionConfig, err error) {
|
||||
this.migrationContext.Log.Infof("Recursively searching for replication master")
|
||||
log.Infof("Recursively searching for replication master")
|
||||
visitedKeys := mysql.NewInstanceKeyMap()
|
||||
return mysql.GetMasterConnectionConfigSafe(this.connectionConfig, visitedKeys, this.migrationContext.AllowedMasterMaster)
|
||||
}
|
||||
|
||||
func (this *Inspector) getReplicationLag() (replicationLag time.Duration, err error) {
|
||||
replicationLag, err = mysql.GetReplicationLagFromSlaveStatus(
|
||||
replicationLag, err = mysql.GetReplicationLag(
|
||||
this.informationSchemaDb,
|
||||
this.migrationContext.InspectorConnectionConfig,
|
||||
)
|
||||
return replicationLag, err
|
||||
}
|
||||
@ -826,4 +765,5 @@ func (this *Inspector) getReplicationLag() (replicationLag time.Duration, err er
|
||||
func (this *Inspector) Teardown() {
|
||||
this.db.Close()
|
||||
this.informationSchemaDb.Close()
|
||||
return
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
test "github.com/openark/golib/tests"
|
||||
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
)
|
||||
|
||||
func TestInspectGetSharedUniqueKeys(t *testing.T) {
|
||||
origUniqKeys := []*sql.UniqueKey{
|
||||
{Columns: *sql.NewColumnList([]string{"id", "item_id"})},
|
||||
{Columns: *sql.NewColumnList([]string{"id", "org_id"})},
|
||||
}
|
||||
ghostUniqKeys := []*sql.UniqueKey{
|
||||
{Columns: *sql.NewColumnList([]string{"id", "item_id"})},
|
||||
{Columns: *sql.NewColumnList([]string{"id", "org_id"})},
|
||||
{Columns: *sql.NewColumnList([]string{"item_id", "user_id"})},
|
||||
}
|
||||
inspector := &Inspector{}
|
||||
sharedUniqKeys := inspector.getSharedUniqueKeys(origUniqKeys, ghostUniqKeys)
|
||||
test.S(t).ExpectEquals(len(sharedUniqKeys), 2)
|
||||
test.S(t).ExpectEquals(sharedUniqKeys[0].Columns.String(), "id,item_id")
|
||||
test.S(t).ExpectEquals(sharedUniqKeys[1].Columns.String(), "id,org_id")
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,256 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/openark/golib/tests"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/github/gh-ost/go/binlog"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
)
|
||||
|
||||
func TestMigratorOnChangelogEvent(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
|
||||
t.Run("heartbeat", func(t *testing.T) {
|
||||
columnValues := sql.ToColumnValues([]interface{}{
|
||||
123,
|
||||
time.Now().Unix(),
|
||||
"heartbeat",
|
||||
"2022-08-16T00:45:10.52Z",
|
||||
})
|
||||
tests.S(t).ExpectNil(migrator.onChangelogEvent(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("state-AllEventsUpToLockProcessed", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
es := <-migrator.applyEventsQueue
|
||||
tests.S(t).ExpectNotNil(es)
|
||||
tests.S(t).ExpectNotNil(es.writeFunc)
|
||||
}(&wg)
|
||||
|
||||
columnValues := sql.ToColumnValues([]interface{}{
|
||||
123,
|
||||
time.Now().Unix(),
|
||||
"state",
|
||||
AllEventsUpToLockProcessed,
|
||||
})
|
||||
tests.S(t).ExpectNil(migrator.onChangelogEvent(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}))
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("state-GhostTableMigrated", func(t *testing.T) {
|
||||
go func() {
|
||||
tests.S(t).ExpectTrue(<-migrator.ghostTableMigrated)
|
||||
}()
|
||||
|
||||
columnValues := sql.ToColumnValues([]interface{}{
|
||||
123,
|
||||
time.Now().Unix(),
|
||||
"state",
|
||||
GhostTableMigrated,
|
||||
})
|
||||
tests.S(t).ExpectNil(migrator.onChangelogEvent(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("state-Migrated", func(t *testing.T) {
|
||||
columnValues := sql.ToColumnValues([]interface{}{
|
||||
123,
|
||||
time.Now().Unix(),
|
||||
"state",
|
||||
Migrated,
|
||||
})
|
||||
tests.S(t).ExpectNil(migrator.onChangelogEvent(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("state-ReadMigrationRangeValues", func(t *testing.T) {
|
||||
columnValues := sql.ToColumnValues([]interface{}{
|
||||
123,
|
||||
time.Now().Unix(),
|
||||
"state",
|
||||
ReadMigrationRangeValues,
|
||||
})
|
||||
tests.S(t).ExpectNil(migrator.onChangelogEvent(&binlog.BinlogDMLEvent{
|
||||
DatabaseName: "test",
|
||||
DML: binlog.InsertDML,
|
||||
NewColumnValues: columnValues,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigratorValidateStatement(t *testing.T) {
|
||||
t.Run("add-column", func(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
tests.S(t).ExpectNil(migrator.parser.ParseAlterStatement(`ALTER TABLE test ADD test_new VARCHAR(64) NOT NULL`))
|
||||
|
||||
tests.S(t).ExpectNil(migrator.validateAlterStatement())
|
||||
tests.S(t).ExpectEquals(len(migrator.migrationContext.DroppedColumnsMap), 0)
|
||||
})
|
||||
|
||||
t.Run("drop-column", func(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
tests.S(t).ExpectNil(migrator.parser.ParseAlterStatement(`ALTER TABLE test DROP abc`))
|
||||
|
||||
tests.S(t).ExpectNil(migrator.validateAlterStatement())
|
||||
tests.S(t).ExpectEquals(len(migrator.migrationContext.DroppedColumnsMap), 1)
|
||||
_, exists := migrator.migrationContext.DroppedColumnsMap["abc"]
|
||||
tests.S(t).ExpectTrue(exists)
|
||||
})
|
||||
|
||||
t.Run("rename-column", func(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
tests.S(t).ExpectNil(migrator.parser.ParseAlterStatement(`ALTER TABLE test CHANGE test123 test1234 bigint unsigned`))
|
||||
|
||||
err := migrator.validateAlterStatement()
|
||||
tests.S(t).ExpectNotNil(err)
|
||||
tests.S(t).ExpectTrue(strings.HasPrefix(err.Error(), "gh-ost believes the ALTER statement renames columns"))
|
||||
tests.S(t).ExpectEquals(len(migrator.migrationContext.DroppedColumnsMap), 0)
|
||||
})
|
||||
|
||||
t.Run("rename-column-approved", func(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
migrator.migrationContext.ApproveRenamedColumns = true
|
||||
tests.S(t).ExpectNil(migrator.parser.ParseAlterStatement(`ALTER TABLE test CHANGE test123 test1234 bigint unsigned`))
|
||||
|
||||
tests.S(t).ExpectNil(migrator.validateAlterStatement())
|
||||
tests.S(t).ExpectEquals(len(migrator.migrationContext.DroppedColumnsMap), 0)
|
||||
})
|
||||
|
||||
t.Run("rename-table", func(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
tests.S(t).ExpectNil(migrator.parser.ParseAlterStatement(`ALTER TABLE test RENAME TO test_new`))
|
||||
|
||||
err := migrator.validateAlterStatement()
|
||||
tests.S(t).ExpectNotNil(err)
|
||||
tests.S(t).ExpectTrue(errors.Is(err, ErrMigratorUnsupportedRenameAlter))
|
||||
tests.S(t).ExpectEquals(len(migrator.migrationContext.DroppedColumnsMap), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigratorCreateFlagFiles(t *testing.T) {
|
||||
tmpdir, err := os.MkdirTemp("", t.Name())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrationContext.PostponeCutOverFlagFile = filepath.Join(tmpdir, "cut-over.flag")
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
tests.S(t).ExpectNil(migrator.createFlagFiles())
|
||||
tests.S(t).ExpectNil(migrator.createFlagFiles()) // twice to test already-exists
|
||||
|
||||
_, err = os.Stat(migrationContext.PostponeCutOverFlagFile)
|
||||
tests.S(t).ExpectNil(err)
|
||||
}
|
||||
|
||||
func TestMigratorGetProgressPercent(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
|
||||
{
|
||||
tests.S(t).ExpectEquals(migrator.getProgressPercent(0), float64(100.0))
|
||||
}
|
||||
{
|
||||
migrationContext.TotalRowsCopied = 250
|
||||
tests.S(t).ExpectEquals(migrator.getProgressPercent(1000), float64(25.0))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigratorGetMigrationStateAndETA(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
now := time.Now()
|
||||
migrationContext.RowCopyStartTime = now.Add(-time.Minute)
|
||||
migrationContext.RowCopyEndTime = now
|
||||
|
||||
{
|
||||
migrationContext.TotalRowsCopied = 456
|
||||
state, eta, etaDuration := migrator.getMigrationStateAndETA(123456)
|
||||
tests.S(t).ExpectEquals(state, "migrating")
|
||||
tests.S(t).ExpectEquals(eta, "4h29m44s")
|
||||
tests.S(t).ExpectEquals(etaDuration.String(), "4h29m44s")
|
||||
}
|
||||
{
|
||||
migrationContext.TotalRowsCopied = 456
|
||||
state, eta, etaDuration := migrator.getMigrationStateAndETA(456)
|
||||
tests.S(t).ExpectEquals(state, "migrating")
|
||||
tests.S(t).ExpectEquals(eta, "due")
|
||||
tests.S(t).ExpectEquals(etaDuration.String(), "0s")
|
||||
}
|
||||
{
|
||||
migrationContext.TotalRowsCopied = 123456
|
||||
state, eta, etaDuration := migrator.getMigrationStateAndETA(456)
|
||||
tests.S(t).ExpectEquals(state, "migrating")
|
||||
tests.S(t).ExpectEquals(eta, "due")
|
||||
tests.S(t).ExpectEquals(etaDuration.String(), "0s")
|
||||
}
|
||||
{
|
||||
atomic.StoreInt64(&migrationContext.CountingRowsFlag, 1)
|
||||
state, eta, etaDuration := migrator.getMigrationStateAndETA(123456)
|
||||
tests.S(t).ExpectEquals(state, "counting rows")
|
||||
tests.S(t).ExpectEquals(eta, "due")
|
||||
tests.S(t).ExpectEquals(etaDuration.String(), "0s")
|
||||
}
|
||||
{
|
||||
atomic.StoreInt64(&migrationContext.CountingRowsFlag, 0)
|
||||
atomic.StoreInt64(&migrationContext.IsPostponingCutOver, 1)
|
||||
state, eta, etaDuration := migrator.getMigrationStateAndETA(123456)
|
||||
tests.S(t).ExpectEquals(state, "postponing cut-over")
|
||||
tests.S(t).ExpectEquals(eta, "due")
|
||||
tests.S(t).ExpectEquals(etaDuration.String(), "0s")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigratorShouldPrintStatus(t *testing.T) {
|
||||
migrationContext := base.NewMigrationContext()
|
||||
migrator := NewMigrator(migrationContext, "1.2.3")
|
||||
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(NoPrintStatusRule, 10, time.Second)) // test 'rule != HeuristicPrintStatusRule' return
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 10, time.Second)) // test 'etaDuration.Seconds() <= 60'
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 90, time.Second)) // test 'etaDuration.Seconds() <= 60' again
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 90, time.Minute)) // test 'etaDuration.Seconds() <= 180'
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 60, 90*time.Second)) // test 'elapsedSeconds <= 180'
|
||||
tests.S(t).ExpectFalse(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 61, 90*time.Second)) // test 'elapsedSeconds <= 180'
|
||||
tests.S(t).ExpectFalse(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 99, 210*time.Second)) // test 'elapsedSeconds <= 180'
|
||||
tests.S(t).ExpectFalse(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 12345, 86400*time.Second)) // test 'else'
|
||||
tests.S(t).ExpectTrue(migrator.shouldPrintStatus(HeuristicPrintStatusRule, 30030, 86400*time.Second)) // test 'else' again
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/outbrain/golib/log"
|
||||
)
|
||||
|
||||
type printStatusFunc func(PrintStatusRule, io.Writer)
|
||||
@ -48,12 +49,12 @@ func (this *Server) BindSocketFile() (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Listening on unix socket file: %s", this.migrationContext.ServeSocketFile)
|
||||
log.Infof("Listening on unix socket file: %s", this.migrationContext.ServeSocketFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Server) RemoveSocketFile() (err error) {
|
||||
this.migrationContext.Log.Infof("Removing socket file: %s", this.migrationContext.ServeSocketFile)
|
||||
log.Infof("Removing socket file: %s", this.migrationContext.ServeSocketFile)
|
||||
return os.Remove(this.migrationContext.ServeSocketFile)
|
||||
}
|
||||
|
||||
@ -65,7 +66,7 @@ func (this *Server) BindTCPPort() (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.migrationContext.Log.Infof("Listening on tcp port: %d", this.migrationContext.ServeTCPPort)
|
||||
log.Infof("Listening on tcp port: %d", this.migrationContext.ServeTCPPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -75,7 +76,7 @@ func (this *Server) Serve() (err error) {
|
||||
for {
|
||||
conn, err := this.unixListener.Accept()
|
||||
if err != nil {
|
||||
this.migrationContext.Log.Errore(err)
|
||||
log.Errore(err)
|
||||
}
|
||||
go this.handleConnection(conn)
|
||||
}
|
||||
@ -87,7 +88,7 @@ func (this *Server) Serve() (err error) {
|
||||
for {
|
||||
conn, err := this.tcpListener.Accept()
|
||||
if err != nil {
|
||||
this.migrationContext.Log.Errore(err)
|
||||
log.Errore(err)
|
||||
}
|
||||
go this.handleConnection(conn)
|
||||
}
|
||||
@ -117,22 +118,21 @@ func (this *Server) onServerCommand(command string, writer *bufio.Writer) (err e
|
||||
} else {
|
||||
fmt.Fprintf(writer, "%s\n", err.Error())
|
||||
}
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
}
|
||||
|
||||
// applyServerCommand parses and executes commands by user
|
||||
func (this *Server) applyServerCommand(command string, writer *bufio.Writer) (printStatusRule PrintStatusRule, err error) {
|
||||
printStatusRule = NoPrintStatusRule
|
||||
|
||||
tokens := strings.SplitN(command, "=", 2)
|
||||
command = strings.TrimSpace(tokens[0])
|
||||
arg := ""
|
||||
if len(tokens) > 1 {
|
||||
arg = strings.TrimSpace(tokens[1])
|
||||
if unquoted, err := strconv.Unquote(arg); err == nil {
|
||||
arg = unquoted
|
||||
}
|
||||
}
|
||||
argIsQuestion := (arg == "?")
|
||||
throttleHint := "# Note: you may only throttle for as long as your binary logs are not purged"
|
||||
throttleHint := "# Note: you may only throttle for as long as your binary logs are not purged\n"
|
||||
|
||||
if err := this.hooksExecutor.onInteractiveCommand(command); err != nil {
|
||||
return NoPrintStatusRule, err
|
||||
@ -141,12 +141,10 @@ func (this *Server) applyServerCommand(command string, writer *bufio.Writer) (pr
|
||||
switch command {
|
||||
case "help":
|
||||
{
|
||||
fmt.Fprint(writer, `available commands:
|
||||
fmt.Fprintln(writer, `available commands:
|
||||
status # Print a detailed status message
|
||||
sup # Print a short status message
|
||||
coordinates # Print the currently inspected coordinates
|
||||
applier # Print the hostname of the applier
|
||||
inspector # Print the hostname of the inspector
|
||||
coordinates # Print the currently inspected coordinates
|
||||
chunk-size=<newsize> # Set a new chunk-size
|
||||
dml-batch-size=<newsize> # Set a new dml-batch-size
|
||||
nice-ratio=<ratio> # Set a new nice-ratio, immediate sleep after each row-copy operation, float (examples: 0 is aggressive, 0.7 adds 70% runtime, 1.0 doubles runtime, 2.0 triples runtime, ...)
|
||||
@ -177,22 +175,6 @@ help # This message
|
||||
}
|
||||
return NoPrintStatusRule, fmt.Errorf("coordinates are read-only")
|
||||
}
|
||||
case "applier":
|
||||
if this.migrationContext.ApplierConnectionConfig != nil && this.migrationContext.ApplierConnectionConfig.ImpliedKey != nil {
|
||||
fmt.Fprintf(writer, "Host: %s, Version: %s\n",
|
||||
this.migrationContext.ApplierConnectionConfig.ImpliedKey.String(),
|
||||
this.migrationContext.ApplierMySQLVersion,
|
||||
)
|
||||
}
|
||||
return NoPrintStatusRule, nil
|
||||
case "inspector":
|
||||
if this.migrationContext.InspectorConnectionConfig != nil && this.migrationContext.InspectorConnectionConfig.ImpliedKey != nil {
|
||||
fmt.Fprintf(writer, "Host: %s, Version: %s\n",
|
||||
this.migrationContext.InspectorConnectionConfig.ImpliedKey.String(),
|
||||
this.migrationContext.InspectorMySQLVersion,
|
||||
)
|
||||
}
|
||||
return NoPrintStatusRule, nil
|
||||
case "chunk-size":
|
||||
{
|
||||
if argIsQuestion {
|
||||
@ -280,7 +262,7 @@ help # This message
|
||||
return NoPrintStatusRule, nil
|
||||
}
|
||||
this.migrationContext.SetThrottleQuery(arg)
|
||||
fmt.Fprintln(writer, throttleHint)
|
||||
fmt.Fprintf(writer, throttleHint)
|
||||
return ForcePrintStatusAndHintRule, nil
|
||||
}
|
||||
case "throttle-http":
|
||||
@ -290,7 +272,7 @@ help # This message
|
||||
return NoPrintStatusRule, nil
|
||||
}
|
||||
this.migrationContext.SetThrottleHTTP(arg)
|
||||
fmt.Fprintln(writer, throttleHint)
|
||||
fmt.Fprintf(writer, throttleHint)
|
||||
return ForcePrintStatusAndHintRule, nil
|
||||
}
|
||||
case "throttle-control-replicas":
|
||||
@ -307,22 +289,12 @@ help # This message
|
||||
}
|
||||
case "throttle", "pause", "suspend":
|
||||
{
|
||||
if arg != "" && arg != this.migrationContext.OriginalTableName {
|
||||
// User explicitly provided table name. This is a courtesy protection mechanism
|
||||
err := fmt.Errorf("User commanded 'throttle' on %s, but migrated table is %s; ignoring request.", arg, this.migrationContext.OriginalTableName)
|
||||
return NoPrintStatusRule, err
|
||||
}
|
||||
atomic.StoreInt64(&this.migrationContext.ThrottleCommandedByUser, 1)
|
||||
fmt.Fprintln(writer, throttleHint)
|
||||
fmt.Fprintf(writer, throttleHint)
|
||||
return ForcePrintStatusAndHintRule, nil
|
||||
}
|
||||
case "no-throttle", "unthrottle", "resume", "continue":
|
||||
{
|
||||
if arg != "" && arg != this.migrationContext.OriginalTableName {
|
||||
// User explicitly provided table name. This is a courtesy protection mechanism
|
||||
err := fmt.Errorf("User commanded 'no-throttle' on %s, but migrated table is %s; ignoring request.", arg, this.migrationContext.OriginalTableName)
|
||||
return NoPrintStatusRule, err
|
||||
}
|
||||
atomic.StoreInt64(&this.migrationContext.ThrottleCommandedByUser, 0)
|
||||
return ForcePrintStatusAndHintRule, nil
|
||||
}
|
||||
@ -347,16 +319,7 @@ help # This message
|
||||
}
|
||||
case "panic":
|
||||
{
|
||||
if arg == "" && this.migrationContext.ForceNamedPanicCommand {
|
||||
err := fmt.Errorf("User commanded 'panic' without specifying table name, but --force-named-panic is set")
|
||||
return NoPrintStatusRule, err
|
||||
}
|
||||
if arg != "" && arg != this.migrationContext.OriginalTableName {
|
||||
// User explicitly provided table name. This is a courtesy protection mechanism
|
||||
err := fmt.Errorf("User commanded 'panic' on %s, but migrated table is %s; ignoring request.", arg, this.migrationContext.OriginalTableName)
|
||||
return NoPrintStatusRule, err
|
||||
}
|
||||
err := fmt.Errorf("User commanded 'panic'. The migration will be aborted without cleanup. Please drop the gh-ost tables before trying again.")
|
||||
err := fmt.Errorf("User commanded 'panic'. I will now panic, without cleanup. PANIC!")
|
||||
this.migrationContext.PanicAbort <- err
|
||||
return NoPrintStatusRule, err
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -16,7 +16,8 @@ import (
|
||||
"github.com/github/gh-ost/go/binlog"
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
|
||||
"github.com/openark/golib/sqlutils"
|
||||
"github.com/outbrain/golib/log"
|
||||
"github.com/outbrain/golib/sqlutils"
|
||||
)
|
||||
|
||||
type BinlogEventListener struct {
|
||||
@ -42,7 +43,6 @@ type EventsStreamer struct {
|
||||
listenersMutex *sync.Mutex
|
||||
eventsChannel chan *binlog.BinlogEntry
|
||||
binlogReader *binlog.GoMySQLReader
|
||||
name string
|
||||
}
|
||||
|
||||
func NewEventsStreamer(migrationContext *base.MigrationContext) *EventsStreamer {
|
||||
@ -52,13 +52,13 @@ func NewEventsStreamer(migrationContext *base.MigrationContext) *EventsStreamer
|
||||
listeners: [](*BinlogEventListener){},
|
||||
listenersMutex: &sync.Mutex{},
|
||||
eventsChannel: make(chan *binlog.BinlogEntry, EventsChannelBufferSize),
|
||||
name: "streamer",
|
||||
}
|
||||
}
|
||||
|
||||
// AddListener registers a new listener for binlog events, on a per-table basis
|
||||
func (this *EventsStreamer) AddListener(
|
||||
async bool, databaseName string, tableName string, onDmlEvent func(event *binlog.BinlogDMLEvent) error) (err error) {
|
||||
|
||||
this.listenersMutex.Lock()
|
||||
defer this.listenersMutex.Unlock()
|
||||
|
||||
@ -86,10 +86,10 @@ func (this *EventsStreamer) notifyListeners(binlogEvent *binlog.BinlogDMLEvent)
|
||||
|
||||
for _, listener := range this.listeners {
|
||||
listener := listener
|
||||
if !strings.EqualFold(listener.databaseName, binlogEvent.DatabaseName) {
|
||||
if strings.ToLower(listener.databaseName) != strings.ToLower(binlogEvent.DatabaseName) {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(listener.tableName, binlogEvent.TableName) {
|
||||
if strings.ToLower(listener.tableName) != strings.ToLower(binlogEvent.TableName) {
|
||||
continue
|
||||
}
|
||||
if listener.async {
|
||||
@ -107,7 +107,7 @@ func (this *EventsStreamer) InitDBConnections() (err error) {
|
||||
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, EventsStreamerUri); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := base.ValidateConnection(this.db, this.connectionConfig, this.migrationContext, this.name); err != nil {
|
||||
if _, err := base.ValidateConnection(this.db, this.connectionConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := this.readCurrentBinlogCoordinates(); err != nil {
|
||||
@ -122,7 +122,10 @@ func (this *EventsStreamer) InitDBConnections() (err error) {
|
||||
|
||||
// initBinlogReader creates and connects the reader: we hook up to a MySQL server as a replica
|
||||
func (this *EventsStreamer) initBinlogReader(binlogCoordinates *mysql.BinlogCoordinates) error {
|
||||
goMySQLReader := binlog.NewGoMySQLReader(this.migrationContext)
|
||||
goMySQLReader, err := binlog.NewGoMySQLReader(this.migrationContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := goMySQLReader.ConnectBinlogStreamer(*binlogCoordinates); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -157,7 +160,7 @@ func (this *EventsStreamer) readCurrentBinlogCoordinates() error {
|
||||
if !foundMasterStatus {
|
||||
return fmt.Errorf("Got no results from SHOW MASTER STATUS. Bailing out")
|
||||
}
|
||||
this.migrationContext.Log.Debugf("Streamer binlog coordinates: %+v", *this.initialBinlogCoordinates)
|
||||
log.Debugf("Streamer binlog coordinates: %+v", *this.initialBinlogCoordinates)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -183,7 +186,7 @@ func (this *EventsStreamer) StreamEvents(canStopStreaming func() bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
this.migrationContext.Log.Infof("StreamEvents encountered unexpected error: %+v", err)
|
||||
log.Infof("StreamEvents encountered unexpected error: %+v", err)
|
||||
this.migrationContext.MarkPointOfInterest()
|
||||
time.Sleep(ReconnectStreamerSleepSeconds * time.Second)
|
||||
|
||||
@ -199,7 +202,7 @@ func (this *EventsStreamer) StreamEvents(canStopStreaming func() bool) error {
|
||||
|
||||
// Reposition at same binlog file.
|
||||
lastAppliedRowsEventHint = this.binlogReader.LastAppliedRowsEventHint
|
||||
this.migrationContext.Log.Infof("Reconnecting... Will resume at %+v", lastAppliedRowsEventHint)
|
||||
log.Infof("Reconnecting... Will resume at %+v", lastAppliedRowsEventHint)
|
||||
if err := this.initBinlogReader(this.GetReconnectBinlogCoordinates()); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -210,10 +213,11 @@ func (this *EventsStreamer) StreamEvents(canStopStreaming func() bool) error {
|
||||
|
||||
func (this *EventsStreamer) Close() (err error) {
|
||||
err = this.binlogReader.Close()
|
||||
this.migrationContext.Log.Infof("Closed streamer connection. err=%+v", err)
|
||||
log.Infof("Closed streamer connection. err=%+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *EventsStreamer) Teardown() {
|
||||
this.db.Close()
|
||||
return
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@ -16,25 +15,24 @@ import (
|
||||
"github.com/github/gh-ost/go/base"
|
||||
"github.com/github/gh-ost/go/mysql"
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
"github.com/outbrain/golib/log"
|
||||
)
|
||||
|
||||
var (
|
||||
httpStatusMessages = map[int]string{
|
||||
httpStatusMessages map[int]string = map[int]string{
|
||||
200: "OK",
|
||||
404: "Not found",
|
||||
417: "Expectation failed",
|
||||
429: "Too many requests",
|
||||
500: "Internal server error",
|
||||
-1: "Connection error",
|
||||
}
|
||||
// See https://github.com/github/freno/blob/master/doc/http.md
|
||||
httpStatusFrenoMessages = map[int]string{
|
||||
httpStatusFrenoMessages map[int]string = map[int]string{
|
||||
200: "OK",
|
||||
404: "freno: unknown metric",
|
||||
417: "freno: access forbidden",
|
||||
429: "freno: threshold exceeded",
|
||||
500: "freno: internal error",
|
||||
-1: "freno: connection error",
|
||||
}
|
||||
)
|
||||
|
||||
@ -43,22 +41,16 @@ const frenoMagicHint = "freno"
|
||||
// Throttler collects metrics related to throttling and makes informed decision
|
||||
// whether throttling should take place.
|
||||
type Throttler struct {
|
||||
appVersion string
|
||||
migrationContext *base.MigrationContext
|
||||
applier *Applier
|
||||
httpClient *http.Client
|
||||
httpClientTimeout time.Duration
|
||||
inspector *Inspector
|
||||
finishedMigrating int64
|
||||
}
|
||||
|
||||
func NewThrottler(migrationContext *base.MigrationContext, applier *Applier, inspector *Inspector, appVersion string) *Throttler {
|
||||
func NewThrottler(migrationContext *base.MigrationContext, applier *Applier, inspector *Inspector) *Throttler {
|
||||
return &Throttler{
|
||||
appVersion: appVersion,
|
||||
migrationContext: migrationContext,
|
||||
applier: applier,
|
||||
httpClient: &http.Client{},
|
||||
httpClientTimeout: time.Duration(migrationContext.ThrottleHTTPTimeoutMillis) * time.Millisecond,
|
||||
inspector: inspector,
|
||||
finishedMigrating: 0,
|
||||
}
|
||||
@ -92,7 +84,6 @@ func (this *Throttler) shouldThrottle() (result bool, reason string, reasonHint
|
||||
if statusCode != 0 && statusCode != http.StatusOK {
|
||||
return true, this.throttleHttpMessage(int(statusCode)), base.NoThrottleReasonHint
|
||||
}
|
||||
|
||||
// Replication lag throttle
|
||||
maxLagMillisecondsThrottleThreshold := atomic.LoadInt64(&this.migrationContext.MaxLagMillisecondsThrottleThreshold)
|
||||
lag := atomic.LoadInt64(&this.migrationContext.CurrentLag)
|
||||
@ -129,7 +120,7 @@ func parseChangelogHeartbeat(heartbeatValue string) (lag time.Duration, err erro
|
||||
// parseChangelogHeartbeat parses a string timestamp and deduces replication lag
|
||||
func (this *Throttler) parseChangelogHeartbeat(heartbeatValue string) (err error) {
|
||||
if lag, err := parseChangelogHeartbeat(heartbeatValue); err != nil {
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
} else {
|
||||
atomic.StoreInt64(&this.migrationContext.CurrentLag, int64(lag))
|
||||
return nil
|
||||
@ -149,15 +140,15 @@ func (this *Throttler) collectReplicationLag(firstThrottlingCollected chan<- boo
|
||||
if this.migrationContext.TestOnReplica || this.migrationContext.MigrateOnReplica {
|
||||
// when running on replica, the heartbeat injection is also done on the replica.
|
||||
// This means we will always get a good heartbeat value.
|
||||
// When running on replica, we should instead check the `SHOW SLAVE STATUS` output.
|
||||
if lag, err := mysql.GetReplicationLagFromSlaveStatus(this.inspector.informationSchemaDb); err != nil {
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
// When runnign on replica, we should instead check the `SHOW SLAVE STATUS` output.
|
||||
if lag, err := mysql.GetReplicationLag(this.inspector.informationSchemaDb, this.inspector.connectionConfig); err != nil {
|
||||
return log.Errore(err)
|
||||
} else {
|
||||
atomic.StoreInt64(&this.migrationContext.CurrentLag, int64(lag))
|
||||
}
|
||||
} else {
|
||||
if heartbeatValue, err := this.inspector.readChangelogState("heartbeat"); err != nil {
|
||||
return this.migrationContext.Log.Errore(err)
|
||||
return log.Errore(err)
|
||||
} else {
|
||||
this.parseChangelogHeartbeat(heartbeatValue)
|
||||
}
|
||||
@ -168,9 +159,8 @@ func (this *Throttler) collectReplicationLag(firstThrottlingCollected chan<- boo
|
||||
collectFunc()
|
||||
firstThrottlingCollected <- true
|
||||
|
||||
ticker := time.NewTicker(time.Duration(this.migrationContext.HeartbeatIntervalMilliseconds) * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
ticker := time.Tick(time.Duration(this.migrationContext.HeartbeatIntervalMilliseconds) * time.Millisecond)
|
||||
for range ticker {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
}
|
||||
@ -180,6 +170,7 @@ func (this *Throttler) collectReplicationLag(firstThrottlingCollected chan<- boo
|
||||
|
||||
// collectControlReplicasLag polls all the control replicas to get maximum lag value
|
||||
func (this *Throttler) collectControlReplicasLag() {
|
||||
|
||||
if atomic.LoadInt64(&this.migrationContext.HibernateUntil) > 0 {
|
||||
return
|
||||
}
|
||||
@ -195,12 +186,9 @@ func (this *Throttler) collectControlReplicasLag() {
|
||||
dbUri := connectionConfig.GetDBUri("information_schema")
|
||||
|
||||
var heartbeatValue string
|
||||
db, _, err := mysql.GetDB(this.migrationContext.Uuid, dbUri)
|
||||
if err != nil {
|
||||
if db, _, err := mysql.GetDB(this.migrationContext.Uuid, dbUri); err != nil {
|
||||
return lag, err
|
||||
}
|
||||
|
||||
if err := db.QueryRow(replicationLagQuery).Scan(&heartbeatValue); err != nil {
|
||||
} else if err = db.QueryRow(replicationLagQuery).Scan(&heartbeatValue); err != nil {
|
||||
return lag, err
|
||||
}
|
||||
|
||||
@ -244,14 +232,12 @@ func (this *Throttler) collectControlReplicasLag() {
|
||||
}
|
||||
this.migrationContext.SetControlReplicasLagResult(readControlReplicasLag())
|
||||
}
|
||||
|
||||
aggressiveTicker := time.Tick(100 * time.Millisecond)
|
||||
relaxedFactor := 10
|
||||
counter := 0
|
||||
shouldReadLagAggressively := false
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
for range aggressiveTicker {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
}
|
||||
@ -294,53 +280,24 @@ func (this *Throttler) collectThrottleHTTPStatus(firstThrottlingCollected chan<-
|
||||
if url == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), this.httpClientTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
|
||||
resp, err := http.Head(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("gh-ost/%s", this.appVersion))
|
||||
|
||||
resp, err := this.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
atomic.StoreInt64(&this.migrationContext.ThrottleHTTPStatusCode, int64(resp.StatusCode))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err := collectFunc()
|
||||
if err != nil {
|
||||
// If not told to ignore errors, we'll throttle on HTTP connection issues
|
||||
if !this.migrationContext.IgnoreHTTPErrors {
|
||||
atomic.StoreInt64(&this.migrationContext.ThrottleHTTPStatusCode, int64(-1))
|
||||
}
|
||||
}
|
||||
|
||||
collectFunc()
|
||||
firstThrottlingCollected <- true
|
||||
|
||||
collectInterval := time.Duration(this.migrationContext.ThrottleHTTPIntervalMillis) * time.Millisecond
|
||||
ticker := time.NewTicker(collectInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
ticker := time.Tick(100 * time.Millisecond)
|
||||
for range ticker {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sleep, err := collectFunc()
|
||||
if err != nil {
|
||||
// If not told to ignore errors, we'll throttle on HTTP connection issues
|
||||
if !this.migrationContext.IgnoreHTTPErrors {
|
||||
atomic.StoreInt64(&this.migrationContext.ThrottleHTTPStatusCode, int64(-1))
|
||||
}
|
||||
}
|
||||
|
||||
if sleep {
|
||||
if sleep, _ := collectFunc(); sleep {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
@ -373,7 +330,7 @@ func (this *Throttler) collectGeneralThrottleMetrics() error {
|
||||
hibernateDuration := time.Duration(this.migrationContext.CriticalLoadHibernateSeconds) * time.Second
|
||||
hibernateUntilTime := time.Now().Add(hibernateDuration)
|
||||
atomic.StoreInt64(&this.migrationContext.HibernateUntil, hibernateUntilTime.UnixNano())
|
||||
this.migrationContext.Log.Errorf("critical-load met: %s=%d, >=%d. Will hibernate for the duration of %+v, until %+v", variableName, value, threshold, hibernateDuration, hibernateUntilTime)
|
||||
log.Errorf("critical-load met: %s=%d, >=%d. Will hibernate for the duration of %+v, until %+v", variableName, value, threshold, hibernateDuration, hibernateUntilTime)
|
||||
go func() {
|
||||
time.Sleep(hibernateDuration)
|
||||
this.migrationContext.SetThrottleGeneralCheckResult(base.NewThrottleCheckResult(true, "leaving hibernation", base.LeavingHibernationThrottleReasonHint))
|
||||
@ -386,7 +343,7 @@ func (this *Throttler) collectGeneralThrottleMetrics() error {
|
||||
this.migrationContext.PanicAbort <- fmt.Errorf("critical-load met: %s=%d, >=%d", variableName, value, threshold)
|
||||
}
|
||||
if criticalLoadMet && this.migrationContext.CriticalLoadIntervalMilliseconds > 0 {
|
||||
this.migrationContext.Log.Errorf("critical-load met once: %s=%d, >=%d. Will check again in %d millis", variableName, value, threshold, this.migrationContext.CriticalLoadIntervalMilliseconds)
|
||||
log.Errorf("critical-load met once: %s=%d, >=%d. Will check again in %d millis", variableName, value, threshold, this.migrationContext.CriticalLoadIntervalMilliseconds)
|
||||
go func() {
|
||||
timer := time.NewTimer(time.Millisecond * time.Duration(this.migrationContext.CriticalLoadIntervalMilliseconds))
|
||||
<-timer.C
|
||||
@ -446,9 +403,8 @@ func (this *Throttler) initiateThrottlerCollection(firstThrottlingCollected chan
|
||||
this.collectGeneralThrottleMetrics()
|
||||
firstThrottlingCollected <- true
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
throttlerMetricsTick := time.Tick(1 * time.Second)
|
||||
for range throttlerMetricsTick {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
}
|
||||
@ -459,7 +415,9 @@ func (this *Throttler) initiateThrottlerCollection(firstThrottlingCollected chan
|
||||
}
|
||||
|
||||
// initiateThrottlerChecks initiates the throttle ticker and sets the basic behavior of throttling.
|
||||
func (this *Throttler) initiateThrottlerChecks() {
|
||||
func (this *Throttler) initiateThrottlerChecks() error {
|
||||
throttlerTick := time.Tick(100 * time.Millisecond)
|
||||
|
||||
throttlerFunction := func() {
|
||||
alreadyThrottling, currentReason, _ := this.migrationContext.IsThrottled()
|
||||
shouldThrottle, throttleReason, throttleReasonHint := this.shouldThrottle()
|
||||
@ -476,15 +434,14 @@ func (this *Throttler) initiateThrottlerChecks() {
|
||||
this.migrationContext.SetThrottled(shouldThrottle, throttleReason, throttleReasonHint)
|
||||
}
|
||||
throttlerFunction()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
for range throttlerTick {
|
||||
if atomic.LoadInt64(&this.finishedMigrating) > 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
throttlerFunction()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// throttle sees if throttling needs take place, and if so, continuously sleeps (blocks)
|
||||
@ -504,6 +461,6 @@ func (this *Throttler) throttle(onThrottled func()) {
|
||||
}
|
||||
|
||||
func (this *Throttler) Teardown() {
|
||||
this.migrationContext.Log.Debugf("Tearing down...")
|
||||
log.Debugf("Tearing down...")
|
||||
atomic.StoreInt64(&this.finishedMigrating, 1)
|
||||
}
|
||||
|
@ -1,21 +1,36 @@
|
||||
/*
|
||||
Copyright 2015 Shlomi Noach, courtesy Booking.com
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var detachPattern *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
detachPattern, _ = regexp.Compile(`//([^/:]+):([\d]+)`) // e.g. `//binlog.01234:567890`
|
||||
}
|
||||
|
||||
type BinlogType int
|
||||
|
||||
const (
|
||||
BinaryLog BinlogType = iota
|
||||
RelayLog
|
||||
)
|
||||
|
||||
// BinlogCoordinates described binary log coordinates in the form of log file & log position.
|
||||
type BinlogCoordinates struct {
|
||||
LogFile string
|
||||
LogPos int64
|
||||
Type BinlogType
|
||||
}
|
||||
|
||||
// ParseInstanceKey will parse an InstanceKey from a string representation such as 127.0.0.1:3306
|
||||
@ -47,7 +62,7 @@ func (this *BinlogCoordinates) Equals(other *BinlogCoordinates) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return this.LogFile == other.LogFile && this.LogPos == other.LogPos
|
||||
return this.LogFile == other.LogFile && this.LogPos == other.LogPos && this.Type == other.Type
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the log file is empty, unnamed
|
||||
@ -72,5 +87,76 @@ func (this *BinlogCoordinates) SmallerThanOrEquals(other *BinlogCoordinates) boo
|
||||
if this.SmallerThan(other) {
|
||||
return true
|
||||
}
|
||||
return this.LogFile == other.LogFile && this.LogPos == other.LogPos
|
||||
return this.LogFile == other.LogFile && this.LogPos == other.LogPos // No Type comparison
|
||||
}
|
||||
|
||||
// FileSmallerThan returns true if this coordinate's file is strictly smaller than the other's.
|
||||
func (this *BinlogCoordinates) FileSmallerThan(other *BinlogCoordinates) bool {
|
||||
return this.LogFile < other.LogFile
|
||||
}
|
||||
|
||||
// FileNumberDistance returns the numeric distance between this coordinate's file number and the other's.
|
||||
// Effectively it means "how many rotates/FLUSHes would make these coordinates's file reach the other's"
|
||||
func (this *BinlogCoordinates) FileNumberDistance(other *BinlogCoordinates) int {
|
||||
thisNumber, _ := this.FileNumber()
|
||||
otherNumber, _ := other.FileNumber()
|
||||
return otherNumber - thisNumber
|
||||
}
|
||||
|
||||
// FileNumber returns the numeric value of the file, and the length in characters representing the number in the filename.
|
||||
// Example: FileNumber() of mysqld.log.000789 is (789, 6)
|
||||
func (this *BinlogCoordinates) FileNumber() (int, int) {
|
||||
tokens := strings.Split(this.LogFile, ".")
|
||||
numPart := tokens[len(tokens)-1]
|
||||
numLen := len(numPart)
|
||||
fileNum, err := strconv.Atoi(numPart)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return fileNum, numLen
|
||||
}
|
||||
|
||||
// PreviousFileCoordinatesBy guesses the filename of the previous binlog/relaylog, by given offset (number of files back)
|
||||
func (this *BinlogCoordinates) PreviousFileCoordinatesBy(offset int) (BinlogCoordinates, error) {
|
||||
result := BinlogCoordinates{LogPos: 0, Type: this.Type}
|
||||
|
||||
fileNum, numLen := this.FileNumber()
|
||||
if fileNum == 0 {
|
||||
return result, errors.New("Log file number is zero, cannot detect previous file")
|
||||
}
|
||||
newNumStr := fmt.Sprintf("%d", (fileNum - offset))
|
||||
newNumStr = strings.Repeat("0", numLen-len(newNumStr)) + newNumStr
|
||||
|
||||
tokens := strings.Split(this.LogFile, ".")
|
||||
tokens[len(tokens)-1] = newNumStr
|
||||
result.LogFile = strings.Join(tokens, ".")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PreviousFileCoordinates guesses the filename of the previous binlog/relaylog
|
||||
func (this *BinlogCoordinates) PreviousFileCoordinates() (BinlogCoordinates, error) {
|
||||
return this.PreviousFileCoordinatesBy(1)
|
||||
}
|
||||
|
||||
// PreviousFileCoordinates guesses the filename of the previous binlog/relaylog
|
||||
func (this *BinlogCoordinates) NextFileCoordinates() (BinlogCoordinates, error) {
|
||||
result := BinlogCoordinates{LogPos: 0, Type: this.Type}
|
||||
|
||||
fileNum, numLen := this.FileNumber()
|
||||
newNumStr := fmt.Sprintf("%d", (fileNum + 1))
|
||||
newNumStr = strings.Repeat("0", numLen-len(newNumStr)) + newNumStr
|
||||
|
||||
tokens := strings.Split(this.LogFile, ".")
|
||||
tokens[len(tokens)-1] = newNumStr
|
||||
result.LogFile = strings.Join(tokens, ".")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FileSmallerThan returns true if this coordinate's file is strictly smaller than the other's.
|
||||
func (this *BinlogCoordinates) DetachedCoordinates() (isDetached bool, detachedLogFile string, detachedLogPos string) {
|
||||
detachedCoordinatesSubmatch := detachPattern.FindStringSubmatch(this.LogFile)
|
||||
if len(detachedCoordinatesSubmatch) == 0 {
|
||||
return false, "", ""
|
||||
}
|
||||
return true, detachedCoordinatesSubmatch[1], detachedCoordinatesSubmatch[2]
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ package mysql
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -37,6 +37,57 @@ func TestBinlogCoordinates(t *testing.T) {
|
||||
test.S(t).ExpectTrue(c1.SmallerThanOrEquals(&c3))
|
||||
}
|
||||
|
||||
func TestBinlogNext(t *testing.T) {
|
||||
c1 := BinlogCoordinates{LogFile: "mysql-bin.00017", LogPos: 104}
|
||||
cres, err := c1.NextFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql-bin.00018")
|
||||
|
||||
c2 := BinlogCoordinates{LogFile: "mysql-bin.00099", LogPos: 104}
|
||||
cres, err = c2.NextFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql-bin.00100")
|
||||
|
||||
c3 := BinlogCoordinates{LogFile: "mysql.00.prod.com.00099", LogPos: 104}
|
||||
cres, err = c3.NextFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql.00.prod.com.00100")
|
||||
}
|
||||
|
||||
func TestBinlogPrevious(t *testing.T) {
|
||||
c1 := BinlogCoordinates{LogFile: "mysql-bin.00017", LogPos: 104}
|
||||
cres, err := c1.PreviousFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql-bin.00016")
|
||||
|
||||
c2 := BinlogCoordinates{LogFile: "mysql-bin.00100", LogPos: 104}
|
||||
cres, err = c2.PreviousFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql-bin.00099")
|
||||
|
||||
c3 := BinlogCoordinates{LogFile: "mysql.00.prod.com.00100", LogPos: 104}
|
||||
cres, err = c3.PreviousFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(c1.Type, cres.Type)
|
||||
test.S(t).ExpectEquals(cres.LogFile, "mysql.00.prod.com.00099")
|
||||
|
||||
c4 := BinlogCoordinates{LogFile: "mysql.00.prod.com.00000", LogPos: 104}
|
||||
_, err = c4.PreviousFileCoordinates()
|
||||
|
||||
test.S(t).ExpectNotNil(err)
|
||||
}
|
||||
|
||||
func TestBinlogCoordinatesAsKey(t *testing.T) {
|
||||
m := make(map[BinlogCoordinates]bool)
|
||||
|
||||
@ -52,3 +103,20 @@ func TestBinlogCoordinatesAsKey(t *testing.T) {
|
||||
|
||||
test.S(t).ExpectEquals(len(m), 3)
|
||||
}
|
||||
|
||||
func TestBinlogFileNumber(t *testing.T) {
|
||||
c1 := BinlogCoordinates{LogFile: "mysql-bin.00017", LogPos: 104}
|
||||
c2 := BinlogCoordinates{LogFile: "mysql-bin.00022", LogPos: 104}
|
||||
|
||||
test.S(t).ExpectEquals(c1.FileNumberDistance(&c1), 0)
|
||||
test.S(t).ExpectEquals(c1.FileNumberDistance(&c2), 5)
|
||||
test.S(t).ExpectEquals(c2.FileNumberDistance(&c1), -5)
|
||||
}
|
||||
|
||||
func TestBinlogFileNumberDistance(t *testing.T) {
|
||||
c1 := BinlogCoordinates{LogFile: "mysql-bin.00017", LogPos: 104}
|
||||
fileNum, numLen := c1.FileNumber()
|
||||
|
||||
test.S(t).ExpectEquals(fileNum, 17)
|
||||
test.S(t).ExpectEquals(numLen, 5)
|
||||
}
|
||||
|
@ -1,35 +1,21 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
TLS_CONFIG_KEY = "ghost"
|
||||
)
|
||||
|
||||
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
|
||||
type ConnectionConfig struct {
|
||||
Key InstanceKey
|
||||
User string
|
||||
Password string
|
||||
ImpliedKey *InstanceKey
|
||||
tlsConfig *tls.Config
|
||||
Timeout float64
|
||||
TransactionIsolation string
|
||||
Key InstanceKey
|
||||
User string
|
||||
Password string
|
||||
ImpliedKey *InstanceKey
|
||||
}
|
||||
|
||||
func NewConnectionConfig() *ConnectionConfig {
|
||||
@ -43,12 +29,9 @@ func NewConnectionConfig() *ConnectionConfig {
|
||||
// DuplicateCredentials creates a new connection config with given key and with same credentials as this config
|
||||
func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig {
|
||||
config := &ConnectionConfig{
|
||||
Key: key,
|
||||
User: this.User,
|
||||
Password: this.Password,
|
||||
tlsConfig: this.tlsConfig,
|
||||
Timeout: this.Timeout,
|
||||
TransactionIsolation: this.TransactionIsolation,
|
||||
Key: key,
|
||||
User: this.User,
|
||||
Password: this.Password,
|
||||
}
|
||||
config.ImpliedKey = &config.Key
|
||||
return config
|
||||
@ -59,55 +42,13 @@ func (this *ConnectionConfig) Duplicate() *ConnectionConfig {
|
||||
}
|
||||
|
||||
func (this *ConnectionConfig) String() string {
|
||||
return fmt.Sprintf("%s, user=%s, usingTLS=%t", this.Key.DisplayString(), this.User, this.tlsConfig != nil)
|
||||
return fmt.Sprintf("%s, user=%s", this.Key.DisplayString(), this.User)
|
||||
}
|
||||
|
||||
func (this *ConnectionConfig) Equals(other *ConnectionConfig) bool {
|
||||
return this.Key.Equals(&other.Key) || this.ImpliedKey.Equals(other.ImpliedKey)
|
||||
}
|
||||
|
||||
func (this *ConnectionConfig) UseTLS(caCertificatePath, clientCertificate, clientKey string, allowInsecure bool) error {
|
||||
var rootCertPool *x509.CertPool
|
||||
var certs []tls.Certificate
|
||||
var err error
|
||||
|
||||
if caCertificatePath == "" {
|
||||
rootCertPool, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
rootCertPool = x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(caCertificatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||
return errors.New("could not add ca certificate to cert pool")
|
||||
}
|
||||
}
|
||||
if clientCertificate != "" || clientKey != "" {
|
||||
cert, err := tls.LoadX509KeyPair(clientCertificate, clientKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certs = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
this.tlsConfig = &tls.Config{
|
||||
ServerName: this.Key.Hostname,
|
||||
Certificates: certs,
|
||||
RootCAs: rootCertPool,
|
||||
InsecureSkipVerify: allowInsecure,
|
||||
}
|
||||
|
||||
return mysql.RegisterTLSConfig(TLS_CONFIG_KEY, this.tlsConfig)
|
||||
}
|
||||
|
||||
func (this *ConnectionConfig) TLSConfig() *tls.Config {
|
||||
return this.tlsConfig
|
||||
}
|
||||
|
||||
func (this *ConnectionConfig) GetDBUri(databaseName string) string {
|
||||
hostname := this.Key.Hostname
|
||||
var ip = net.ParseIP(hostname)
|
||||
@ -115,23 +56,5 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
|
||||
// Wrap IPv6 literals in square brackets
|
||||
hostname = fmt.Sprintf("[%s]", hostname)
|
||||
}
|
||||
|
||||
// go-mysql-driver defaults to false if tls param is not provided; explicitly setting here to
|
||||
// simplify construction of the DSN below.
|
||||
tlsOption := "false"
|
||||
if this.tlsConfig != nil {
|
||||
tlsOption = TLS_CONFIG_KEY
|
||||
}
|
||||
connectionParams := []string{
|
||||
"autocommit=true",
|
||||
"charset=utf8mb4,utf8,latin1",
|
||||
"interpolateParams=true",
|
||||
fmt.Sprintf("tls=%s", tlsOption),
|
||||
fmt.Sprintf("transaction_isolation=%q", this.TransactionIsolation),
|
||||
fmt.Sprintf("timeout=%fs", this.Timeout),
|
||||
fmt.Sprintf("readTimeout=%fs", this.Timeout),
|
||||
fmt.Sprintf("writeTimeout=%fs", this.Timeout),
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", this.User, this.Password, hostname, this.Key.Port, databaseName, strings.Join(connectionParams, "&"))
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1", this.User, this.Password, hostname, this.Key.Port, databaseName)
|
||||
}
|
||||
|
@ -1,20 +1,15 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
)
|
||||
|
||||
const (
|
||||
transactionIsolation = "REPEATABLE-READ"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -29,7 +24,6 @@ func TestNewConnectionConfig(t *testing.T) {
|
||||
test.S(t).ExpectEquals(c.ImpliedKey.Port, 0)
|
||||
test.S(t).ExpectEquals(c.User, "")
|
||||
test.S(t).ExpectEquals(c.Password, "")
|
||||
test.S(t).ExpectEquals(c.TransactionIsolation, "")
|
||||
}
|
||||
|
||||
func TestDuplicateCredentials(t *testing.T) {
|
||||
@ -37,11 +31,6 @@ func TestDuplicateCredentials(t *testing.T) {
|
||||
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||
c.User = "gromit"
|
||||
c.Password = "penguin"
|
||||
c.tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "feathers",
|
||||
}
|
||||
c.TransactionIsolation = transactionIsolation
|
||||
|
||||
dup := c.DuplicateCredentials(InstanceKey{Hostname: "otherhost", Port: 3310})
|
||||
test.S(t).ExpectEquals(dup.Key.Hostname, "otherhost")
|
||||
@ -50,8 +39,6 @@ func TestDuplicateCredentials(t *testing.T) {
|
||||
test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3310)
|
||||
test.S(t).ExpectEquals(dup.User, "gromit")
|
||||
test.S(t).ExpectEquals(dup.Password, "penguin")
|
||||
test.S(t).ExpectEquals(dup.tlsConfig, c.tlsConfig)
|
||||
test.S(t).ExpectEquals(dup.TransactionIsolation, c.TransactionIsolation)
|
||||
}
|
||||
|
||||
func TestDuplicate(t *testing.T) {
|
||||
@ -59,7 +46,6 @@ func TestDuplicate(t *testing.T) {
|
||||
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||
c.User = "gromit"
|
||||
c.Password = "penguin"
|
||||
c.TransactionIsolation = transactionIsolation
|
||||
|
||||
dup := c.Duplicate()
|
||||
test.S(t).ExpectEquals(dup.Key.Hostname, "myhost")
|
||||
@ -68,30 +54,4 @@ func TestDuplicate(t *testing.T) {
|
||||
test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3306)
|
||||
test.S(t).ExpectEquals(dup.User, "gromit")
|
||||
test.S(t).ExpectEquals(dup.Password, "penguin")
|
||||
test.S(t).ExpectEquals(dup.TransactionIsolation, transactionIsolation)
|
||||
}
|
||||
|
||||
func TestGetDBUri(t *testing.T) {
|
||||
c := NewConnectionConfig()
|
||||
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||
c.User = "gromit"
|
||||
c.Password = "penguin"
|
||||
c.Timeout = 1.2345
|
||||
c.TransactionIsolation = transactionIsolation
|
||||
|
||||
uri := c.GetDBUri("test")
|
||||
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=false&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
|
||||
}
|
||||
|
||||
func TestGetDBUriWithTLSSetup(t *testing.T) {
|
||||
c := NewConnectionConfig()
|
||||
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||
c.User = "gromit"
|
||||
c.Password = "penguin"
|
||||
c.Timeout = 1.2345
|
||||
c.tlsConfig = &tls.Config{}
|
||||
c.TransactionIsolation = transactionIsolation
|
||||
|
||||
uri := c.GetDBUri("test")
|
||||
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=ghost&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 Shlomi Noach, courtesy Booking.com
|
||||
Copyright 2022 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -8,21 +7,12 @@ package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultInstancePort = 3306
|
||||
|
||||
var (
|
||||
ipv4HostPortRegexp = regexp.MustCompile("^([^:]+):([0-9]+)$")
|
||||
ipv4HostRegexp = regexp.MustCompile("^([^:]+)$")
|
||||
|
||||
// e.g. [2001:db8:1f70::999:de8:7648:6e8]:3308
|
||||
ipv6HostPortRegexp = regexp.MustCompile("^\\[([:0-9a-fA-F]+)\\]:([0-9]+)$") //nolint:gosimple
|
||||
// e.g. 2001:db8:1f70::999:de8:7648:6e8
|
||||
ipv6HostRegexp = regexp.MustCompile("^([:0-9a-fA-F]+)$")
|
||||
const (
|
||||
DefaultInstancePort = 3306
|
||||
)
|
||||
|
||||
// InstanceKey is an instance indicator, identified by hostname and port
|
||||
@ -35,34 +25,25 @@ const detachHint = "//"
|
||||
|
||||
// ParseInstanceKey will parse an InstanceKey from a string representation such as 127.0.0.1:3306
|
||||
func NewRawInstanceKey(hostPort string) (*InstanceKey, error) {
|
||||
var hostname, port string
|
||||
if submatch := ipv4HostPortRegexp.FindStringSubmatch(hostPort); len(submatch) > 0 {
|
||||
hostname = submatch[1]
|
||||
port = submatch[2]
|
||||
} else if submatch := ipv4HostRegexp.FindStringSubmatch(hostPort); len(submatch) > 0 {
|
||||
hostname = submatch[1]
|
||||
} else if submatch := ipv6HostPortRegexp.FindStringSubmatch(hostPort); len(submatch) > 0 {
|
||||
hostname = submatch[1]
|
||||
port = submatch[2]
|
||||
} else if submatch := ipv6HostRegexp.FindStringSubmatch(hostPort); len(submatch) > 0 {
|
||||
hostname = submatch[1]
|
||||
} else {
|
||||
return nil, fmt.Errorf("Cannot parse address: %s", hostPort)
|
||||
tokens := strings.SplitN(hostPort, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
return nil, fmt.Errorf("Cannot parse InstanceKey from %s. Expected format is host:port", hostPort)
|
||||
}
|
||||
instanceKey := &InstanceKey{Hostname: hostname, Port: DefaultInstancePort}
|
||||
if port != "" {
|
||||
var err error
|
||||
if instanceKey.Port, err = strconv.Atoi(port); err != nil {
|
||||
return instanceKey, fmt.Errorf("Invalid port: %s", port)
|
||||
}
|
||||
instanceKey := &InstanceKey{Hostname: tokens[0]}
|
||||
var err error
|
||||
if instanceKey.Port, err = strconv.Atoi(tokens[1]); err != nil {
|
||||
return instanceKey, fmt.Errorf("Invalid port: %s", tokens[1])
|
||||
}
|
||||
|
||||
return instanceKey, nil
|
||||
}
|
||||
|
||||
// ParseInstanceKey will parse an InstanceKey from a string representation such as 127.0.0.1:3306.
|
||||
// ParseRawInstanceKeyLoose will parse an InstanceKey from a string representation such as 127.0.0.1:3306.
|
||||
// The port part is optional; there will be no name resolve
|
||||
func ParseInstanceKey(hostPort string) (*InstanceKey, error) {
|
||||
func ParseRawInstanceKeyLoose(hostPort string) (*InstanceKey, error) {
|
||||
if !strings.Contains(hostPort, ":") {
|
||||
return &InstanceKey{Hostname: hostPort, Port: DefaultInstancePort}, nil
|
||||
}
|
||||
return NewRawInstanceKey(hostPort)
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ func (this *InstanceKeyMap) ReadCommaDelimitedList(list string) error {
|
||||
}
|
||||
tokens := strings.Split(list, ",")
|
||||
for _, token := range tokens {
|
||||
key, err := ParseInstanceKey(token)
|
||||
key, err := ParseRawInstanceKeyLoose(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel(log.ERROR)
|
||||
}
|
||||
|
||||
func TestParseInstanceKey(t *testing.T) {
|
||||
{
|
||||
key, err := ParseInstanceKey("myhost:1234")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "myhost")
|
||||
test.S(t).ExpectEquals(key.Port, 1234)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("myhost")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "myhost")
|
||||
test.S(t).ExpectEquals(key.Port, 3306)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("10.0.0.3:3307")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "10.0.0.3")
|
||||
test.S(t).ExpectEquals(key.Port, 3307)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("10.0.0.3")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "10.0.0.3")
|
||||
test.S(t).ExpectEquals(key.Port, 3306)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("[2001:db8:1f70::999:de8:7648:6e8]:3308")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "2001:db8:1f70::999:de8:7648:6e8")
|
||||
test.S(t).ExpectEquals(key.Port, 3308)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("::1")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "::1")
|
||||
test.S(t).ExpectEquals(key.Port, 3306)
|
||||
}
|
||||
{
|
||||
key, err := ParseInstanceKey("0:0:0:0:0:0:0:0")
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(key.Hostname, "0:0:0:0:0:0:0:0")
|
||||
test.S(t).ExpectEquals(key.Port, 3306)
|
||||
}
|
||||
{
|
||||
_, err := ParseInstanceKey("[2001:xxxx:1f70::999:de8:7648:6e8]:3308")
|
||||
test.S(t).ExpectNotNil(err)
|
||||
}
|
||||
{
|
||||
_, err := ParseInstanceKey("10.0.0.4:")
|
||||
test.S(t).ExpectNotNil(err)
|
||||
}
|
||||
{
|
||||
_, err := ParseInstanceKey("10.0.0.4:5.6.7")
|
||||
test.S(t).ExpectNotNil(err)
|
||||
}
|
||||
}
|
@ -8,21 +8,17 @@ package mysql
|
||||
import (
|
||||
gosql "database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/github/gh-ost/go/sql"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
"github.com/openark/golib/sqlutils"
|
||||
"github.com/outbrain/golib/log"
|
||||
"github.com/outbrain/golib/sqlutils"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxTableNameLength = 64
|
||||
MaxReplicationPasswordLength = 32
|
||||
MaxDBPoolConnections = 3
|
||||
)
|
||||
const MaxTableNameLength = 64
|
||||
const MaxReplicationPasswordLength = 32
|
||||
|
||||
type ReplicationLagResult struct {
|
||||
Key InstanceKey
|
||||
@ -42,26 +38,28 @@ func (this *ReplicationLagResult) HasLag() bool {
|
||||
var knownDBs map[string]*gosql.DB = make(map[string]*gosql.DB)
|
||||
var knownDBsMutex = &sync.Mutex{}
|
||||
|
||||
func GetDB(migrationUuid string, mysql_uri string) (db *gosql.DB, exists bool, err error) {
|
||||
func GetDB(migrationUuid string, mysql_uri string) (*gosql.DB, bool, error) {
|
||||
cacheKey := migrationUuid + ":" + mysql_uri
|
||||
|
||||
knownDBsMutex.Lock()
|
||||
defer knownDBsMutex.Unlock()
|
||||
defer func() {
|
||||
knownDBsMutex.Unlock()
|
||||
}()
|
||||
|
||||
if db, exists = knownDBs[cacheKey]; !exists {
|
||||
db, err = gosql.Open("mysql", mysql_uri)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
var exists bool
|
||||
if _, exists = knownDBs[cacheKey]; !exists {
|
||||
if db, err := gosql.Open("mysql", mysql_uri); err == nil {
|
||||
knownDBs[cacheKey] = db
|
||||
} else {
|
||||
return db, exists, err
|
||||
}
|
||||
db.SetMaxOpenConns(MaxDBPoolConnections)
|
||||
db.SetMaxIdleConns(MaxDBPoolConnections)
|
||||
knownDBs[cacheKey] = db
|
||||
}
|
||||
return db, exists, nil
|
||||
return knownDBs[cacheKey], exists, nil
|
||||
}
|
||||
|
||||
// GetReplicationLagFromSlaveStatus returns replication lag for a given db; via SHOW SLAVE STATUS
|
||||
func GetReplicationLagFromSlaveStatus(informationSchemaDb *gosql.DB) (replicationLag time.Duration, err error) {
|
||||
// GetReplicationLag returns replication lag for a given connection config; either by explicit query
|
||||
// or via SHOW SLAVE STATUS
|
||||
func GetReplicationLag(informationSchemaDb *gosql.DB, connectionConfig *ConnectionConfig) (replicationLag time.Duration, err error) {
|
||||
err = sqlutils.QueryRowsMap(informationSchemaDb, `show slave status`, func(m sqlutils.RowMap) error {
|
||||
slaveIORunning := m.GetString("Slave_IO_Running")
|
||||
slaveSQLRunning := m.GetString("Slave_SQL_Running")
|
||||
@ -85,6 +83,9 @@ func GetMasterKeyFromSlaveStatus(connectionConfig *ConnectionConfig) (masterKey
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = sqlutils.QueryRowsMap(db, `show slave status`, func(rowMap sqlutils.RowMap) error {
|
||||
// We wish to recognize the case where the topology's master actually has replication configuration.
|
||||
// This can happen when a DBA issues a `RESET SLAVE` instead of `RESET SLAVE ALL`.
|
||||
@ -97,6 +98,7 @@ func GetMasterKeyFromSlaveStatus(connectionConfig *ConnectionConfig) (masterKey
|
||||
slaveIORunning := rowMap.GetString("Slave_IO_Running")
|
||||
slaveSQLRunning := rowMap.GetString("Slave_SQL_Running")
|
||||
|
||||
//
|
||||
if slaveIORunning != "Yes" || slaveSQLRunning != "Yes" {
|
||||
return fmt.Errorf("Replication on %+v is broken: Slave_IO_Running: %s, Slave_SQL_Running: %s. Please make sure replication runs before using gh-ost.",
|
||||
connectionConfig.Key,
|
||||
@ -176,7 +178,7 @@ func GetInstanceKey(db *gosql.DB) (instanceKey *InstanceKey, err error) {
|
||||
}
|
||||
|
||||
// GetTableColumns reads column list from given table
|
||||
func GetTableColumns(db *gosql.DB, databaseName, tableName string) (*sql.ColumnList, *sql.ColumnList, error) {
|
||||
func GetTableColumns(db *gosql.DB, databaseName, tableName string) (*sql.ColumnList, error) {
|
||||
query := fmt.Sprintf(`
|
||||
show columns from %s.%s
|
||||
`,
|
||||
@ -184,30 +186,18 @@ func GetTableColumns(db *gosql.DB, databaseName, tableName string) (*sql.ColumnL
|
||||
sql.EscapeName(tableName),
|
||||
)
|
||||
columnNames := []string{}
|
||||
virtualColumnNames := []string{}
|
||||
err := sqlutils.QueryRowsMap(db, query, func(rowMap sqlutils.RowMap) error {
|
||||
columnName := rowMap.GetString("Field")
|
||||
columnNames = append(columnNames, columnName)
|
||||
if strings.Contains(rowMap.GetString("Extra"), " GENERATED") {
|
||||
log.Debugf("%s is a generated column", columnName)
|
||||
virtualColumnNames = append(virtualColumnNames, columnName)
|
||||
}
|
||||
columnNames = append(columnNames, rowMap.GetString("Field"))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if len(columnNames) == 0 {
|
||||
return nil, nil, log.Errorf("Found 0 columns on %s.%s. Bailing out",
|
||||
return nil, log.Errorf("Found 0 columns on %s.%s. Bailing out",
|
||||
sql.EscapeName(databaseName),
|
||||
sql.EscapeName(tableName),
|
||||
)
|
||||
}
|
||||
return sql.NewColumnList(columnNames), sql.NewColumnList(virtualColumnNames), nil
|
||||
}
|
||||
|
||||
// Kill executes a KILL QUERY by connection id
|
||||
func Kill(db *gosql.DB, connectionID string) error {
|
||||
_, err := db.Exec(`KILL QUERY %s`, connectionID)
|
||||
return err
|
||||
return sql.NewColumnList(columnNames), nil
|
||||
}
|
||||
|
65
go/os/process.go
Normal file
65
go/os/process.go
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2014 Outbrain Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"github.com/outbrain/golib/log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func execCmd(commandText string, arguments ...string) (*exec.Cmd, string, error) {
|
||||
commandBytes := []byte(commandText)
|
||||
tmpFile, err := ioutil.TempFile("", "gh-ost-process-cmd-")
|
||||
if err != nil {
|
||||
return nil, "", log.Errore(err)
|
||||
}
|
||||
ioutil.WriteFile(tmpFile.Name(), commandBytes, 0644)
|
||||
log.Debugf("execCmd: %s", commandText)
|
||||
shellArguments := append([]string{}, tmpFile.Name())
|
||||
shellArguments = append(shellArguments, arguments...)
|
||||
log.Debugf("%+v", shellArguments)
|
||||
return exec.Command("bash", shellArguments...), tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// CommandRun executes a command
|
||||
func CommandRun(commandText string, arguments ...string) error {
|
||||
cmd, tmpFileName, err := execCmd(commandText, arguments...)
|
||||
defer os.Remove(tmpFileName)
|
||||
if err != nil {
|
||||
return log.Errore(err)
|
||||
}
|
||||
err = cmd.Run()
|
||||
return log.Errore(err)
|
||||
}
|
||||
|
||||
// RunCommandWithOutput executes a command and return output bytes
|
||||
func RunCommandWithOutput(commandText string) ([]byte, error) {
|
||||
cmd, tmpFileName, err := execCmd(commandText)
|
||||
defer os.Remove(tmpFileName)
|
||||
if err != nil {
|
||||
return nil, log.Errore(err)
|
||||
}
|
||||
|
||||
outputBytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, log.Errore(err)
|
||||
}
|
||||
|
||||
return outputBytes, nil
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -15,11 +15,11 @@ type ValueComparisonSign string
|
||||
|
||||
const (
|
||||
LessThanComparisonSign ValueComparisonSign = "<"
|
||||
LessThanOrEqualsComparisonSign ValueComparisonSign = "<="
|
||||
EqualsComparisonSign ValueComparisonSign = "="
|
||||
GreaterThanOrEqualsComparisonSign ValueComparisonSign = ">="
|
||||
GreaterThanComparisonSign ValueComparisonSign = ">"
|
||||
NotEqualsComparisonSign ValueComparisonSign = "!="
|
||||
LessThanOrEqualsComparisonSign = "<="
|
||||
EqualsComparisonSign = "="
|
||||
GreaterThanOrEqualsComparisonSign = ">="
|
||||
GreaterThanComparisonSign = ">"
|
||||
NotEqualsComparisonSign = "!="
|
||||
)
|
||||
|
||||
// EscapeName will escape a db/table/column/... name by wrapping with backticks.
|
||||
@ -33,13 +33,11 @@ func EscapeName(name string) string {
|
||||
}
|
||||
|
||||
func buildColumnsPreparedValues(columns *ColumnList) []string {
|
||||
values := make([]string, columns.Len())
|
||||
values := make([]string, columns.Len(), columns.Len())
|
||||
for i, column := range columns.Columns() {
|
||||
var token string
|
||||
if column.timezoneConversion != nil {
|
||||
token = fmt.Sprintf("convert_tz(?, '%s', '%s')", column.timezoneConversion.ToTimezone, "+00:00")
|
||||
} else if column.enumToTextConversion {
|
||||
token = fmt.Sprintf("ELT(?, %s)", column.EnumValues)
|
||||
} else if column.Type == JSONColumnType {
|
||||
token = "convert(? using utf8mb4)"
|
||||
} else {
|
||||
@ -51,7 +49,7 @@ func buildColumnsPreparedValues(columns *ColumnList) []string {
|
||||
}
|
||||
|
||||
func buildPreparedValues(length int) []string {
|
||||
values := make([]string, length)
|
||||
values := make([]string, length, length)
|
||||
for i := 0; i < length; i++ {
|
||||
values[i] = "?"
|
||||
}
|
||||
@ -59,7 +57,7 @@ func buildPreparedValues(length int) []string {
|
||||
}
|
||||
|
||||
func duplicateNames(names []string) []string {
|
||||
duplicate := make([]string, len(names))
|
||||
duplicate := make([]string, len(names), len(names))
|
||||
copy(duplicate, names)
|
||||
return duplicate
|
||||
}
|
||||
@ -110,8 +108,6 @@ func BuildSetPreparedClause(columns *ColumnList) (result string, err error) {
|
||||
var setToken string
|
||||
if column.timezoneConversion != nil {
|
||||
setToken = fmt.Sprintf("%s=convert_tz(?, '%s', '%s')", EscapeName(column.Name), column.timezoneConversion.ToTimezone, "+00:00")
|
||||
} else if column.enumToTextConversion {
|
||||
setToken = fmt.Sprintf("%s=ELT(?, %s)", EscapeName(column.Name), column.EnumValues)
|
||||
} else if column.Type == JSONColumnType {
|
||||
setToken = fmt.Sprintf("%s=convert(? using utf8mb4)", EscapeName(column.Name))
|
||||
} else {
|
||||
@ -144,12 +140,13 @@ func BuildRangeComparison(columns []string, values []string, args []interface{},
|
||||
comparisons := []string{}
|
||||
|
||||
for i, column := range columns {
|
||||
//
|
||||
value := values[i]
|
||||
rangeComparison, err := BuildValueComparison(column, value, comparisonSign)
|
||||
if err != nil {
|
||||
return "", explodedArgs, err
|
||||
}
|
||||
if i > 0 {
|
||||
if len(columns[0:i]) > 0 {
|
||||
equalitiesComparison, err := BuildEqualsComparison(columns[0:i], values[0:i])
|
||||
if err != nil {
|
||||
return "", explodedArgs, err
|
||||
@ -167,7 +164,7 @@ func BuildRangeComparison(columns []string, values []string, args []interface{},
|
||||
if includeEquals {
|
||||
comparison, err := BuildEqualsComparison(columns, values)
|
||||
if err != nil {
|
||||
return "", explodedArgs, err
|
||||
return "", explodedArgs, nil
|
||||
}
|
||||
comparisons = append(comparisons, comparison)
|
||||
explodedArgs = append(explodedArgs, args...)
|
||||
@ -217,18 +214,14 @@ func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName strin
|
||||
return "", explodedArgs, err
|
||||
}
|
||||
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
|
||||
transactionalClause := ""
|
||||
if transactionalTable {
|
||||
transactionalClause = "lock in share mode"
|
||||
}
|
||||
result = fmt.Sprintf(`
|
||||
insert /* gh-ost %s.%s */ ignore into %s.%s (%s)
|
||||
(select %s from %s.%s force index (%s)
|
||||
where (%s and %s) %s
|
||||
where (%s and %s)
|
||||
)
|
||||
`, databaseName, originalTableName, databaseName, ghostTableName, mappedSharedColumnsListing,
|
||||
sharedColumnsListing, databaseName, originalTableName, uniqueKey,
|
||||
rangeStartComparison, rangeEndComparison, transactionalClause)
|
||||
rangeStartComparison, rangeEndComparison)
|
||||
return result, explodedArgs, nil
|
||||
}
|
||||
|
||||
@ -261,8 +254,8 @@ func BuildUniqueKeyRangeEndPreparedQueryViaOffset(databaseName, tableName string
|
||||
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
|
||||
|
||||
uniqueKeyColumnNames := duplicateNames(uniqueKeyColumns.Names())
|
||||
uniqueKeyColumnAscending := make([]string, len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnDescending := make([]string, len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnAscending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnDescending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
|
||||
for i, column := range uniqueKeyColumns.Columns() {
|
||||
uniqueKeyColumnNames[i] = EscapeName(uniqueKeyColumnNames[i])
|
||||
if column.Type == EnumColumnType {
|
||||
@ -316,8 +309,8 @@ func BuildUniqueKeyRangeEndPreparedQueryViaTemptable(databaseName, tableName str
|
||||
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
|
||||
|
||||
uniqueKeyColumnNames := duplicateNames(uniqueKeyColumns.Names())
|
||||
uniqueKeyColumnAscending := make([]string, len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnDescending := make([]string, len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnAscending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnDescending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
|
||||
for i, column := range uniqueKeyColumns.Columns() {
|
||||
uniqueKeyColumnNames[i] = EscapeName(uniqueKeyColumnNames[i])
|
||||
if column.Type == EnumColumnType {
|
||||
@ -368,7 +361,7 @@ func buildUniqueKeyMinMaxValuesPreparedQuery(databaseName, tableName string, uni
|
||||
tableName = EscapeName(tableName)
|
||||
|
||||
uniqueKeyColumnNames := duplicateNames(uniqueKeyColumns.Names())
|
||||
uniqueKeyColumnOrder := make([]string, len(uniqueKeyColumnNames))
|
||||
uniqueKeyColumnOrder := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
|
||||
for i, column := range uniqueKeyColumns.Columns() {
|
||||
uniqueKeyColumnNames[i] = EscapeName(uniqueKeyColumnNames[i])
|
||||
if column.Type == EnumColumnType {
|
||||
@ -400,7 +393,7 @@ func BuildDMLDeleteQuery(databaseName, tableName string, tableColumns, uniqueKey
|
||||
}
|
||||
for _, column := range uniqueKeyColumns.Columns() {
|
||||
tableOrdinal := tableColumns.Ordinals[column.Name]
|
||||
arg := column.convertArg(args[tableOrdinal], true)
|
||||
arg := column.convertArg(args[tableOrdinal])
|
||||
uniqueKeyArgs = append(uniqueKeyArgs, arg)
|
||||
}
|
||||
databaseName = EscapeName(databaseName)
|
||||
@ -437,7 +430,7 @@ func BuildDMLInsertQuery(databaseName, tableName string, tableColumns, sharedCol
|
||||
|
||||
for _, column := range sharedColumns.Columns() {
|
||||
tableOrdinal := tableColumns.Ordinals[column.Name]
|
||||
arg := column.convertArg(args[tableOrdinal], false)
|
||||
arg := column.convertArg(args[tableOrdinal])
|
||||
sharedArgs = append(sharedArgs, arg)
|
||||
}
|
||||
|
||||
@ -485,33 +478,27 @@ func BuildDMLUpdateQuery(databaseName, tableName string, tableColumns, sharedCol
|
||||
|
||||
for _, column := range sharedColumns.Columns() {
|
||||
tableOrdinal := tableColumns.Ordinals[column.Name]
|
||||
arg := column.convertArg(valueArgs[tableOrdinal], false)
|
||||
arg := column.convertArg(valueArgs[tableOrdinal])
|
||||
sharedArgs = append(sharedArgs, arg)
|
||||
}
|
||||
|
||||
for _, column := range uniqueKeyColumns.Columns() {
|
||||
tableOrdinal := tableColumns.Ordinals[column.Name]
|
||||
arg := column.convertArg(whereArgs[tableOrdinal], true)
|
||||
arg := column.convertArg(whereArgs[tableOrdinal])
|
||||
uniqueKeyArgs = append(uniqueKeyArgs, arg)
|
||||
}
|
||||
|
||||
setClause, err := BuildSetPreparedClause(mappedSharedColumns)
|
||||
if err != nil {
|
||||
return "", sharedArgs, uniqueKeyArgs, err
|
||||
}
|
||||
|
||||
equalsComparison, err := BuildEqualsPreparedComparison(uniqueKeyColumns.Names())
|
||||
if err != nil {
|
||||
return "", sharedArgs, uniqueKeyArgs, err
|
||||
}
|
||||
result = fmt.Sprintf(`
|
||||
update /* gh-ost %s.%s */
|
||||
%s.%s
|
||||
update /* gh-ost %s.%s */
|
||||
%s.%s
|
||||
set
|
||||
%s
|
||||
where
|
||||
%s
|
||||
`, databaseName, tableName,
|
||||
%s
|
||||
`, databaseName, tableName,
|
||||
databaseName, tableName,
|
||||
setClause,
|
||||
equalsComparison,
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -267,7 +267,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) {
|
||||
insert /* gh-ost mydb.tbl */ ignore into mydb.ghost (id, name, position)
|
||||
(select id, name, position from mydb.tbl force index (name_position_uidx)
|
||||
where (((name > ?) or (((name = ?)) AND (position > ?)) or ((name = ?) and (position = ?))) and ((name < ?) or (((name = ?)) AND (position < ?)) or ((name = ?) and (position = ?))))
|
||||
lock in share mode )
|
||||
)
|
||||
`
|
||||
test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected))
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117}))
|
||||
|
@ -8,7 +8,6 @@ package sql
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
type charsetEncoding map[string]encoding.Encoding
|
||||
@ -19,5 +18,4 @@ func init() {
|
||||
charsetEncodingMap = make(map[string]encoding.Encoding)
|
||||
// Begin mappings
|
||||
charsetEncodingMap["latin1"] = charmap.Windows1252
|
||||
charsetEncodingMap["gbk"] = simplifiedchinese.GBK
|
||||
}
|
||||
|
135
go/sql/parser.go
135
go/sql/parser.go
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -12,57 +12,24 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
sanitizeQuotesRegexp = regexp.MustCompile("('[^']*')")
|
||||
renameColumnRegexp = regexp.MustCompile(`(?i)\bchange\s+(column\s+|)([\S]+)\s+([\S]+)\s+`)
|
||||
dropColumnRegexp = regexp.MustCompile(`(?i)\bdrop\s+(column\s+|)([\S]+)$`)
|
||||
renameTableRegexp = regexp.MustCompile(`(?i)\brename\s+(to|as)\s+`)
|
||||
autoIncrementRegexp = regexp.MustCompile(`(?i)\bauto_increment[\s]*=[\s]*([0-9]+)`)
|
||||
alterTableExplicitSchemaTableRegexps = []*regexp.Regexp{
|
||||
// ALTER TABLE `scm`.`tbl` something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+` + "`" + `([^` + "`" + `]+)` + "`" + `[.]` + "`" + `([^` + "`" + `]+)` + "`" + `\s+(.*$)`),
|
||||
// ALTER TABLE `scm`.tbl something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+` + "`" + `([^` + "`" + `]+)` + "`" + `[.]([\S]+)\s+(.*$)`),
|
||||
// ALTER TABLE scm.`tbl` something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+([\S]+)[.]` + "`" + `([^` + "`" + `]+)` + "`" + `\s+(.*$)`),
|
||||
// ALTER TABLE scm.tbl something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+([\S]+)[.]([\S]+)\s+(.*$)`),
|
||||
}
|
||||
alterTableExplicitTableRegexps = []*regexp.Regexp{
|
||||
// ALTER TABLE `tbl` something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+` + "`" + `([^` + "`" + `]+)` + "`" + `\s+(.*$)`),
|
||||
// ALTER TABLE tbl something
|
||||
regexp.MustCompile(`(?i)\balter\s+table\s+([\S]+)\s+(.*$)`),
|
||||
}
|
||||
enumValuesRegexp = regexp.MustCompile("^enum[(](.*)[)]$")
|
||||
sanitizeQuotesRegexp = regexp.MustCompile("('[^']*')")
|
||||
renameColumnRegexp = regexp.MustCompile(`(?i)\bchange\s+(column\s+|)([\S]+)\s+([\S]+)\s+`)
|
||||
dropColumnRegexp = regexp.MustCompile(`(?i)\bdrop\s+(column\s+|)([\S]+)$`)
|
||||
)
|
||||
|
||||
type AlterTableParser struct {
|
||||
columnRenameMap map[string]string
|
||||
droppedColumns map[string]bool
|
||||
isRenameTable bool
|
||||
isAutoIncrementDefined bool
|
||||
|
||||
alterStatementOptions string
|
||||
alterTokens []string
|
||||
|
||||
explicitSchema string
|
||||
explicitTable string
|
||||
type Parser struct {
|
||||
columnRenameMap map[string]string
|
||||
droppedColumns map[string]bool
|
||||
}
|
||||
|
||||
func NewAlterTableParser() *AlterTableParser {
|
||||
return &AlterTableParser{
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
columnRenameMap: make(map[string]string),
|
||||
droppedColumns: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func NewParserFromAlterStatement(alterStatement string) *AlterTableParser {
|
||||
parser := NewAlterTableParser()
|
||||
parser.ParseAlterStatement(alterStatement)
|
||||
return parser
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) tokenizeAlterStatement(alterStatement string) (tokens []string) {
|
||||
func (this *Parser) tokenizeAlterStatement(alterStatement string) (tokens []string, err error) {
|
||||
terminatingQuote := rune(0)
|
||||
f := func(c rune) bool {
|
||||
switch {
|
||||
@ -86,16 +53,16 @@ func (this *AlterTableParser) tokenizeAlterStatement(alterStatement string) (tok
|
||||
for i := range tokens {
|
||||
tokens[i] = strings.TrimSpace(tokens[i])
|
||||
}
|
||||
return tokens
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) sanitizeQuotesFromAlterStatement(alterStatement string) (strippedStatement string) {
|
||||
func (this *Parser) sanitizeQuotesFromAlterStatement(alterStatement string) (strippedStatement string) {
|
||||
strippedStatement = alterStatement
|
||||
strippedStatement = sanitizeQuotesRegexp.ReplaceAllString(strippedStatement, "''")
|
||||
return strippedStatement
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) parseAlterToken(alterToken string) {
|
||||
func (this *Parser) parseAlterToken(alterToken string) (err error) {
|
||||
{
|
||||
// rename
|
||||
allStringSubmatch := renameColumnRegexp.FindAllStringSubmatch(alterToken, -1)
|
||||
@ -119,46 +86,19 @@ func (this *AlterTableParser) parseAlterToken(alterToken string) {
|
||||
this.droppedColumns[submatch[2]] = true
|
||||
}
|
||||
}
|
||||
{
|
||||
// rename table
|
||||
if renameTableRegexp.MatchString(alterToken) {
|
||||
this.isRenameTable = true
|
||||
}
|
||||
}
|
||||
{
|
||||
// auto_increment
|
||||
if autoIncrementRegexp.MatchString(alterToken) {
|
||||
this.isAutoIncrementDefined = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) ParseAlterStatement(alterStatement string) (err error) {
|
||||
this.alterStatementOptions = alterStatement
|
||||
for _, alterTableRegexp := range alterTableExplicitSchemaTableRegexps {
|
||||
if submatch := alterTableRegexp.FindStringSubmatch(this.alterStatementOptions); len(submatch) > 0 {
|
||||
this.explicitSchema = submatch[1]
|
||||
this.explicitTable = submatch[2]
|
||||
this.alterStatementOptions = submatch[3]
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, alterTableRegexp := range alterTableExplicitTableRegexps {
|
||||
if submatch := alterTableRegexp.FindStringSubmatch(this.alterStatementOptions); len(submatch) > 0 {
|
||||
this.explicitTable = submatch[1]
|
||||
this.alterStatementOptions = submatch[2]
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, alterToken := range this.tokenizeAlterStatement(this.alterStatementOptions) {
|
||||
func (this *Parser) ParseAlterStatement(alterStatement string) (err error) {
|
||||
alterTokens, _ := this.tokenizeAlterStatement(alterStatement)
|
||||
for _, alterToken := range alterTokens {
|
||||
alterToken = this.sanitizeQuotesFromAlterStatement(alterToken)
|
||||
this.parseAlterToken(alterToken)
|
||||
this.alterTokens = append(this.alterTokens, alterToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) GetNonTrivialRenames() map[string]string {
|
||||
func (this *Parser) GetNonTrivialRenames() map[string]string {
|
||||
result := make(map[string]string)
|
||||
for column, renamed := range this.columnRenameMap {
|
||||
if column != renamed {
|
||||
@ -168,45 +108,10 @@ func (this *AlterTableParser) GetNonTrivialRenames() map[string]string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) HasNonTrivialRenames() bool {
|
||||
func (this *Parser) HasNonTrivialRenames() bool {
|
||||
return len(this.GetNonTrivialRenames()) > 0
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) DroppedColumnsMap() map[string]bool {
|
||||
func (this *Parser) DroppedColumnsMap() map[string]bool {
|
||||
return this.droppedColumns
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) IsRenameTable() bool {
|
||||
return this.isRenameTable
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) IsAutoIncrementDefined() bool {
|
||||
return this.isAutoIncrementDefined
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) GetExplicitSchema() string {
|
||||
return this.explicitSchema
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) HasExplicitSchema() bool {
|
||||
return this.GetExplicitSchema() != ""
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) GetExplicitTable() string {
|
||||
return this.explicitTable
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) HasExplicitTable() bool {
|
||||
return this.GetExplicitTable() != ""
|
||||
}
|
||||
|
||||
func (this *AlterTableParser) GetAlterStatementOptions() string {
|
||||
return this.alterStatementOptions
|
||||
}
|
||||
|
||||
func ParseEnumValues(enumColumnType string) string {
|
||||
if submatch := enumValuesRegexp.FindStringSubmatch(enumColumnType); len(submatch) > 0 {
|
||||
return submatch[1]
|
||||
}
|
||||
return enumColumnType
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 GitHub Inc.
|
||||
Copyright 2016 GitHub Inc.
|
||||
See https://github.com/github/gh-ost/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@ -9,8 +9,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -19,53 +19,28 @@ func init() {
|
||||
|
||||
func TestParseAlterStatement(t *testing.T) {
|
||||
statement := "add column t int, engine=innodb"
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectFalse(parser.HasNonTrivialRenames())
|
||||
test.S(t).ExpectFalse(parser.IsAutoIncrementDefined())
|
||||
}
|
||||
|
||||
func TestParseAlterStatementTrivialRename(t *testing.T) {
|
||||
statement := "add column t int, change ts ts timestamp, engine=innodb"
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectFalse(parser.HasNonTrivialRenames())
|
||||
test.S(t).ExpectFalse(parser.IsAutoIncrementDefined())
|
||||
test.S(t).ExpectEquals(len(parser.columnRenameMap), 1)
|
||||
test.S(t).ExpectEquals(parser.columnRenameMap["ts"], "ts")
|
||||
}
|
||||
|
||||
func TestParseAlterStatementWithAutoIncrement(t *testing.T) {
|
||||
statements := []string{
|
||||
"auto_increment=7",
|
||||
"auto_increment = 7",
|
||||
"AUTO_INCREMENT = 71",
|
||||
"add column t int, change ts ts timestamp, auto_increment=7 engine=innodb",
|
||||
"add column t int, change ts ts timestamp, auto_increment =7 engine=innodb",
|
||||
"add column t int, change ts ts timestamp, AUTO_INCREMENT = 7 engine=innodb",
|
||||
"add column t int, change ts ts timestamp, engine=innodb auto_increment=73425",
|
||||
}
|
||||
for _, statement := range statements {
|
||||
parser := NewAlterTableParser()
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectTrue(parser.IsAutoIncrementDefined())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAlterStatementTrivialRenames(t *testing.T) {
|
||||
statement := "add column t int, change ts ts timestamp, CHANGE f `f` float, engine=innodb"
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectFalse(parser.HasNonTrivialRenames())
|
||||
test.S(t).ExpectFalse(parser.IsAutoIncrementDefined())
|
||||
test.S(t).ExpectEquals(len(parser.columnRenameMap), 2)
|
||||
test.S(t).ExpectEquals(parser.columnRenameMap["ts"], "ts")
|
||||
test.S(t).ExpectEquals(parser.columnRenameMap["f"], "f")
|
||||
@ -83,11 +58,9 @@ func TestParseAlterStatementNonTrivial(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, statement := range statements {
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectFalse(parser.IsAutoIncrementDefined())
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
renames := parser.GetNonTrivialRenames()
|
||||
test.S(t).ExpectEquals(len(renames), 2)
|
||||
test.S(t).ExpectEquals(renames["i"], "count")
|
||||
@ -96,46 +69,46 @@ func TestParseAlterStatementNonTrivial(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTokenizeAlterStatement(t *testing.T) {
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
{
|
||||
alterStatement := "add column t int"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int, change column i int"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int, change column i int 'some comment'"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int 'some comment'"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int, change column i int 'some comment, with comma'"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int 'some comment, with comma'"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int, add column d decimal(10,2)"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "add column d decimal(10,2)"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int, add column e enum('a','b','c')"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "add column e enum('a','b','c')"}))
|
||||
}
|
||||
{
|
||||
alterStatement := "add column t int(11), add column e enum('a','b','c')"
|
||||
tokens := parser.tokenizeAlterStatement(alterStatement)
|
||||
tokens, _ := parser.tokenizeAlterStatement(alterStatement)
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int(11)", "add column e enum('a','b','c')"}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeQuotesFromAlterStatement(t *testing.T) {
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
{
|
||||
alterStatement := "add column e enum('a','b','c')"
|
||||
strippedStatement := parser.sanitizeQuotesFromAlterStatement(alterStatement)
|
||||
@ -149,8 +122,9 @@ func TestSanitizeQuotesFromAlterStatement(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseAlterStatementDroppedColumns(t *testing.T) {
|
||||
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
statement := "drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
@ -158,17 +132,16 @@ func TestParseAlterStatementDroppedColumns(t *testing.T) {
|
||||
test.S(t).ExpectTrue(parser.droppedColumns["b"])
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
statement := "drop column b, drop key c_idx, drop column `d`"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectEquals(len(parser.droppedColumns), 2)
|
||||
test.S(t).ExpectTrue(parser.droppedColumns["b"])
|
||||
test.S(t).ExpectTrue(parser.droppedColumns["d"])
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
statement := "drop column b, drop key c_idx, drop column `d`, drop `e`, drop primary key, drop foreign key fk_1"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
@ -178,7 +151,7 @@ func TestParseAlterStatementDroppedColumns(t *testing.T) {
|
||||
test.S(t).ExpectTrue(parser.droppedColumns["e"])
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
parser := NewParser()
|
||||
statement := "drop column b, drop bad statement, add column i int"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
@ -186,153 +159,3 @@ func TestParseAlterStatementDroppedColumns(t *testing.T) {
|
||||
test.S(t).ExpectTrue(parser.droppedColumns["b"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAlterStatementRenameTable(t *testing.T) {
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectFalse(parser.isRenameTable)
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "rename as something_else"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectTrue(parser.isRenameTable)
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "drop column b, rename as something_else"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, statement)
|
||||
test.S(t).ExpectTrue(parser.isRenameTable)
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "engine=innodb rename as something_else"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectTrue(parser.isRenameTable)
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "rename as something_else, engine=innodb"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectTrue(parser.isRenameTable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAlterStatementExplicitTable(t *testing.T) {
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table tbl drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table `tbl` drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table `scm with spaces`.`tbl` drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm with spaces")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table `scm`.`tbl with spaces` drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl with spaces")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table `scm`.tbl drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table scm.`tbl` drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table scm.tbl drop column b"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b"}))
|
||||
}
|
||||
{
|
||||
parser := NewAlterTableParser()
|
||||
statement := "alter table scm.tbl drop column b, add index idx(i)"
|
||||
err := parser.ParseAlterStatement(statement)
|
||||
test.S(t).ExpectNil(err)
|
||||
test.S(t).ExpectEquals(parser.explicitSchema, "scm")
|
||||
test.S(t).ExpectEquals(parser.explicitTable, "tbl")
|
||||
test.S(t).ExpectEquals(parser.alterStatementOptions, "drop column b, add index idx(i)")
|
||||
test.S(t).ExpectTrue(reflect.DeepEqual(parser.alterTokens, []string{"drop column b", "add index idx(i)"}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnumValues(t *testing.T) {
|
||||
{
|
||||
s := "enum('red','green','blue','orange')"
|
||||
values := ParseEnumValues(s)
|
||||
test.S(t).ExpectEquals(values, "'red','green','blue','orange'")
|
||||
}
|
||||
{
|
||||
s := "('red','green','blue','orange')"
|
||||
values := ParseEnumValues(s)
|
||||
test.S(t).ExpectEquals(values, "('red','green','blue','orange')")
|
||||
}
|
||||
{
|
||||
s := "zzz"
|
||||
values := ParseEnumValues(s)
|
||||
test.S(t).ExpectEquals(values, "zzz")
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@ -16,14 +15,13 @@ import (
|
||||
type ColumnType int
|
||||
|
||||
const (
|
||||
UnknownColumnType ColumnType = iota
|
||||
TimestampColumnType
|
||||
DateTimeColumnType
|
||||
EnumColumnType
|
||||
MediumIntColumnType
|
||||
JSONColumnType
|
||||
FloatColumnType
|
||||
BinaryColumnType
|
||||
UnknownColumnType ColumnType = iota
|
||||
TimestampColumnType = iota
|
||||
DateTimeColumnType = iota
|
||||
EnumColumnType = iota
|
||||
MediumIntColumnType = iota
|
||||
JSONColumnType = iota
|
||||
FloatColumnType = iota
|
||||
)
|
||||
|
||||
const maxMediumintUnsigned int32 = 16777215
|
||||
@ -32,48 +30,20 @@ type TimezoneConversion struct {
|
||||
ToTimezone string
|
||||
}
|
||||
|
||||
type CharacterSetConversion struct {
|
||||
ToCharset string
|
||||
FromCharset string
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Name string
|
||||
IsUnsigned bool
|
||||
Charset string
|
||||
Type ColumnType
|
||||
EnumValues string
|
||||
timezoneConversion *TimezoneConversion
|
||||
enumToTextConversion bool
|
||||
// add Octet length for binary type, fix bytes with suffix "00" get clipped in mysql binlog.
|
||||
// https://github.com/github/gh-ost/issues/909
|
||||
BinaryOctetLength uint
|
||||
charsetConversion *CharacterSetConversion
|
||||
Name string
|
||||
IsUnsigned bool
|
||||
Charset string
|
||||
Type ColumnType
|
||||
timezoneConversion *TimezoneConversion
|
||||
}
|
||||
|
||||
func (this *Column) convertArg(arg interface{}, isUniqueKeyColumn bool) interface{} {
|
||||
func (this *Column) convertArg(arg interface{}) interface{} {
|
||||
if s, ok := arg.(string); ok {
|
||||
arg2Bytes := []byte(s)
|
||||
// convert to bytes if character string without charsetConversion.
|
||||
if this.Charset != "" && this.charsetConversion == nil {
|
||||
arg = arg2Bytes
|
||||
} else {
|
||||
if encoding, ok := charsetEncodingMap[this.Charset]; ok {
|
||||
arg, _ = encoding.NewDecoder().String(s)
|
||||
}
|
||||
// string, charset conversion
|
||||
if encoding, ok := charsetEncodingMap[this.Charset]; ok {
|
||||
arg, _ = encoding.NewDecoder().String(s)
|
||||
}
|
||||
|
||||
if this.Type == BinaryColumnType && isUniqueKeyColumn {
|
||||
size := len(arg2Bytes)
|
||||
if uint(size) < this.BinaryOctetLength {
|
||||
buf := bytes.NewBuffer(arg2Bytes)
|
||||
for i := uint(0); i < (this.BinaryOctetLength - uint(size)); i++ {
|
||||
buf.Write([]byte{0})
|
||||
}
|
||||
arg = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
@ -209,18 +179,6 @@ func (this *ColumnList) HasTimezoneConversion(columnName string) bool {
|
||||
return this.GetColumn(columnName).timezoneConversion != nil
|
||||
}
|
||||
|
||||
func (this *ColumnList) SetEnumToTextConversion(columnName string) {
|
||||
this.GetColumn(columnName).enumToTextConversion = true
|
||||
}
|
||||
|
||||
func (this *ColumnList) IsEnumToTextConversion(columnName string) bool {
|
||||
return this.GetColumn(columnName).enumToTextConversion
|
||||
}
|
||||
|
||||
func (this *ColumnList) SetEnumValues(columnName string, enumValues string) {
|
||||
this.GetColumn(columnName).EnumValues = enumValues
|
||||
}
|
||||
|
||||
func (this *ColumnList) String() string {
|
||||
return strings.Join(this.Names(), ",")
|
||||
}
|
||||
@ -248,10 +206,6 @@ func (this *ColumnList) Len() int {
|
||||
return len(this.columns)
|
||||
}
|
||||
|
||||
func (this *ColumnList) SetCharsetConversion(columnName string, fromCharset string, toCharset string) {
|
||||
this.GetColumn(columnName).charsetConversion = &CharacterSetConversion{FromCharset: fromCharset, ToCharset: toCharset}
|
||||
}
|
||||
|
||||
// UniqueKey is the combination of a key's name and columns
|
||||
type UniqueKey struct {
|
||||
Name string
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
|
||||
"reflect"
|
||||
|
||||
"github.com/openark/golib/log"
|
||||
test "github.com/openark/golib/tests"
|
||||
"github.com/outbrain/golib/log"
|
||||
test "github.com/outbrain/golib/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -1,13 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
color varchar(32),
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
|
||||
insert into gh_ost_test values (null, 11, 'red');
|
||||
insert into gh_ost_test values (null, 13, 'green');
|
||||
insert into gh_ost_test values (null, 17, 'blue');
|
@ -1 +0,0 @@
|
||||
--attempt-instant-ddl
|
@ -1,17 +0,0 @@
|
||||
drop event if exists gh_ost_test;
|
||||
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
insert into gh_ost_test values (NULL, 11);
|
||||
insert into gh_ost_test values (NULL, 13);
|
||||
insert into gh_ost_test values (NULL, 17);
|
||||
insert into gh_ost_test values (NULL, 23);
|
||||
insert into gh_ost_test values (NULL, 29);
|
||||
insert into gh_ost_test values (NULL, 31);
|
||||
insert into gh_ost_test values (NULL, 37);
|
||||
delete from gh_ost_test where id>=5;
|
@ -1 +0,0 @@
|
||||
AUTO_INCREMENT=7
|
@ -1 +0,0 @@
|
||||
--alter='AUTO_INCREMENT=7'
|
@ -1,17 +0,0 @@
|
||||
drop event if exists gh_ost_test;
|
||||
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
insert into gh_ost_test values (NULL, 11);
|
||||
insert into gh_ost_test values (NULL, 13);
|
||||
insert into gh_ost_test values (NULL, 17);
|
||||
insert into gh_ost_test values (NULL, 23);
|
||||
insert into gh_ost_test values (NULL, 29);
|
||||
insert into gh_ost_test values (NULL, 31);
|
||||
insert into gh_ost_test values (NULL, 37);
|
||||
delete from gh_ost_test where id>=5;
|
@ -1 +0,0 @@
|
||||
AUTO_INCREMENT=8
|
@ -1,13 +0,0 @@
|
||||
drop event if exists gh_ost_test;
|
||||
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
insert into gh_ost_test values (NULL, 11);
|
||||
insert into gh_ost_test values (NULL, 13);
|
||||
insert into gh_ost_test values (NULL, 17);
|
||||
insert into gh_ost_test values (NULL, 23);
|
@ -1 +0,0 @@
|
||||
AUTO_INCREMENT=5
|
@ -1,9 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
set session sql_mode='NO_AUTO_VALUE_ON_ZERO';
|
||||
insert into gh_ost_test values (0, 23);
|
@ -1,21 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id bigint auto_increment,
|
||||
val bigint not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 18446744073709551615);
|
||||
insert into gh_ost_test values (null, 18446744073709551614);
|
||||
insert into gh_ost_test values (null, 18446744073709551613);
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter="change val val bigint"
|
@ -1,20 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11);
|
||||
insert into gh_ost_test values (null, 13);
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter="add column is_good bit null default 0"
|
@ -1 +0,0 @@
|
||||
id, i
|
@ -1 +0,0 @@
|
||||
id, i
|
@ -1,24 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
is_good bit null default 0,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11, 0);
|
||||
insert into gh_ost_test values (null, 13, 1);
|
||||
insert into gh_ost_test values (null, 17, 1);
|
||||
|
||||
update gh_ost_test set is_good=0 where i=13 order by id desc limit 1;
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter="modify column is_good bit not null default 0" --approve-renamed-columns
|
@ -1,40 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
ts0 timestamp(6) default current_timestamp(6),
|
||||
updated tinyint unsigned default 0,
|
||||
primary key(id, ts0)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11, sysdate(6), 0);
|
||||
update gh_ost_test set updated = 1 where i = 11 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 13, sysdate(6), 0);
|
||||
update gh_ost_test set updated = 1 where i = 13 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 17, sysdate(6), 0);
|
||||
update gh_ost_test set updated = 1 where i = 17 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 19, sysdate(6), 0);
|
||||
update gh_ost_test set updated = 1 where i = 19 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 23, sysdate(6), 0);
|
||||
update gh_ost_test set updated = 1 where i = 23 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 29, sysdate(6), 0);
|
||||
insert into gh_ost_test values (null, 31, sysdate(6), 0);
|
||||
insert into gh_ost_test values (null, 37, sysdate(6), 0);
|
||||
insert into gh_ost_test values (null, 41, sysdate(6), 0);
|
||||
delete from gh_ost_test where i = 31 order by id desc limit 1;
|
||||
end ;;
|
@ -1,40 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
v varchar(128),
|
||||
updated tinyint unsigned default 0,
|
||||
primary key(id, v)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11, 'eleven', 0);
|
||||
update gh_ost_test set updated = 1 where i = 11 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 13, 'thirteen', 0);
|
||||
update gh_ost_test set updated = 1 where i = 13 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 17, 'seventeen', 0);
|
||||
update gh_ost_test set updated = 1 where i = 17 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 19, 'nineteen', 0);
|
||||
update gh_ost_test set updated = 1 where i = 19 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 23, 'twenty three', 0);
|
||||
update gh_ost_test set updated = 1 where i = 23 order by id desc limit 1;
|
||||
|
||||
insert into gh_ost_test values (null, 29, 'twenty nine', 0);
|
||||
insert into gh_ost_test values (null, 31, 'thirty one', 0);
|
||||
insert into gh_ost_test values (null, 37, 'thirty seven', 0);
|
||||
insert into gh_ost_test values (null, 41, 'forty one', 0);
|
||||
delete from gh_ost_test where i = 31 order by id desc limit 1;
|
||||
end ;;
|
@ -1,28 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
t varchar(128) charset utf8 collate utf8_general_ci,
|
||||
tl varchar(128) charset latin1 not null,
|
||||
ta varchar(128) charset ascii not null,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
insert into gh_ost_test values (null, 'Hello world, Καλημέρα κόσμε, コンニチハ', 'átesting0', 'initial');
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, md5(rand()), 'átesting-a', 'a');
|
||||
insert into gh_ost_test values (null, 'novo proprietário', 'átesting-b', 'b');
|
||||
insert into gh_ost_test values (null, '2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm', 'átesting-c', 'c');
|
||||
insert into gh_ost_test values (null, 'usuário', 'átesting-x', 'x');
|
||||
|
||||
delete from gh_ost_test where ta='x' order by id desc limit 1;
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter='convert to character set utf8mb4'
|
@ -1,27 +0,0 @@
|
||||
set session time_zone='+00:00';
|
||||
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
create_time timestamp NULL DEFAULT '0000-00-00 00:00:00',
|
||||
update_time timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
counter int(10) unsigned DEFAULT NULL,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
set session time_zone='+00:00';
|
||||
insert into gh_ost_test values (1, '0000-00-00 00:00:00', now(), 0);
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
set session time_zone='+00:00';
|
||||
update gh_ost_test set counter = counter + 1 where id = 1;
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter='add column name varchar(1)'
|
@ -1 +0,0 @@
|
||||
id, create_time, update_time, counter
|
@ -1 +0,0 @@
|
||||
id, create_time, update_time, counter
|
@ -17,7 +17,7 @@ create event gh_ost_test
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
disable on slave
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11, now(), now(), now(), 0);
|
||||
|
@ -1,20 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int unsigned auto_increment,
|
||||
i int not null,
|
||||
dt datetime,
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--allow-zero-in-date --alter="change column dt dt datetime not null default '1970-00-00 00:00:00'"
|
@ -1,23 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
dec0 decimal(65,30) unsigned NOT NULL DEFAULT '0.000000000000000000000000000000',
|
||||
dec1 decimal(65,30) unsigned NOT NULL DEFAULT '1.000000000000000000000000000000',
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 0.0, 0.0);
|
||||
insert into gh_ost_test values (null, 2.0, 4.0);
|
||||
insert into gh_ost_test values (null, 99999999999999999999999999999999999.000, 6.0);
|
||||
update gh_ost_test set dec1=4.5 where dec2=4.0 order by id desc limit 1;
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
Percona
|
@ -1,26 +0,0 @@
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int auto_increment,
|
||||
i int not null,
|
||||
e enum('red', 'green', 'blue', 'orange') null default null collate 'utf8_bin',
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
insert into gh_ost_test values (null, 7, 'red');
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 11, 'red');
|
||||
insert into gh_ost_test values (null, 13, 'green');
|
||||
insert into gh_ost_test values (null, 17, 'blue');
|
||||
set @last_insert_id := last_insert_id();
|
||||
update gh_ost_test set e='orange' where id = @last_insert_id;
|
||||
end ;;
|
@ -1 +0,0 @@
|
||||
--alter="change e e varchar(32) not null default ''"
|
@ -1,21 +0,0 @@
|
||||
set session sql_mode='';
|
||||
drop table if exists gh_ost_test;
|
||||
create table gh_ost_test (
|
||||
id int unsigned auto_increment,
|
||||
i int not null,
|
||||
dt datetime not null default '1970-00-00 00:00:00',
|
||||
primary key(id)
|
||||
) auto_increment=1;
|
||||
|
||||
drop event if exists gh_ost_test;
|
||||
delimiter ;;
|
||||
create event gh_ost_test
|
||||
on schedule every 1 second
|
||||
starts current_timestamp
|
||||
ends current_timestamp + interval 60 second
|
||||
on completion not preserve
|
||||
enable
|
||||
do
|
||||
begin
|
||||
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
|
||||
end ;;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user