github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/redo/reader/reader_test.go (about)

     1  //  Copyright 2021 PingCAP, 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  //  See the License for the specific language governing permissions and
    12  //  limitations under the License.
    13  
    14  package reader
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/uuid"
    26  	"github.com/pingcap/tiflow/cdc/model"
    27  	"github.com/pingcap/tiflow/cdc/model/codec"
    28  	"github.com/pingcap/tiflow/cdc/redo/common"
    29  	"github.com/pingcap/tiflow/cdc/redo/writer"
    30  	"github.com/pingcap/tiflow/cdc/redo/writer/file"
    31  	"github.com/pingcap/tiflow/pkg/redo"
    32  	"github.com/stretchr/testify/require"
    33  	"golang.org/x/sync/errgroup"
    34  )
    35  
    36  func genLogFile(
    37  	ctx context.Context, t *testing.T,
    38  	dir string, logType string,
    39  	minCommitTs, maxCommitTs uint64,
    40  ) {
    41  	cfg := &writer.LogWriterConfig{
    42  		MaxLogSizeInBytes: 100000,
    43  		Dir:               dir,
    44  	}
    45  	fileName := fmt.Sprintf(redo.RedoLogFileFormatV2, "capture", "default",
    46  		"changefeed", logType, maxCommitTs, uuid.NewString(), redo.LogEXT)
    47  	w, err := file.NewFileWriter(ctx, cfg, writer.WithLogFileName(func() string {
    48  		return fileName
    49  	}))
    50  	require.Nil(t, err)
    51  	if logType == redo.RedoRowLogFileType {
    52  		// generate unsorted logs
    53  		for ts := maxCommitTs; ts >= minCommitTs; ts-- {
    54  			event := &model.RowChangedEvent{
    55  				CommitTs: ts,
    56  				TableInfo: &model.TableInfo{
    57  					TableName: model.TableName{
    58  						Schema: "test",
    59  						Table:  "t",
    60  					},
    61  				},
    62  			}
    63  			log := event.ToRedoLog()
    64  			rawData, err := codec.MarshalRedoLog(log, nil)
    65  			require.Nil(t, err)
    66  			_, err = w.Write(rawData)
    67  			require.Nil(t, err)
    68  		}
    69  	} else if logType == redo.RedoDDLLogFileType {
    70  		event := &model.DDLEvent{
    71  			CommitTs:  maxCommitTs,
    72  			TableInfo: &model.TableInfo{},
    73  		}
    74  		log := event.ToRedoLog()
    75  		rawData, err := codec.MarshalRedoLog(log, nil)
    76  		require.Nil(t, err)
    77  		_, err = w.Write(rawData)
    78  		require.Nil(t, err)
    79  	}
    80  	err = w.Close()
    81  	require.Nil(t, err)
    82  }
    83  
    84  func TestReadLogs(t *testing.T) {
    85  	t.Parallel()
    86  
    87  	dir := t.TempDir()
    88  	ctx, cancel := context.WithCancel(context.Background())
    89  
    90  	meta := &common.LogMeta{
    91  		CheckpointTs: 11,
    92  		ResolvedTs:   100,
    93  	}
    94  	for _, logType := range []string{redo.RedoRowLogFileType, redo.RedoDDLLogFileType} {
    95  		genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs)
    96  		genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs)
    97  		genLogFile(ctx, t, dir, logType, 12, 12)
    98  		genLogFile(ctx, t, dir, logType, meta.ResolvedTs, meta.ResolvedTs)
    99  	}
   100  	expectedRows := []uint64{12, meta.ResolvedTs}
   101  	expectedDDLs := []uint64{meta.CheckpointTs, meta.CheckpointTs, 12, meta.ResolvedTs}
   102  
   103  	uri, err := url.Parse(fmt.Sprintf("file://%s", dir))
   104  	require.NoError(t, err)
   105  	r := &LogReader{
   106  		cfg: &LogReaderConfig{
   107  			Dir:                t.TempDir(),
   108  			URI:                *uri,
   109  			UseExternalStorage: true,
   110  		},
   111  		meta:  meta,
   112  		rowCh: make(chan *model.RowChangedEventInRedoLog, defaultReaderChanSize),
   113  		ddlCh: make(chan *model.DDLEvent, defaultReaderChanSize),
   114  	}
   115  	eg, egCtx := errgroup.WithContext(ctx)
   116  	eg.Go(func() error {
   117  		return r.Run(egCtx)
   118  	})
   119  
   120  	for _, ts := range expectedRows {
   121  		row, err := r.ReadNextRow(egCtx)
   122  		require.NoError(t, err)
   123  		require.Equal(t, ts, row.CommitTs)
   124  	}
   125  	for _, ts := range expectedDDLs {
   126  		ddl, err := r.ReadNextDDL(egCtx)
   127  		require.NoError(t, err)
   128  		require.Equal(t, ts, ddl.CommitTs)
   129  	}
   130  
   131  	cancel()
   132  	require.ErrorIs(t, eg.Wait(), nil)
   133  }
   134  
   135  func TestLogReaderClose(t *testing.T) {
   136  	t.Parallel()
   137  
   138  	dir := t.TempDir()
   139  	ctx, cancel := context.WithCancel(context.Background())
   140  
   141  	meta := &common.LogMeta{
   142  		CheckpointTs: 11,
   143  		ResolvedTs:   100,
   144  	}
   145  	for _, logType := range []string{redo.RedoRowLogFileType, redo.RedoDDLLogFileType} {
   146  		genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs)
   147  		genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs)
   148  		genLogFile(ctx, t, dir, logType, 12, 12)
   149  		genLogFile(ctx, t, dir, logType, meta.ResolvedTs, meta.CheckpointTs)
   150  	}
   151  
   152  	uri, err := url.Parse(fmt.Sprintf("file://%s", dir))
   153  	require.NoError(t, err)
   154  	r := &LogReader{
   155  		cfg: &LogReaderConfig{
   156  			Dir:                t.TempDir(),
   157  			URI:                *uri,
   158  			UseExternalStorage: true,
   159  		},
   160  		meta:  meta,
   161  		rowCh: make(chan *model.RowChangedEventInRedoLog, 1),
   162  		ddlCh: make(chan *model.DDLEvent, 1),
   163  	}
   164  	eg, egCtx := errgroup.WithContext(ctx)
   165  	eg.Go(func() error {
   166  		return r.Run(egCtx)
   167  	})
   168  
   169  	time.Sleep(2 * time.Second)
   170  	cancel()
   171  	require.ErrorIs(t, eg.Wait(), context.Canceled)
   172  }
   173  
   174  func TestNewLogReaderAndReadMeta(t *testing.T) {
   175  	t.Parallel()
   176  
   177  	dir := t.TempDir()
   178  	genMetaFile(t, dir, &common.LogMeta{
   179  		CheckpointTs: 11,
   180  		ResolvedTs:   22,
   181  	})
   182  	genMetaFile(t, dir, &common.LogMeta{
   183  		CheckpointTs: 12,
   184  		ResolvedTs:   21,
   185  	})
   186  
   187  	tests := []struct {
   188  		name                             string
   189  		dir                              string
   190  		wantCheckpointTs, wantResolvedTs uint64
   191  		wantErr                          string
   192  	}{
   193  		{
   194  			name:             "happy",
   195  			dir:              dir,
   196  			wantCheckpointTs: 12,
   197  			wantResolvedTs:   22,
   198  		},
   199  		{
   200  			name:    "no meta file",
   201  			dir:     t.TempDir(),
   202  			wantErr: ".*no redo meta file found in dir*.",
   203  		},
   204  		{
   205  			name:    "wrong dir",
   206  			dir:     "xxx",
   207  			wantErr: ".*fail to open storage for redo log*.",
   208  		},
   209  		{
   210  			name:             "context cancel",
   211  			dir:              dir,
   212  			wantCheckpointTs: 12,
   213  			wantResolvedTs:   22,
   214  			wantErr:          context.Canceled.Error(),
   215  		},
   216  	}
   217  	for _, tt := range tests {
   218  		ctx := context.Background()
   219  		if tt.name == "context cancel" {
   220  			ctx1, cancel := context.WithCancel(context.Background())
   221  			cancel()
   222  			ctx = ctx1
   223  		}
   224  		uriStr := fmt.Sprintf("file://%s", tt.dir)
   225  		uri, err := url.Parse(uriStr)
   226  		require.Nil(t, err)
   227  		l, err := newLogReader(ctx, &LogReaderConfig{
   228  			Dir:                t.TempDir(),
   229  			URI:                *uri,
   230  			UseExternalStorage: redo.IsExternalStorage(uri.Scheme),
   231  		})
   232  		if tt.wantErr != "" {
   233  			require.Regexp(t, tt.wantErr, err, tt.name)
   234  		} else {
   235  			require.Nil(t, err, tt.name)
   236  			cts, rts, err := l.ReadMeta(ctx)
   237  			require.Nil(t, err, tt.name)
   238  			require.Equal(t, tt.wantCheckpointTs, cts, tt.name)
   239  			require.Equal(t, tt.wantResolvedTs, rts, tt.name)
   240  		}
   241  	}
   242  }
   243  
   244  func genMetaFile(t *testing.T, dir string, meta *common.LogMeta) {
   245  	fileName := fmt.Sprintf(redo.RedoMetaFileFormat, "capture", "default",
   246  		"changefeed", redo.RedoMetaFileType, uuid.NewString(), redo.MetaEXT)
   247  	path := filepath.Join(dir, fileName)
   248  	f, err := os.Create(path)
   249  	require.Nil(t, err)
   250  	data, err := meta.MarshalMsg(nil)
   251  	require.Nil(t, err)
   252  	_, err = f.Write(data)
   253  	require.Nil(t, err)
   254  }
   255  
   256  func TestLogHeapLess(t *testing.T) {
   257  	tests := []struct {
   258  		name   string
   259  		h      logHeap
   260  		i      int
   261  		j      int
   262  		expect bool
   263  	}{
   264  		{
   265  			name: "Delete before Update",
   266  			h: logHeap{
   267  				{
   268  					data: &model.RedoLog{
   269  						Type: model.RedoLogTypeRow,
   270  						RedoRow: model.RedoRowChangedEvent{
   271  							Row: &model.RowChangedEventInRedoLog{
   272  								CommitTs: 100,
   273  								Table: &model.TableName{
   274  									Schema:      "test",
   275  									Table:       "table",
   276  									TableID:     1,
   277  									IsPartition: false,
   278  								},
   279  								PreColumns: []*model.Column{
   280  									{
   281  										Name:  "col-1",
   282  										Value: 1,
   283  									}, {
   284  										Name:  "col-2",
   285  										Value: 2,
   286  									},
   287  								},
   288  							},
   289  						},
   290  					},
   291  				},
   292  				{
   293  					data: &model.RedoLog{
   294  						Type: model.RedoLogTypeRow,
   295  						RedoRow: model.RedoRowChangedEvent{
   296  							Row: &model.RowChangedEventInRedoLog{
   297  								CommitTs: 100,
   298  								Table: &model.TableName{
   299  									Schema:      "test",
   300  									Table:       "table",
   301  									TableID:     1,
   302  									IsPartition: false,
   303  								},
   304  								PreColumns: []*model.Column{
   305  									{
   306  										Name:  "col-1",
   307  										Value: 1,
   308  									}, {
   309  										Name:  "col-2",
   310  										Value: 2,
   311  									},
   312  								},
   313  								Columns: []*model.Column{
   314  									{
   315  										Name:  "col-1",
   316  										Value: 1,
   317  									}, {
   318  										Name:  "col-2",
   319  										Value: 3,
   320  									},
   321  								},
   322  							},
   323  						},
   324  					},
   325  				},
   326  			},
   327  			i:      0,
   328  			j:      1,
   329  			expect: true,
   330  		},
   331  		{
   332  			name: "Update before Insert",
   333  			h: logHeap{
   334  				{
   335  					data: &model.RedoLog{
   336  						Type: model.RedoLogTypeRow,
   337  						RedoRow: model.RedoRowChangedEvent{
   338  							Row: &model.RowChangedEventInRedoLog{
   339  								CommitTs: 100,
   340  								Table: &model.TableName{
   341  									Schema:      "test",
   342  									Table:       "table",
   343  									TableID:     1,
   344  									IsPartition: false,
   345  								},
   346  								PreColumns: []*model.Column{
   347  									{
   348  										Name:  "col-1",
   349  										Value: 1,
   350  									}, {
   351  										Name:  "col-2",
   352  										Value: 2,
   353  									},
   354  								},
   355  								Columns: []*model.Column{
   356  									{
   357  										Name:  "col-1",
   358  										Value: 1,
   359  									}, {
   360  										Name:  "col-2",
   361  										Value: 3,
   362  									},
   363  								},
   364  							},
   365  						},
   366  					},
   367  				},
   368  				{
   369  					data: &model.RedoLog{
   370  						Type: model.RedoLogTypeRow,
   371  						RedoRow: model.RedoRowChangedEvent{
   372  							Row: &model.RowChangedEventInRedoLog{
   373  								CommitTs: 100,
   374  								Table: &model.TableName{
   375  									Schema:      "test",
   376  									Table:       "table",
   377  									TableID:     1,
   378  									IsPartition: false,
   379  								},
   380  								Columns: []*model.Column{
   381  									{
   382  										Name:  "col-1",
   383  										Value: 1,
   384  									}, {
   385  										Name:  "col-2",
   386  										Value: 1,
   387  									},
   388  								},
   389  							},
   390  						},
   391  					},
   392  				},
   393  			},
   394  			i:      0,
   395  			j:      1,
   396  			expect: true,
   397  		},
   398  		{
   399  			name: "Update before Delete",
   400  			h: logHeap{
   401  				{
   402  					data: &model.RedoLog{
   403  						Type: model.RedoLogTypeRow,
   404  						RedoRow: model.RedoRowChangedEvent{
   405  							Row: &model.RowChangedEventInRedoLog{
   406  								CommitTs: 100,
   407  								Table: &model.TableName{
   408  									Schema:      "test",
   409  									Table:       "table",
   410  									TableID:     1,
   411  									IsPartition: false,
   412  								},
   413  								PreColumns: []*model.Column{
   414  									{
   415  										Name:  "col-1",
   416  										Value: 1,
   417  									}, {
   418  										Name:  "col-2",
   419  										Value: 2,
   420  									},
   421  								},
   422  								Columns: []*model.Column{
   423  									{
   424  										Name:  "col-1",
   425  										Value: 1,
   426  									}, {
   427  										Name:  "col-2",
   428  										Value: 3,
   429  									},
   430  								},
   431  							},
   432  						},
   433  					},
   434  				},
   435  				{
   436  					data: &model.RedoLog{
   437  						Type: model.RedoLogTypeRow,
   438  						RedoRow: model.RedoRowChangedEvent{
   439  							Row: &model.RowChangedEventInRedoLog{
   440  								CommitTs: 100,
   441  								Table: &model.TableName{
   442  									Schema:      "test",
   443  									Table:       "table",
   444  									TableID:     1,
   445  									IsPartition: false,
   446  								},
   447  								PreColumns: []*model.Column{
   448  									{
   449  										Name:  "col-1",
   450  										Value: 1,
   451  									}, {
   452  										Name:  "col-2",
   453  										Value: 1,
   454  									},
   455  								},
   456  							},
   457  						},
   458  					},
   459  				},
   460  			},
   461  			i:      0,
   462  			j:      1,
   463  			expect: false,
   464  		},
   465  		{
   466  			name: "Update before Update",
   467  			h: logHeap{
   468  				{
   469  					data: &model.RedoLog{
   470  						Type: model.RedoLogTypeRow,
   471  						RedoRow: model.RedoRowChangedEvent{
   472  							Row: &model.RowChangedEventInRedoLog{
   473  								CommitTs: 100,
   474  								Table: &model.TableName{
   475  									Schema:      "test",
   476  									Table:       "table",
   477  									TableID:     1,
   478  									IsPartition: false,
   479  								},
   480  								PreColumns: []*model.Column{
   481  									{
   482  										Name:  "col-1",
   483  										Value: 1,
   484  									}, {
   485  										Name:  "col-2",
   486  										Value: 2,
   487  									},
   488  								},
   489  								Columns: []*model.Column{
   490  									{
   491  										Name:  "col-1",
   492  										Value: 1,
   493  									}, {
   494  										Name:  "col-2",
   495  										Value: 3,
   496  									},
   497  								},
   498  							},
   499  						},
   500  					},
   501  				},
   502  				{
   503  					data: &model.RedoLog{
   504  						Type: model.RedoLogTypeRow,
   505  						RedoRow: model.RedoRowChangedEvent{
   506  							Row: &model.RowChangedEventInRedoLog{
   507  								CommitTs: 100,
   508  								Table: &model.TableName{
   509  									Schema:      "test",
   510  									Table:       "table",
   511  									TableID:     1,
   512  									IsPartition: false,
   513  								},
   514  								PreColumns: []*model.Column{
   515  									{
   516  										Name:  "col-1",
   517  										Value: 1,
   518  									}, {
   519  										Name:  "col-2",
   520  										Value: 1,
   521  									},
   522  								},
   523  								Columns: []*model.Column{
   524  									{
   525  										Name:  "col-1",
   526  										Value: 1,
   527  									}, {
   528  										Name:  "col-2",
   529  										Value: 4,
   530  									},
   531  								},
   532  							},
   533  						},
   534  					},
   535  				},
   536  			},
   537  			i:      0,
   538  			j:      1,
   539  			expect: true,
   540  		},
   541  		{
   542  			name: "Delete before Insert",
   543  			h: logHeap{
   544  				{
   545  					data: &model.RedoLog{
   546  						Type: model.RedoLogTypeRow,
   547  						RedoRow: model.RedoRowChangedEvent{
   548  							Row: &model.RowChangedEventInRedoLog{
   549  								CommitTs: 100,
   550  								Table: &model.TableName{
   551  									Schema:      "test",
   552  									Table:       "table",
   553  									TableID:     1,
   554  									IsPartition: false,
   555  								},
   556  								PreColumns: []*model.Column{
   557  									{
   558  										Name:  "col-1",
   559  										Value: 1,
   560  									}, {
   561  										Name:  "col-2",
   562  										Value: 1,
   563  									},
   564  								},
   565  							},
   566  						},
   567  					},
   568  				},
   569  				{
   570  					data: &model.RedoLog{
   571  						Type: model.RedoLogTypeRow,
   572  						RedoRow: model.RedoRowChangedEvent{
   573  							Row: &model.RowChangedEventInRedoLog{
   574  								CommitTs: 100,
   575  								Table: &model.TableName{
   576  									Schema:      "test",
   577  									Table:       "table",
   578  									TableID:     1,
   579  									IsPartition: false,
   580  								},
   581  								Columns: []*model.Column{
   582  									{
   583  										Name:  "col-1",
   584  										Value: 1,
   585  									}, {
   586  										Name:  "col-2",
   587  										Value: 3,
   588  									},
   589  								},
   590  							},
   591  						},
   592  					},
   593  				},
   594  			},
   595  			i:      0,
   596  			j:      1,
   597  			expect: true,
   598  		},
   599  		{
   600  			name: "Same type of operations, different commit ts",
   601  			h: logHeap{
   602  				{
   603  					data: &model.RedoLog{
   604  						Type: model.RedoLogTypeRow,
   605  						RedoRow: model.RedoRowChangedEvent{
   606  							Row: &model.RowChangedEventInRedoLog{
   607  								CommitTs: 100,
   608  								Table: &model.TableName{
   609  									Schema:      "test",
   610  									Table:       "table",
   611  									TableID:     1,
   612  									IsPartition: false,
   613  								},
   614  							},
   615  						},
   616  					},
   617  				},
   618  				{
   619  					data: &model.RedoLog{
   620  						Type: model.RedoLogTypeRow,
   621  						RedoRow: model.RedoRowChangedEvent{
   622  							Row: &model.RowChangedEventInRedoLog{
   623  								CommitTs: 200,
   624  								Table: &model.TableName{
   625  									Schema:      "test",
   626  									Table:       "table",
   627  									TableID:     1,
   628  									IsPartition: false,
   629  								},
   630  							},
   631  						},
   632  					},
   633  				},
   634  			},
   635  			i:      0,
   636  			j:      1,
   637  			expect: true,
   638  		},
   639  		{
   640  			name: "Same type of operations, same commit ts, different startTs",
   641  			h: logHeap{
   642  				{
   643  					data: &model.RedoLog{
   644  						Type: model.RedoLogTypeRow,
   645  						RedoRow: model.RedoRowChangedEvent{
   646  							Row: &model.RowChangedEventInRedoLog{
   647  								CommitTs: 100,
   648  								StartTs:  80,
   649  								Table: &model.TableName{
   650  									Schema:      "test",
   651  									Table:       "table",
   652  									TableID:     1,
   653  									IsPartition: false,
   654  								},
   655  							},
   656  						},
   657  					},
   658  				},
   659  				{
   660  					data: &model.RedoLog{
   661  						Type: model.RedoLogTypeRow,
   662  						RedoRow: model.RedoRowChangedEvent{
   663  							Row: &model.RowChangedEventInRedoLog{
   664  								CommitTs: 100,
   665  								StartTs:  90,
   666  								Table: &model.TableName{
   667  									Schema:      "test",
   668  									Table:       "table",
   669  									TableID:     1,
   670  									IsPartition: false,
   671  								},
   672  							},
   673  						},
   674  					},
   675  				},
   676  			},
   677  			i:      0,
   678  			j:      1,
   679  			expect: true,
   680  		},
   681  		{
   682  			name: "Same type of operations, same commit ts",
   683  			h: logHeap{
   684  				{
   685  					data: &model.RedoLog{
   686  						Type: model.RedoLogTypeRow,
   687  						RedoRow: model.RedoRowChangedEvent{
   688  							Row: &model.RowChangedEventInRedoLog{
   689  								CommitTs: 100,
   690  								Table: &model.TableName{
   691  									Schema:      "test",
   692  									Table:       "table",
   693  									TableID:     1,
   694  									IsPartition: false,
   695  								},
   696  								PreColumns: []*model.Column{
   697  									{
   698  										Name:  "col-1",
   699  										Value: 1,
   700  									}, {
   701  										Name:  "col-2",
   702  										Value: 2,
   703  									},
   704  								},
   705  								Columns: []*model.Column{
   706  									{
   707  										Name:  "col-1",
   708  										Value: 1,
   709  									}, {
   710  										Name:  "col-2",
   711  										Value: 3,
   712  									},
   713  								},
   714  							},
   715  						},
   716  					},
   717  				},
   718  				{
   719  					data: &model.RedoLog{
   720  						Type: model.RedoLogTypeRow,
   721  						RedoRow: model.RedoRowChangedEvent{
   722  							Row: &model.RowChangedEventInRedoLog{
   723  								CommitTs: 100,
   724  								Table: &model.TableName{
   725  									Schema:      "test",
   726  									Table:       "table",
   727  									TableID:     1,
   728  									IsPartition: false,
   729  								},
   730  								PreColumns: []*model.Column{
   731  									{
   732  										Name:  "col-1",
   733  										Value: 1,
   734  									}, {
   735  										Name:  "col-2",
   736  										Value: 1,
   737  									},
   738  								},
   739  								Columns: []*model.Column{
   740  									{
   741  										Name:  "col-1",
   742  										Value: 1,
   743  									}, {
   744  										Name:  "col-2",
   745  										Value: 3,
   746  									},
   747  								},
   748  							},
   749  						},
   750  					},
   751  				},
   752  			},
   753  			i:      0,
   754  			j:      1,
   755  			expect: true,
   756  		},
   757  	}
   758  
   759  	for _, tt := range tests {
   760  		t.Run(tt.name, func(t *testing.T) {
   761  			if got := tt.h.Less(tt.i, tt.j); got != tt.expect {
   762  				t.Errorf("logHeap.Less() = %v, want %v", got, tt.expect)
   763  			}
   764  		})
   765  	}
   766  }