github.com/matrixorigin/matrixone@v1.2.0/pkg/frontend/connector.go (about)

     1  // Copyright 2021 - 2023 Matrix Origin
     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 frontend
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"time"
    23  
    24  	plan2 "github.com/matrixorigin/matrixone/pkg/sql/plan"
    25  
    26  	"github.com/matrixorigin/matrixone/pkg/pb/plan"
    27  	"github.com/matrixorigin/matrixone/pkg/pb/timestamp"
    28  	"github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect"
    29  
    30  	"github.com/matrixorigin/matrixone/pkg/common/moerr"
    31  	"github.com/matrixorigin/matrixone/pkg/defines"
    32  	pb "github.com/matrixorigin/matrixone/pkg/pb/task"
    33  	"github.com/matrixorigin/matrixone/pkg/sql/parsers/tree"
    34  	moconnector "github.com/matrixorigin/matrixone/pkg/stream/connector"
    35  	"github.com/matrixorigin/matrixone/pkg/taskservice"
    36  )
    37  
    38  const (
    39  	defaultConnectorTaskMaxRetryTimes = 10
    40  	defaultConnectorTaskRetryInterval = int64(time.Second * 10)
    41  )
    42  
    43  func handleCreateDynamicTable(ctx context.Context, ses *Session, st *tree.CreateTable) error {
    44  	ts := getGlobalPu().TaskService
    45  	if ts == nil {
    46  		return moerr.NewInternalError(ctx, "no task service is found")
    47  	}
    48  	dbName := string(st.Table.Schema())
    49  	if dbName == "" {
    50  		dbName = ses.GetDatabaseName()
    51  	}
    52  	tableName := string(st.Table.Name())
    53  	_, tableDef := ses.GetTxnCompileCtx().Resolve(dbName, tableName, plan2.Snapshot{TS: &timestamp.Timestamp{}})
    54  	if tableDef == nil {
    55  		return moerr.NewNoSuchTable(ctx, dbName, tableName)
    56  	}
    57  	options := make(map[string]string)
    58  	for _, option := range st.DTOptions {
    59  		switch opt := option.(type) {
    60  		case *tree.CreateSourceWithOption:
    61  			key := string(opt.Key)
    62  			val := opt.Val.(*tree.NumVal).OrigString()
    63  			options[key] = val
    64  		}
    65  	}
    66  
    67  	generatedPlan, err := buildPlan(ctx, ses, ses.GetTxnCompileCtx(), st.AsSource)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	query := generatedPlan.GetQuery()
    72  	if query != nil { // Checking if query is not nil
    73  		for _, node := range query.Nodes {
    74  			if node.NodeType == plan.Node_SOURCE_SCAN {
    75  				//collect the stream tableDefs
    76  				streamTableDef := node.TableDef.Defs
    77  				for _, def := range streamTableDef {
    78  					if propertiesDef, ok := def.Def.(*plan.TableDef_DefType_Properties); ok {
    79  						for _, property := range propertiesDef.Properties.Properties {
    80  							options[property.Key] = property.Value
    81  						}
    82  					}
    83  				}
    84  			}
    85  		}
    86  	}
    87  
    88  	options[moconnector.OptConnectorSql] = tree.String(st.AsSource, dialect.MYSQL)
    89  	if err := createConnector(
    90  		ctx,
    91  		ses.GetTenantInfo().GetTenantID(),
    92  		ses.GetTenantName(),
    93  		ses.GetUserName(),
    94  		ts,
    95  		dbName+"."+tableName,
    96  		options,
    97  		st.IfNotExists,
    98  	); err != nil {
    99  		return err
   100  	}
   101  	return nil
   102  }
   103  
   104  func handleCreateConnector(ctx context.Context, ses *Session, st *tree.CreateConnector) error {
   105  	ts := getGlobalPu().TaskService
   106  	if ts == nil {
   107  		return moerr.NewInternalError(ctx, "no task service is found")
   108  	}
   109  	dbName := string(st.TableName.Schema())
   110  	tableName := string(st.TableName.Name())
   111  	_, tableDef := ses.GetTxnCompileCtx().Resolve(dbName, tableName, plan2.Snapshot{TS: &timestamp.Timestamp{}})
   112  	if tableDef == nil {
   113  		return moerr.NewNoSuchTable(ctx, dbName, tableName)
   114  	}
   115  	options := make(map[string]string)
   116  	for _, opt := range st.Options {
   117  		options[string(opt.Key)] = opt.Val.String()
   118  	}
   119  	if err := createConnector(
   120  		ctx,
   121  		ses.GetTenantInfo().GetTenantID(),
   122  		ses.GetTenantName(),
   123  		ses.GetUserName(),
   124  		ts,
   125  		dbName+"."+tableName,
   126  		options,
   127  		false,
   128  	); err != nil {
   129  		return err
   130  	}
   131  	return nil
   132  }
   133  
   134  func connectorTaskMetadata() pb.TaskMetadata {
   135  	return pb.TaskMetadata{
   136  		ID:       "-",
   137  		Executor: pb.TaskCode_ConnectorKafkaSink,
   138  		Options: pb.TaskOptions{
   139  			MaxRetryTimes: defaultConnectorTaskMaxRetryTimes,
   140  			RetryInterval: defaultConnectorTaskRetryInterval,
   141  			DelayDuration: 0,
   142  			Concurrency:   0,
   143  		},
   144  	}
   145  }
   146  
   147  func isSameValue(a, b map[string]string, field string) bool {
   148  	v1, ok := a[field]
   149  	if !ok {
   150  		return false
   151  	}
   152  	v2, ok := b[field]
   153  	if !ok {
   154  		return false
   155  	}
   156  	return strings.EqualFold(v1, v2)
   157  }
   158  
   159  func duplicate(t pb.DaemonTask, options map[string]string) bool {
   160  	if t.TaskStatus == pb.TaskStatus_Canceled {
   161  		return false
   162  	}
   163  	dup := true
   164  	switch d := t.Details.Details.(type) {
   165  	case *pb.Details_Connector:
   166  		checkFields := []string{
   167  			moconnector.OptConnectorType,
   168  			moconnector.OptConnectorTopic,
   169  			moconnector.OptConnectorServers,
   170  		}
   171  		for _, field := range checkFields {
   172  			dup = dup && isSameValue(d.Connector.Options, options, field)
   173  		}
   174  	}
   175  	return dup
   176  }
   177  
   178  func createConnector(
   179  	ctx context.Context,
   180  	accountID uint32,
   181  	account string,
   182  	username string,
   183  	ts taskservice.TaskService,
   184  	tableName string,
   185  	rawOpts map[string]string,
   186  	ifNotExists bool,
   187  ) error {
   188  	options, err := moconnector.MakeStmtOpts(ctx, rawOpts)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	tasks, err := ts.QueryDaemonTask(ctx,
   193  		taskservice.WithTaskType(taskservice.EQ,
   194  			pb.TaskType_TypeKafkaSinkConnector.String()),
   195  		taskservice.WithAccountID(taskservice.EQ,
   196  			accountID),
   197  	)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	for _, t := range tasks {
   202  		dc, ok := t.Details.Details.(*pb.Details_Connector)
   203  		if !ok {
   204  			return moerr.NewInternalError(ctx, fmt.Sprintf("invalid task type %s",
   205  				t.TaskType.String()))
   206  		}
   207  		if dc.Connector.TableName == tableName && duplicate(t, options) {
   208  			// do not return error if ifNotExists is true since the table is not actually created
   209  			if ifNotExists {
   210  				return nil
   211  			}
   212  			return moerr.NewErrDuplicateConnector(ctx, tableName)
   213  		}
   214  	}
   215  	details := &pb.Details{
   216  		AccountID: accountID,
   217  		Account:   account,
   218  		Username:  username,
   219  		Details: &pb.Details_Connector{
   220  			Connector: &pb.ConnectorDetails{
   221  				TableName: tableName,
   222  				Options:   options,
   223  			},
   224  		},
   225  	}
   226  	if err := ts.CreateDaemonTask(ctx, connectorTaskMetadata(), details); err != nil {
   227  		return err
   228  	}
   229  	return nil
   230  }
   231  
   232  func handleDropConnector(ctx context.Context, ses *Session, st *tree.DropConnector) error {
   233  	//todo: handle Create connector
   234  	return nil
   235  }
   236  
   237  func handleDropDynamicTable(ctx context.Context, ses *Session, st *tree.DropTable) error {
   238  	if getGlobalPu() == nil || getGlobalPu().TaskService == nil {
   239  		return moerr.NewInternalError(ctx, "task service not ready yet")
   240  	}
   241  	ts := getGlobalPu().TaskService
   242  
   243  	// Query all relevant tasks belonging to the current tenant
   244  	tasks, err := ts.QueryDaemonTask(ctx,
   245  		taskservice.WithTaskType(taskservice.EQ, pb.TaskType_TypeKafkaSinkConnector.String()),
   246  		taskservice.WithAccountID(taskservice.EQ, ses.GetAccountId()),
   247  		taskservice.WithTaskStatusCond(pb.TaskStatus_Running, pb.TaskStatus_Created, pb.TaskStatus_Paused, pb.TaskStatus_PauseRequested),
   248  	)
   249  	if err != nil || len(tasks) == 0 {
   250  		return err
   251  	}
   252  
   253  	// Filter the tasks within the loop
   254  	for _, tn := range st.Names {
   255  		dbName := string(tn.Schema())
   256  		if dbName == "" {
   257  			dbName = ses.GetDatabaseName()
   258  		}
   259  		fullTableName := dbName + "." + string(tn.Name())
   260  
   261  		for _, task := range tasks {
   262  			if task.Details.Details.(*pb.Details_Connector).Connector.TableName == fullTableName {
   263  				if err := handleCancelDaemonTask(ctx, ses, task.ID); err != nil {
   264  					return err
   265  				}
   266  			}
   267  		}
   268  	}
   269  	return nil
   270  }
   271  
   272  func handleShowConnectors(ctx context.Context, ses *Session) error {
   273  	var err error
   274  	if err := showConnectors(ctx, ses); err != nil {
   275  		return err
   276  	}
   277  	return err
   278  }
   279  
   280  var connectorCols = []Column{
   281  	&MysqlColumn{
   282  		ColumnImpl: ColumnImpl{
   283  			name:       "task_id",
   284  			columnType: defines.MYSQL_TYPE_LONG,
   285  		},
   286  	},
   287  	&MysqlColumn{
   288  		ColumnImpl: ColumnImpl{
   289  			name:       "task_type",
   290  			columnType: defines.MYSQL_TYPE_VARCHAR,
   291  		},
   292  	},
   293  	&MysqlColumn{
   294  		ColumnImpl: ColumnImpl{
   295  			name:       "task_runner",
   296  			columnType: defines.MYSQL_TYPE_VARCHAR,
   297  		},
   298  	},
   299  	&MysqlColumn{
   300  		ColumnImpl: ColumnImpl{
   301  			name:       "task_status",
   302  			columnType: defines.MYSQL_TYPE_VARCHAR,
   303  		},
   304  	},
   305  	&MysqlColumn{
   306  		ColumnImpl: ColumnImpl{
   307  			name:       "table_name",
   308  			columnType: defines.MYSQL_TYPE_VARCHAR,
   309  		},
   310  	},
   311  	&MysqlColumn{
   312  		ColumnImpl: ColumnImpl{
   313  			name:       "options",
   314  			columnType: defines.MYSQL_TYPE_VARCHAR,
   315  		},
   316  	},
   317  	&MysqlColumn{
   318  		ColumnImpl: ColumnImpl{
   319  			name:       "last_heartbeat",
   320  			columnType: defines.MYSQL_TYPE_VARCHAR,
   321  		},
   322  	},
   323  	&MysqlColumn{
   324  		ColumnImpl: ColumnImpl{
   325  			name:       "created_at",
   326  			columnType: defines.MYSQL_TYPE_VARCHAR,
   327  		},
   328  	},
   329  	&MysqlColumn{
   330  		ColumnImpl: ColumnImpl{
   331  			name:       "updated_at",
   332  			columnType: defines.MYSQL_TYPE_VARCHAR,
   333  		},
   334  	},
   335  	&MysqlColumn{
   336  		ColumnImpl: ColumnImpl{
   337  			name:       "end_at",
   338  			columnType: defines.MYSQL_TYPE_VARCHAR,
   339  		},
   340  	},
   341  	&MysqlColumn{
   342  		ColumnImpl: ColumnImpl{
   343  			name:       "last_run",
   344  			columnType: defines.MYSQL_TYPE_VARCHAR,
   345  		},
   346  	},
   347  	&MysqlColumn{
   348  		ColumnImpl: ColumnImpl{
   349  			name:       "error",
   350  			columnType: defines.MYSQL_TYPE_VARCHAR,
   351  		},
   352  	},
   353  }
   354  
   355  func showConnectors(ctx context.Context, ses FeSession) error {
   356  	ts := getGlobalPu().TaskService
   357  	if ts == nil {
   358  		return moerr.NewInternalError(ctx,
   359  			"task service not ready yet, please try again later.")
   360  	}
   361  	tasks, err := ts.QueryDaemonTask(ctx,
   362  		taskservice.WithTaskType(taskservice.EQ,
   363  			pb.TaskType_TypeKafkaSinkConnector.String()),
   364  		taskservice.WithAccountID(taskservice.EQ,
   365  			ses.GetAccountId()),
   366  	)
   367  	if err != nil {
   368  		return err
   369  	}
   370  	mrs := ses.GetMysqlResultSet()
   371  	for _, col := range connectorCols {
   372  		mrs.AddColumn(col)
   373  	}
   374  	for _, t := range tasks {
   375  		row := make([]interface{}, 12)
   376  		row[0] = t.ID
   377  		row[1] = t.TaskType.String()
   378  		row[2] = t.TaskRunner
   379  		row[3] = t.TaskStatus.String()
   380  		details := t.Details.Details.(*pb.Details_Connector)
   381  		row[4] = details.Connector.TableName
   382  		row[5] = optionString(details.Connector.Options)
   383  		if t.LastHeartbeat.IsZero() {
   384  			row[6] = ""
   385  		} else {
   386  			row[6] = t.LastHeartbeat.String()
   387  		}
   388  		row[7] = t.CreateAt.String()
   389  		row[8] = t.UpdateAt.String()
   390  		if t.EndAt.IsZero() {
   391  			row[9] = ""
   392  		} else {
   393  			row[9] = t.EndAt.String()
   394  		}
   395  		if t.LastRun.IsZero() {
   396  			row[10] = ""
   397  		} else {
   398  			row[10] = t.LastRun.String()
   399  		}
   400  		row[11] = t.Details.Error
   401  		mrs.AddRow(row)
   402  	}
   403  	return nil
   404  }
   405  
   406  func optionString(options map[string]string) string {
   407  	items := make([]string, 0, len(options))
   408  	for key, value := range options {
   409  		items = append(items, fmt.Sprintf("%s=%s", key, value))
   410  	}
   411  	sort.Strings(items)
   412  	return strings.Join(items, ",")
   413  }