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]

Reply via email to