github.com/erda-project/erda-infra@v1.0.9/providers/cassandra/cassandra.go (about) 1 // Copyright (c) 2021 Terminus, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cassandra 16 17 import ( 18 "fmt" 19 "reflect" 20 "strings" 21 "time" 22 23 "github.com/erda-project/erda-infra/base/logs" 24 "github.com/erda-project/erda-infra/base/servicehub" 25 writer "github.com/erda-project/erda-infra/pkg/parallel-writer" 26 "github.com/gocql/gocql" 27 ) 28 29 // WriterConfig . 30 type WriterConfig struct { 31 Parallelism uint64 `file:"parallelism" default:"4" desc:"parallelism"` 32 Batch struct { 33 SizeBytes int `file:"size_bytes" desc:"cassandra batch failed size bytes"` 34 Size uint64 `file:"size" default:"100" desc:"batch size"` 35 Timeout time.Duration `file:"timeout" default:"30s" desc:"timeout to flush buffer for batch write"` 36 } `file:"batch"` 37 Retry int `file:"retry" desc:"retry if fail to write"` 38 } 39 40 // SessionConfig . 41 type SessionConfig struct { 42 Keyspace KeyspaceConfig `file:"keyspace"` 43 Consistency string `file:"consistency" default:"LOCAL_ONE"` 44 Reconnection ReconnectionConfig `file:"reconnection"` 45 } 46 47 // KeyspaceConfig . 48 type KeyspaceConfig struct { 49 Name string `file:"name" env:"CASSANDRA_KEYSPACE"` 50 Auto bool `file:"auto"` 51 Replication KeyspaceReplicationConfig `file:"replication"` 52 } 53 54 // KeyspaceReplicationConfig . 55 type KeyspaceReplicationConfig struct { 56 Class string `file:"class" default:"SimpleStrategy"` 57 Factor int32 `file:"factor" default:"2"` 58 } 59 60 // Interface . 61 type Interface interface { 62 CreateKeyspaces(ksc ...*KeyspaceConfig) error 63 NewSession(cfg *SessionConfig) (*Session, error) 64 NewBatchWriter(session *Session, c *WriterConfig, builderCreator func() StatementBuilder) writer.Writer 65 } 66 67 type config struct { 68 Hosts string `file:"host" env:"CASSANDRA_ADDR" default:"localhost:9042" desc:"server hosts"` 69 Security bool `file:"security" env:"CASSANDRA_SECURITY_ENABLE" default:"false" desc:"security"` 70 Username string `file:"username" env:"CASSANDRA_SECURITY_USERNAME" default:"" desc:"username"` 71 Password string `file:"password" env:"CASSANDRA_SECURITY_PASSWORD" default:"" desc:"password"` 72 Timeout time.Duration `file:"timeout" env:"CASSANDRA_TIMEOUT" default:"3s" desc:"session timeout"` 73 } 74 75 // provider . 76 type provider struct { 77 Cfg *config 78 Log logs.Logger 79 hosts []string 80 } 81 82 // Init . 83 func (p *provider) Init(ctx servicehub.Context) error { 84 p.hosts = strings.Split(p.Cfg.Hosts, ",") 85 return nil 86 } 87 88 func (p *provider) newSession(keyspace, consistency string) (*gocql.Session, error) { 89 cluster := gocql.NewCluster(p.hosts...) 90 if p.Cfg.Security && p.Cfg.Username != "" && p.Cfg.Password != "" { 91 cluster.Authenticator = &gocql.PasswordAuthenticator{Username: p.Cfg.Username, Password: p.Cfg.Password} 92 } 93 cluster.Consistency = gocql.ParseConsistency(consistency) 94 cluster.Keyspace = keyspace 95 cluster.Timeout = p.Cfg.Timeout 96 cluster.ConnectTimeout = p.Cfg.Timeout 97 return cluster.CreateSession() 98 } 99 100 // Provide . 101 func (p *provider) Provide(ctx servicehub.DependencyContext, args ...interface{}) interface{} { 102 return &service{ 103 p: p, 104 log: p.Log.Sub(ctx.Caller()), 105 } 106 } 107 108 type service struct { 109 p *provider 110 log logs.Logger 111 name string 112 } 113 114 func (s *service) CreateKeyspaces(ksc ...*KeyspaceConfig) error { 115 var sys *gocql.Session 116 defer func() { 117 if sys != nil { 118 sys.Close() 119 } 120 }() 121 for _, kc := range ksc { 122 if sys == nil { 123 s, err := s.p.newSession("system", gocql.All.String()) 124 if err != nil { 125 return err 126 } 127 sys = s 128 } 129 err := s.createKeySpace(sys, kc) 130 if err != nil { 131 return err 132 } 133 } 134 return nil 135 } 136 137 func (s *service) NewSession(cfg *SessionConfig) (*Session, error) { 138 if cfg.Keyspace.Auto { 139 err := s.CreateKeyspaces(&cfg.Keyspace) 140 if err != nil { 141 return nil, err 142 } 143 } 144 session, err := s.p.newSession(cfg.Keyspace.Name, cfg.Consistency) 145 if err != nil { 146 return nil, fmt.Errorf("new session failed: %w", err) 147 } 148 ms := &Session{ 149 session: session, 150 log: s.log.Sub("MySession"), 151 done: make(chan struct{}), 152 } 153 154 // workaround for issue: https://github.com/gocql/gocql/issues/831 155 // remove it when issue fixed 156 if cfg.Reconnection.Enable { 157 go ms.checkAndReconnect(s.p, cfg) 158 } 159 160 return ms, nil 161 } 162 163 func (s *service) createKeySpace(session *gocql.Session, kc *KeyspaceConfig) error { 164 if _, err := session.KeyspaceMetadata(kc.Name); err == nil { 165 s.log.Infof("keySpace: %s already existed", kc.Name) 166 return nil 167 } 168 169 stmt := fmt.Sprintf("CREATE KEYSPACE IF NOT EXISTS %s WITH replication={'class':'%s', 'replication_factor':%d}", kc.Name, kc.Replication.Class, kc.Replication.Factor) 170 q := session.Query(stmt).Consistency(gocql.All).RetryPolicy(nil) 171 defer q.Release() 172 s.log.Infof("create keySpace: %s", stmt) 173 return q.Exec() 174 } 175 176 func (s *service) NewBatchWriter(session *Session, c *WriterConfig, builderCreator func() StatementBuilder) writer.Writer { 177 return writer.ParallelBatch(func(uint64) writer.Writer { 178 return &batchWriter{ 179 session: session, 180 builder: builderCreator(), 181 retry: c.Retry, 182 retryDuration: 3 * time.Second, 183 log: s.log, 184 batchSizeBytes: c.Batch.SizeBytes, 185 } 186 }, c.Parallelism, c.Batch.Size, c.Batch.Timeout, s.batchWriteError) 187 } 188 189 func (s *service) batchWriteError(err error) error { 190 s.log.Errorf("fail to write cassandra: %s", err) 191 return nil // skip error 192 } 193 194 func init() { 195 servicehub.Register("cassandra", &servicehub.Spec{ 196 Services: []string{"cassandra"}, 197 Description: "cassandra", 198 Types: []reflect.Type{ 199 reflect.TypeOf((*Interface)(nil)).Elem(), 200 }, 201 ConfigFunc: func() interface{} { return &config{} }, 202 Creator: func() servicehub.Provider { 203 return &provider{} 204 }, 205 }) 206 }