This is an automated email from the ASF dual-hosted git repository. maxyang pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/cloudberry-go-libs.git
commit 8663557611567e6e13240e9ae17eda9b6a22a2ab Author: Rakesh Sharma <[email protected]> AuthorDate: Tue Jan 23 21:31:19 2024 +0530 Introducing API GetSegmentConfigurationFromFile() for configuration retrieval from file This PR introduces an API GetSegmentConfigurationFromFile() to parse the gpsegconfig_dump file to retrieve segment configuration information. The recommended use of the API is to get the contents of gp_segment_configuration when the database is down. In the realm of gpdb, the gpsegconfig_dump file holds the crucial segment configuration information. this file is generated in the coordinator data directory through the fts process. gpsegconfig_dump is always in sync with gp_segment_configuration as on each fts probe the file gets updated if there is any change in the segment configuration. Various fts gucs govern the frequency of writing to this file. Note: Since the gpsegconfig_dump file is updated by fts process the information returned by this function can be a bit stale since the user can configure fts to run less frequently The gpsegconfig_dump file follows a structured format, as illustrated in the example below: 1 -1 p p n u 6000 localhost localhost /data/temp1 2 0 p p n u 6002 localhost localhost /data/temp2 3 1 p p n u 6003 localhost localhost /data/temp3 4 2 p p n u 6004 localhost localhost /data/temp4 Example Usage: segments, err := GetSegmentConfigurationFromFile("/path/to/coordinator/data/dir") if err != nil { //Handle error return } *. if gpsegconfig_dump has the following content ( with data-dir). 1 -1 p p n u 6000 localhost localhost /data/qddir 2 0 p p n u 6002 localhost localhost /data/seg1 SegConfig will have the DataDir field populated *. gpsegconfig_dump has the following content ( without data-dir) 1 -1 p p n u 6000 localhost localhost 2 0 p p n u 6002 localhost localhost SegConfig will have the DataDir field empty The structured data, captured in the gpsegconfig_dump file, is now accessible through the newly added API GetSegmentConfigurationFromFile(). This enhancement helps the other utilities to perform some operations even if the database is offline. e.g. In gpsupport utility if the database is down we can use this API to retrieve the data dirs to perform a log collection. or In gpdeletesystem if the database is down the API can be used to perform the database cleanup still. The reason to add it in gp-common-go-libs is that this is the growing API library that is being used by almost all the newly created gp modern utilities like project spine/ gpdr/ gpupgrade/ gpbackup/gprestore. Added test cases to do the unit testing around the following scenario when the file is valid it should provide valid results. when the file is invalid it should fail in parsing or reading it. added test cases to cover old and new field counts of gpsegconfig_dump. when the user passes the wrong coordinatorDataDir argument. --- cluster/cluster.go | 126 ++++++++++++++++++++++++++++++++ cluster/cluster_test.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+) diff --git a/cluster/cluster.go b/cluster/cluster.go index 8752d59..97fc50d 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -6,10 +6,14 @@ package cluster */ import ( + "bufio" "bytes" "fmt" + "os" "os/exec" + "path" "sort" + "strconv" "strings" "github.com/cloudberrydb/gp-common-go-libs/dbconn" @@ -575,3 +579,125 @@ func MustGetSegmentConfiguration(connection *dbconn.DBConn, getMirrors ...bool) gplog.FatalOnError(err) return segConfigs } + +/*GetSegmentConfigurationFromFile parse the gpsegconfig_dump file to retrieve segment configuration information. +Recommended use of the api is to get the contents of gp_segment_configuration when database is down. +If the database is up, use GetSegmentConfiguration()/MustGetSegmentConfiguration() instead. + +gpsegconfig_dump file gets created at $COORDINATOR_DATA_DIR/gpsegconfig_dump by fts process +The frequency of writing to this file is governed by various fts gucs. + +Note: Since the gpsegconfig_dump file is updated by fts process the information returned by +this function can be a bit stale since user can configure fts to run less frequently + +The gpsegconfig_dump file follows a structured format, as illustrated in the example below: +1 -1 p p n u 6000 localhost localhost /data/temp1 +2 0 p p n u 6002 localhost localhost /data/temp2 +3 1 p p n u 6003 localhost localhost /data/temp3 +4 2 p p n u 6004 localhost localhost /data/temp4 + +Example Usage: + segments, err := GetSegmentConfigurationFromFile("/path/to/coordinator/data/dir") + if err != nil { + //Handle error + return + } + +*. if gpsegconfig_dump have the following content ( with data-dir). + 1 -1 p p n u 6000 localhost localhost /data/qddir + 2 0 p p n u 6002 localhost localhost /data/seg1 + SegConfig will have DataDir field populated + +*. gpsegconfig_dump has following content ( without data-dir) + 1 -1 p p n u 6000 localhost localhost + 2 0 p p n u 6002 localhost localhost + SegConfig will have DataDir field empty + + +Parameters: + - coordinatorDataDir - The path to the coordinator data directory containing gpsegconfig_dump file. + can be retrieved from env var COORDINATOR_DATA_DIRECTORY + (e.g. /Users/shrakesh/workspace/gpdb/gpAux/gpdemo/datadirs/qddir/demoDataDir-1) + +Returns: + - []SegConfig: A slice of SegConfig structures representing the segment configuration. + - error: If any occurs during file reading and parsing. +*/ + +func GetSegmentConfigurationFromFile(coordinatorDataDir string) ([]SegConfig, error) { + + /*Check if the given argument coordinator_data_dir is empty*/ + if len(strings.TrimSpace(coordinatorDataDir)) == 0 { + return nil, fmt.Errorf("Coordinator data directory path is empty") + } + + /*Generate gpsegconfig_dump file path*/ + gpsegconfigDump := path.Join(coordinatorDataDir, "gpsegconfig_dump") + + /* Open gpsegconfig_dump */ + fd, err := os.Open(gpsegconfigDump) + if err != nil { + return nil, fmt.Errorf("Failed to open file %s. Error: %s", gpsegconfigDump, err.Error()) + } + defer fd.Close() + + results := make([]SegConfig, 0) + scanner := bufio.NewScanner(fd) + + /*scanning file line by line to extract the fields into SegConfig struct*/ + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + parts := len(fields) + + /* older version of gpsegconfig_dump has 9 parts as it doesn't have datadir + 1 -1 p p n u 7000 shrakeshSMD6M.vmware.com shrakeshSMD6M.vmware.com + newer version of gpsegconfig_dump has 10 parts as it does have datadir + 1 -1 p p n u 7000 shrakeshSMD6M.vmware.com shrakeshSMD6M.vmware.com /data/qddir/demoDataDir-1 */ + if parts != 9 && parts != 10 { + return nil, fmt.Errorf("Unexpected number of fields (%d) in line: %s", parts, scanner.Text()) + } + + dbID, err := strconv.Atoi(fields[0]) + if err != nil { + return nil, fmt.Errorf("Failed to convert dbID with value %s to an int. Error: %s", fields[0], err.Error()) + } + + content, err := strconv.Atoi(fields[1]) + if err != nil { + return nil, fmt.Errorf("Failed to convert content with value %s to an int. Error: %s", fields[1], err.Error()) + } + + port, err := strconv.Atoi(fields[6]) + if err != nil { + return nil, fmt.Errorf("Failed to convert port with value %s to an int. Error: %s", fields[6], err.Error()) + } + + // there are 10 fields in new version of gpsegconfig_dump file + datadir := "" + if parts == 10 { + datadir = fields[9] + } + + seg := SegConfig{ + DbID: dbID, + ContentID: content, + Role: fields[2], + PreferredRole: fields[3], + Mode: fields[4], + Status: fields[5], + Port: port, + Hostname: fields[7], + Address: fields[8], + DataDir: datadir, + } + + results = append(results, seg) + } + + /* validating error during gpsegconfig_dump file read */ + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("Failed to read gpsegconfig_dump file %s: %s", gpsegconfigDump, err.Error()) + } + + return results, nil +} diff --git a/cluster/cluster_test.go b/cluster/cluster_test.go index 9cd825b..38ea1a5 100644 --- a/cluster/cluster_test.go +++ b/cluster/cluster_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/user" + "path" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" @@ -37,6 +38,17 @@ func expectPathToExist(path string) { } } +func createSegConfigFile(content string) *os.File { + filename := path.Join(os.TempDir(), "gpsegconfig_dump") + confFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + Expect(err).To(BeNil()) + _, err = confFile.WriteString(content) + Expect(err).To(BeNil()) + + defer confFile.Close() + return confFile +} + var _ = BeforeSuite(func() { _, _, _, _, logfile = testhelper.SetupTestEnvironment() }) @@ -75,6 +87,180 @@ var _ = Describe("cluster/cluster tests", func() { Expect(cmd).To(Equal([]string{"ssh", "-o", "StrictHostKeyChecking=no", "testUser@some-host", "ls"})) }) }) + + Describe("GetSegmentConfigurationFromFile", func() { + It("should return expected result for a new (10 fields) gpsegconfig_dump file", func() { + //create temp file with the sample data from new version + expRes := cluster.SegConfig{ + DbID: 1, + ContentID: -1, + Role: "p", + PreferredRole: "p", + Mode: "n", + Status: "u", + Port: 7000, + Hostname: "localhost", + Address: "localhost", + DataDir: "/data/qddir/demoDataDir-1", + } + content := fmt.Sprintf("%d %d %s %s %s %s %d %s %s %s", expRes.DbID, expRes.ContentID, expRes.Role, expRes.PreferredRole, expRes.Mode, expRes.Status, expRes.Port, expRes.Hostname, expRes.Address, expRes.DataDir) + tempConfFile := createSegConfigFile(content) + + //call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal(expRes)) + + //Cleanup + os.Remove(tempConfFile.Name()) + }) + + It("should return expected result for an old (9 fields) gpsegconfig_dump file", func() { + //create temp file with the sample data from new version + expRes := cluster.SegConfig{ + DbID: 1, + ContentID: -1, + Role: "p", + PreferredRole: "p", + Mode: "n", + Status: "u", + Port: 7000, + Hostname: "localhost", + Address: "localhost", + } + content := fmt.Sprintf("%d %d %s %s %s %s %d %s %s", expRes.DbID, expRes.ContentID, expRes.Role, expRes.PreferredRole, expRes.Mode, expRes.Status, expRes.Port, expRes.Hostname, expRes.Address) + tempConfFile := createSegConfigFile(content) + + //call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal(expRes)) + + //Cleanup + os.Remove(tempConfFile.Name()) + }) + + It("should return expected result for multiline gpsegconfig_dump file", func() { + //create temp file with the sample data from new version + expRes := []cluster.SegConfig{ + { + DbID: 1, + ContentID: -1, + Role: "p", + PreferredRole: "p", + Mode: "n", + Status: "u", + Port: 7000, + Hostname: "localhost", + Address: "localhost", + DataDir: "/data/qddir/demoDataDir-1", + }, + { + DbID: 2, + ContentID: -1, + Role: "m", + PreferredRole: "m", + Mode: "n", + Status: "u", + Port: 7001, + Hostname: "localhost", + Address: "localhost", + DataDir: "/data/standby/demoDataDir-2", + }, + } + var content string + for _, segconf := range expRes { + text := fmt.Sprintf("%d %d %s %s %s %s %d %s %s %s\n", segconf.DbID, segconf.ContentID, segconf.Role, segconf.PreferredRole, segconf.Mode, segconf.Status, segconf.Port, segconf.Hostname, segconf.Address, segconf.DataDir) + content = content + text + } + + tempConfFile := createSegConfigFile(content) + + //call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(2)) + Expect(result).To(Equal(expRes)) + + //Cleanup + os.Remove(tempConfFile.Name()) + }) + + It("should fail when empty coordinator data directory is provided to function", func() { + // Call the function under test + result, err := cluster.GetSegmentConfigurationFromFile("") + + // Assertions + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Coordinator data directory path is empty")) + }) + + It("should fail when reading invalid file/path", func() { + // Call the function under test + result, err := cluster.GetSegmentConfigurationFromFile("/tmp/") + + // Assertions + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Failed to open file /tmp/gpsegconfig_dump. Error: open /tmp/gpsegconfig_dump: no such file or directory")) + }) + + It("should return an error for a file with less than 9 number of fields", func() { + // Create a temporary file with incorrect fields content + content := "invalid_content\n" + tempConfFile := createSegConfigFile(content) + + // Call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + + // Assertions + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Unexpected number of fields (1) in line: invalid_content")) + + // Cleanup + os.Remove(tempConfFile.Name()) + }) + + It("should return an error for a file with more than 10 number of fields", func() { + // Create a temporary file with incorrect fields content + content := "1 -1 p p n u 7000 localhost localhost /data/dir-1 dummy\n" + tempConfFile := createSegConfigFile(content) + + // Call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + + // Assertions + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Unexpected number of fields (11) in line: 1 -1 p p n u 7000 localhost localhost /data/dir-1 dummy")) + + // Cleanup + os.Remove(tempConfFile.Name()) + }) + + It("should fail when there is type conversion error", func() { + // Create a temporary file with one invalid int field + content := "1a -1 p p n u 7000 localhost localhost /data/dir1\n" + tempConfFile := createSegConfigFile(content) + + //Call the function under test + result, err := cluster.GetSegmentConfigurationFromFile(os.TempDir()) + + // Assertions + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("Failed to convert dbID with value 1a to an int. Error: strconv.Atoi: parsing \"1a\": invalid syntax")) + + //Cleanup + os.Remove(tempConfFile.Name()) + }) + + }) + Describe("GetSegmentConfiguration", func() { header := []string{"dbid", "contentid", "role", "preferredrole", "mode", "status", "port", "hostname", "address", "datadir"} localSegOneValue := cluster.SegConfig{1, 0, "p", "p", "s", "u", 6002, "localhost", "127.0.0.1", "/data/gpseg0"} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
