adding applier, instance_key, instance_key_map
This commit is contained in:
parent
cf87d16044
commit
937491674c
95
go/logic/applier.go
Normal file
95
go/logic/applier.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 GitHub Inc.
|
||||||
|
See https://github.com/github/gh-osc/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
gosql "database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/github/gh-osc/go/base"
|
||||||
|
"github.com/github/gh-osc/go/mysql"
|
||||||
|
"github.com/github/gh-osc/go/sql"
|
||||||
|
|
||||||
|
"github.com/outbrain/golib/log"
|
||||||
|
"github.com/outbrain/golib/sqlutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Applier reads data from the read-MySQL-server (typically a replica, but can be the master)
|
||||||
|
// It is used for gaining initial status and structure, and later also follow up on progress and changelog
|
||||||
|
type Applier struct {
|
||||||
|
connectionConfig *mysql.ConnectionConfig
|
||||||
|
db *gosql.DB
|
||||||
|
migrationContext *base.MigrationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplier() *Applier {
|
||||||
|
return &Applier{
|
||||||
|
connectionConfig: base.GetMigrationContext().MasterConnectionConfig,
|
||||||
|
migrationContext: base.GetMigrationContext(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Applier) InitDBConnections() (err error) {
|
||||||
|
ApplierUri := this.connectionConfig.GetDBUri(this.migrationContext.DatabaseName)
|
||||||
|
if this.db, _, err = sqlutils.GetDB(ApplierUri); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := this.validateConnection(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateConnection issues a simple can-connect to MySQL
|
||||||
|
func (this *Applier) validateConnection() error {
|
||||||
|
query := `select @@global.port`
|
||||||
|
var port int
|
||||||
|
if err := this.db.QueryRow(query).Scan(&port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if port != this.connectionConfig.Key.Port {
|
||||||
|
return fmt.Errorf("Unexpected database port reported: %+v", port)
|
||||||
|
}
|
||||||
|
log.Infof("connection validated on %+v", this.connectionConfig.Key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateGhostTable creates the ghost table on the master
|
||||||
|
func (this *Applier) CreateGhostTable() error {
|
||||||
|
query := fmt.Sprintf(`create /* gh-osc */ table %s.%s like %s.%s`,
|
||||||
|
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||||
|
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||||
|
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||||
|
sql.EscapeName(this.migrationContext.OriginalTableName),
|
||||||
|
)
|
||||||
|
log.Infof("Creating ghost table %s.%s",
|
||||||
|
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||||
|
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||||
|
)
|
||||||
|
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("Table created")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateGhostTable creates the ghost table on the master
|
||||||
|
func (this *Applier) AlterGhost() error {
|
||||||
|
query := fmt.Sprintf(`alter /* gh-osc */ table %s.%s %s`,
|
||||||
|
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||||
|
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||||
|
this.migrationContext.AlterStatement,
|
||||||
|
)
|
||||||
|
log.Infof("Altering ghost table %s.%s",
|
||||||
|
sql.EscapeName(this.migrationContext.DatabaseName),
|
||||||
|
sql.EscapeName(this.migrationContext.GetGhostTableName()),
|
||||||
|
)
|
||||||
|
log.Debugf("ALTER statement: %s", query)
|
||||||
|
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("Table altered")
|
||||||
|
return nil
|
||||||
|
}
|
126
go/mysql/instance_key.go
Normal file
126
go/mysql/instance_key.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 Shlomi Noach, courtesy Booking.com
|
||||||
|
|
||||||
|
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 mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultInstancePort = 3306
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstanceKey is an instance indicator, identifued by hostname and port
|
||||||
|
type InstanceKey struct {
|
||||||
|
Hostname string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
const detachHint = "//"
|
||||||
|
|
||||||
|
// ParseInstanceKey will parse an InstanceKey from a string representation such as 127.0.0.1:3306
|
||||||
|
func NewRawInstanceKey(hostPort string) (*InstanceKey, error) {
|
||||||
|
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: 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ParseRawInstanceKeyLoose(hostPort string) (*InstanceKey, error) {
|
||||||
|
if !strings.Contains(hostPort, ":") {
|
||||||
|
return &InstanceKey{Hostname: hostPort, Port: DefaultInstancePort}, nil
|
||||||
|
}
|
||||||
|
return NewRawInstanceKey(hostPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals tests equality between this key and another key
|
||||||
|
func (this *InstanceKey) Equals(other *InstanceKey) bool {
|
||||||
|
if other == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.Hostname == other.Hostname && this.Port == other.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmallerThan returns true if this key is dictionary-smaller than another.
|
||||||
|
// This is used for consistent sorting/ordering; there's nothing magical about it.
|
||||||
|
func (this *InstanceKey) SmallerThan(other *InstanceKey) bool {
|
||||||
|
if this.Hostname < other.Hostname {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if this.Hostname == other.Hostname && this.Port < other.Port {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDetached returns 'true' when this hostname is logically "detached"
|
||||||
|
func (this *InstanceKey) IsDetached() bool {
|
||||||
|
return strings.HasPrefix(this.Hostname, detachHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid uses simple heuristics to see whether this key represents an actual instance
|
||||||
|
func (this *InstanceKey) IsValid() bool {
|
||||||
|
if this.Hostname == "_" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if this.IsDetached() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(this.Hostname) > 0 && this.Port > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetachedKey returns an instance key whose hostname is detahced: invalid, but recoverable
|
||||||
|
func (this *InstanceKey) DetachedKey() *InstanceKey {
|
||||||
|
if this.IsDetached() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
return &InstanceKey{Hostname: fmt.Sprintf("%s%s", detachHint, this.Hostname), Port: this.Port}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReattachedKey returns an instance key whose hostname is detahced: invalid, but recoverable
|
||||||
|
func (this *InstanceKey) ReattachedKey() *InstanceKey {
|
||||||
|
if !this.IsDetached() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
return &InstanceKey{Hostname: this.Hostname[len(detachHint):], Port: this.Port}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringCode returns an official string representation of this key
|
||||||
|
func (this *InstanceKey) StringCode() string {
|
||||||
|
return fmt.Sprintf("%s:%d", this.Hostname, this.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayString returns a user-friendly string representation of this key
|
||||||
|
func (this *InstanceKey) DisplayString() string {
|
||||||
|
return this.StringCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a user-friendly string representation of this key
|
||||||
|
func (this InstanceKey) String() string {
|
||||||
|
return this.StringCode()
|
||||||
|
}
|
106
go/mysql/instance_key_map.go
Normal file
106
go/mysql/instance_key_map.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 Shlomi Noach, courtesy Booking.com
|
||||||
|
|
||||||
|
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 mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstanceKeyMap is a convenience struct for listing InstanceKey-s
|
||||||
|
type InstanceKeyMap map[InstanceKey]bool
|
||||||
|
|
||||||
|
func NewInstanceKeyMap() *InstanceKeyMap {
|
||||||
|
return &InstanceKeyMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddKey adds a single key to this map
|
||||||
|
func (this *InstanceKeyMap) AddKey(key InstanceKey) {
|
||||||
|
(*this)[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddKeys adds all given keys to this map
|
||||||
|
func (this *InstanceKeyMap) AddKeys(keys []InstanceKey) {
|
||||||
|
for _, key := range keys {
|
||||||
|
this.AddKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasKey checks if given key is within the map
|
||||||
|
func (this *InstanceKeyMap) HasKey(key InstanceKey) bool {
|
||||||
|
_, ok := (*this)[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceKeys returns keys in this map in the form of an array
|
||||||
|
func (this *InstanceKeyMap) GetInstanceKeys() []InstanceKey {
|
||||||
|
res := []InstanceKey{}
|
||||||
|
for key := range *this {
|
||||||
|
res = append(res, key)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON will marshal this map as JSON
|
||||||
|
func (this *InstanceKeyMap) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(this.GetInstanceKeys())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON will marshal this map as JSON
|
||||||
|
func (this *InstanceKeyMap) ToJSON() (string, error) {
|
||||||
|
bytes, err := this.MarshalJSON()
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSONString will marshal this map as JSON
|
||||||
|
func (this *InstanceKeyMap) ToJSONString() string {
|
||||||
|
s, _ := this.ToJSON()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCommaDelimitedList will export this map in comma delimited format
|
||||||
|
func (this *InstanceKeyMap) ToCommaDelimitedList() string {
|
||||||
|
keyDisplays := []string{}
|
||||||
|
for key := range *this {
|
||||||
|
keyDisplays = append(keyDisplays, key.DisplayString())
|
||||||
|
}
|
||||||
|
return strings.Join(keyDisplays, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadJson unmarshalls a json into this map
|
||||||
|
func (this *InstanceKeyMap) ReadJson(jsonString string) error {
|
||||||
|
var keys []InstanceKey
|
||||||
|
err := json.Unmarshal([]byte(jsonString), &keys)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
this.AddKeys(keys)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadJson unmarshalls a json into this map
|
||||||
|
func (this *InstanceKeyMap) ReadCommaDelimitedList(list string) error {
|
||||||
|
tokens := strings.Split(list, ",")
|
||||||
|
for _, token := range tokens {
|
||||||
|
key, err := ParseRawInstanceKeyLoose(token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
this.AddKey(*key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user