From e747b6d79190e59a03b8aee780611367af4b6cea Mon Sep 17 00:00:00 2001 From: Tim Middleton Date: Tue, 26 Mar 2024 15:19:08 +0800 Subject: [PATCH] Add -D option to get departed members. Fix get persistence error (#169) --- docs/reference/15_members.adoc | 19 +++++++++- pkg/cmd/cluster.go | 2 +- pkg/cmd/formatting.go | 34 ++++++++++++++---- pkg/cmd/member.go | 29 +++++++++++++-- pkg/cmd/persistence.go | 6 +++- pkg/cmd/service.go | 10 +++--- pkg/cmd/snapshot.go | 5 ++- pkg/cmd/utils.go | 66 +++++++++++++++++++++++++++++++++- pkg/cmd/utils_test.go | 65 +++++++++++++++++++++++++++++++++ pkg/config/config_helper.go | 23 ++++++++---- pkg/fetcher/http_fetcher.go | 2 +- scripts/run-compat-ce.sh | 10 +++--- test/common/common.go | 6 +++- 13 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 pkg/cmd/utils_test.go diff --git a/docs/reference/15_members.adoc b/docs/reference/15_members.adoc index c042672..022f472 100644 --- a/docs/reference/15_members.adoc +++ b/docs/reference/15_members.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2021, 2023 Oracle and/or its affiliates. + Copyright (c) 2021, 2024 Oracle and/or its affiliates. Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. @@ -68,6 +68,23 @@ NODE ID ADDRESS PORT PROCESS MEMBER ROLE STORAGE MAX NOTE: You can also use `-o wide` to display more columns. +Display all departed members. + +[source,bash] +---- +cohctl get members -c local -D +---- +Output: +[source,bash] +---- +NODE ID TIMESTAMP ADDRESS MACHINE ID LOCATION ROLE + 3 2024-03-26 09:28:12.65 127.0.0.1:49170 10131 machine:localhost,process:5892,member:storage-2 CoherenceServer + 5 2024-03-26 09:26:22.24 127.0.0.1:50251 10131 machine:localhost,process:6600,member:storage-3 CoherenceServer + 4 2024-03-26 08:11:00.537 127.0.0.1:50250 10131 machine:localhost,process:6601,member:storage-4 CoherenceServer +---- + +NOTE: Members are displayed in descending order of departure time. + [#get-network-stats] ==== Get Network Stats diff --git a/pkg/cmd/cluster.go b/pkg/cmd/cluster.go index 607622b..7c5be56 100644 --- a/pkg/cmd/cluster.go +++ b/pkg/cmd/cluster.go @@ -558,7 +558,7 @@ addition information as well as '-v' to displayed additional information.`, sb.WriteString("\nMEMBERS\n") sb.WriteString("-------\n") - sb.WriteString(FormatMembers(members.Members, verboseOutput, storageMap, false)) + sb.WriteString(FormatMembers(members.Members, verboseOutput, storageMap, false, cluster.MembersDepartureCount)) sb.WriteString("\nSERVICES\n") sb.WriteString("--------\n") diff --git a/pkg/cmd/formatting.go b/pkg/cmd/formatting.go index 45abb8e..19f321a 100644 --- a/pkg/cmd/formatting.go +++ b/pkg/cmd/formatting.go @@ -90,12 +90,12 @@ func FormatCurrentCluster(clusterName string) string { // FormatCluster returns a string representing a cluster. func FormatCluster(cluster config.Cluster) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("Cluster Name: %s\n", cluster.ClusterName)) - sb.WriteString(fmt.Sprintf("Version: %s\n", cluster.Version)) - sb.WriteString(fmt.Sprintf("Cluster TotalSize: %d\n", cluster.ClusterSize)) - sb.WriteString(fmt.Sprintf("License Mode: %s\n", cluster.LicenseMode)) - sb.WriteString(fmt.Sprintf("Departure Count: %d\n", cluster.MembersDepartureCount)) - sb.WriteString(fmt.Sprintf("Running: %v\n", cluster.Running)) + sb.WriteString(fmt.Sprintf("Cluster Name: %s\n", cluster.ClusterName)) + sb.WriteString(fmt.Sprintf("Version: %s\n", cluster.Version)) + sb.WriteString(fmt.Sprintf("Cluster TotalSize: %d\n", cluster.ClusterSize)) + sb.WriteString(fmt.Sprintf("License Mode: %s\n", cluster.LicenseMode)) + sb.WriteString(fmt.Sprintf("Departure Count: %d\n", cluster.MembersDepartureCount)) + sb.WriteString(fmt.Sprintf("Running: %v\n", cluster.Running)) return sb.String() } @@ -105,6 +105,9 @@ func FormatCluster(cluster config.Cluster) string { // orderedColumns are the column names, expanded, that should be displayed first for context. func FormatJSONForDescribe(jsonValue []byte, showAllColumns bool, orderedColumns ...string) (string, error) { var result map[string]json.RawMessage + if len(jsonValue) == 0 { + return "", nil + } err := json.Unmarshal(jsonValue, &result) if err != nil { return "", fmt.Errorf("unable to unmarshal value in FormatJSONForDescribe %v", err) @@ -1318,7 +1321,7 @@ func FormatMemberHealth(health []config.HealthSummary) string { } // FormatMembers returns the member's information in a column formatted output. -func FormatMembers(members []config.Member, verbose bool, storageMap map[int]bool, summary bool) string { +func FormatMembers(members []config.Member, verbose bool, storageMap map[int]bool, summary bool, departureCount int) string { var ( memberCount = len(members) alignmentWide = []string{R, L, L, R, L, L, L, L, L, R, R, L, R, R, R} @@ -1406,6 +1409,7 @@ func FormatMembers(members []config.Member, verbose bool, storageMap map[int]boo result := fmt.Sprintf("Total cluster members: %d\n", memberCount) + fmt.Sprintf("Storage enabled count: %d\n", storageCount) + + fmt.Sprintf("Departure count: %d\n\n", departureCount) + fmt.Sprintf("Cluster Heap - Total: %s Used: %s Available: %s (%4.1f%%)\n", strings.TrimSpace(formattingFunction(int64(totalMaxMemoryMB)*MB)), strings.TrimSpace(formattingFunction(int64(totalUsedMB)*MB)), @@ -1428,6 +1432,22 @@ func FormatMembers(members []config.Member, verbose bool, storageMap map[int]boo return result } +// FormatDepartedMembers returns the departed member's information in a column formatted output. +func FormatDepartedMembers(members []config.DepartedMembers) string { + sort.Slice(members, func(p, q int) bool { + return members[p].TimeStamp > members[q].TimeStamp + }) + + table := newFormattedTable().WithHeader(NodeIDColumn, "TIMESTAMP", AddressColumn, "MACHINE ID", "LOCATION", RoleColumn). + WithAlignment([]string{R, L, L, L, L, L}...) + + for _, value := range members { + table.AddRow(value.NodeID, value.TimeStamp, value.Address, value.MachineID, value.Location, value.Role) + } + + return table.String() +} + // FormatNetworkStatistics returns all the member's network statistics in a column formatted output. func FormatNetworkStatistics(members []config.Member) string { var ( diff --git a/pkg/cmd/member.go b/pkg/cmd/member.go index f79a5ea..e76035c 100644 --- a/pkg/cmd/member.go +++ b/pkg/cmd/member.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -42,7 +42,8 @@ var ( p2pSortByPublisher bool p2pSortByReceiver bool - memberSummary bool + memberSummary bool + departedMembers bool tracingRatio float32 ) @@ -89,6 +90,8 @@ func getMembers(cmd *cobra.Command, networkStats bool) error { var ( members = config.Members{} storage = config.StorageDetails{} + cluster = config.Cluster{} + clusterResult []byte membersResult []byte storageResult []byte ) @@ -103,6 +106,11 @@ func getMembers(cmd *cobra.Command, networkStats bool) error { return err } + clusterResult, err = dataFetcher.GetClusterDetailsJSON() + if err != nil { + return err + } + if strings.Contains(OutputFormat, constants.JSONPATH) { result, err = utils.GetJSONPathResults(membersResult, OutputFormat) if err != nil { @@ -115,6 +123,7 @@ func getMembers(cmd *cobra.Command, networkStats bool) error { printWatchHeader(cmd) cmd.Println(FormatCurrentCluster(connection)) + err = json.Unmarshal(membersResult, &members) if err != nil { return utils.GetError(unableToDecode, err) @@ -125,6 +134,11 @@ func getMembers(cmd *cobra.Command, networkStats bool) error { return utils.GetError("unable to decode storage details", err) } + err = json.Unmarshal(clusterResult, &cluster) + if err != nil { + return utils.GetError("unable to decode cluster details", err) + } + storageMap := utils.GetStorageMap(storage) var filteredMembers []config.Member @@ -145,7 +159,15 @@ func getMembers(cmd *cobra.Command, networkStats bool) error { if networkStats { cmd.Println(FormatNetworkStatistics(filteredMembers)) } else { - cmd.Print(FormatMembers(filteredMembers, true, storageMap, memberSummary)) + if departedMembers { + departedList, err1 := decodeDepartedMembers(cluster.MembersDeparted) + if err1 != nil { + return err1 + } + cmd.Println(FormatDepartedMembers(departedList)) + } else { + cmd.Print(FormatMembers(filteredMembers, true, storageMap, memberSummary, cluster.MembersDepartureCount)) + } } } @@ -1129,6 +1151,7 @@ func init() { getMembersCmd.Flags().StringVarP(&roleName, "role", "r", all, roleNameDescription) getMembersCmd.Flags().BoolVarP(&memberSummary, "summary", "S", false, "show a member summary") + getMembersCmd.Flags().BoolVarP(&departedMembers, "departed", "D", false, "show departed members only") getNetworkStatsCmd.Flags().StringVarP(&roleName, "role", "r", all, roleNameDescription) diff --git a/pkg/cmd/persistence.go b/pkg/cmd/persistence.go index c6a153b..2ab59ce 100644 --- a/pkg/cmd/persistence.go +++ b/pkg/cmd/persistence.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -107,6 +107,10 @@ func processPersistenceServices(deDuplicatedServices []config.ServiceSummary, da return } + if len(data) == 0 { + return + } + err1 = json.Unmarshal(data, &coordinator) if err1 != nil { errorSink.AppendError(utils.GetError("unable to unmarshall persistence coordinator", err1)) diff --git a/pkg/cmd/service.go b/pkg/cmd/service.go index 444547e..716845f 100644 --- a/pkg/cmd/service.go +++ b/pkg/cmd/service.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -747,9 +747,11 @@ service is a cache service.`, return err } - err = json.Unmarshal(coordData, &coordinator) - if err != nil { - return err + if len(coordData) > 0 { + err = json.Unmarshal(coordData, &coordinator) + if err != nil { + return err + } } value, err = FormatJSONForDescribe(coordData, false, diff --git a/pkg/cmd/snapshot.go b/pkg/cmd/snapshot.go index 664cfea..bf9f029 100644 --- a/pkg/cmd/snapshot.go +++ b/pkg/cmd/snapshot.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -113,6 +113,9 @@ local snapshots are shown, but you can use the -a option to show archived snapsh newSnapshots = append(newSnapshots, snapshotList...) } else { coordData, err = dataFetcher.GetPersistenceCoordinator(serviceNameValue) + if len(coordData) == 0 { + return + } if err != nil { errorSink.AppendError(err) return diff --git a/pkg/cmd/utils.go b/pkg/cmd/utils.go index 7e0b348..fb68f3e 100644 --- a/pkg/cmd/utils.go +++ b/pkg/cmd/utils.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ @@ -606,3 +606,67 @@ func runClearCommand(cmd *cobra.Command, command string, args ...string) { process.Stdout = cmd.OutOrStdout() _ = process.Run() } + +func decodeDepartedMembers(members []string) ([]config.DepartedMembers, error) { + var ( + membersList = make([]config.DepartedMembers, 0) + errInvalid = errors.New("invalid content") + ) + + const ( + prefix = "Member(" + suffix = ")" + ) + + for _, value := range members { + if !strings.HasPrefix(value, prefix) { + return nil, errInvalid + } + + value = strings.Replace(value, prefix, "", 1) + if !strings.HasSuffix(value, suffix) { + return nil, errInvalid + } + + value, _ = strings.CutSuffix(value, suffix) + + // get the fields + v := strings.Split(value, ", ") + + member := config.DepartedMembers{} + + // go through each field and extract + + count := 1 + for _, f := range v { + s := strings.Split(f, "=") + if len(s) != 2 { + return nil, errInvalid + } + + setField(&member, count, s[1]) + count++ + } + + membersList = append(membersList, member) + } + + return membersList, nil +} + +func setField(member *config.DepartedMembers, field int, value string) { + switch field { + case 1: + member.NodeID = value + case 2: + member.TimeStamp = value + case 3: + member.Address = value + case 4: + member.MachineID = value + case 5: + member.Location = value + case 6: + member.Role = value + } +} diff --git a/pkg/cmd/utils_test.go b/pkg/cmd/utils_test.go new file mode 100644 index 0000000..ea7b15d --- /dev/null +++ b/pkg/cmd/utils_test.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ + +package cmd + +import ( + . "github.com/onsi/gomega" + "github.com/oracle/coherence-cli/pkg/config" + "testing" +) + +func TestDecodeMemberDetails(t *testing.T) { + var ( + result []config.DepartedMembers + g = NewGomegaWithT(t) + invalid1 = []string{"rubbish"} + invalid2 = []string{"Id=4, Timestamp=2024-03-26 08:11:00.537, Address=127.0.0.1:50250, MachineId=10131, Location=machine:localhost,process:6601,member:storage-4, Role=CoherenceServer)"} + invalid3 = []string{"MemberId=4, Timestamp=2024-03-26 08:11:00.537, Address=127.0.0.1:50250, MachineId=10131, Location=machine:localhost,process:6601,member:storage-4, Role=CoherenceServer)"} + valid1 = []string{"Member(Id=4, Timestamp=2024-03-26 08:11:00.537, Address=127.0.0.1:50250, MachineId=10131, Location=machine:localhost,process:6601,member:storage-4, Role=CoherenceServer)"} + valid2 = []string{ + "Member(Id=4, Timestamp=2024-03-26 08:11:00.537, Address=127.0.0.1:50250, MachineId=10131, Location=machine:localhost,process:6601,member:storage-4, Role=CoherenceServer)", + "Member(Id=3, Timestamp=2024-03-26 08:11:00.536, Address=127.0.0.1:50259, MachineId=10135, Location=machine:localhost,process:6601,member:storage-5, Role=CoherenceServer1)"} + ) + + _, err := decodeDepartedMembers(invalid1) + g.Expect(err).To(HaveOccurred()) + + _, err = decodeDepartedMembers(invalid2) + g.Expect(err).To(HaveOccurred()) + + _, err = decodeDepartedMembers(invalid3) + g.Expect(err).To(HaveOccurred()) + + result, err = decodeDepartedMembers(valid1) + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(result).To(Not(BeNil())) + g.Expect(len(result)).To(Equal(1)) + g.Expect(result[0].NodeID).To(Equal("4")) + g.Expect(result[0].TimeStamp).To(Equal("2024-03-26 08:11:00.537")) + g.Expect(result[0].Address).To(Equal("127.0.0.1:50250")) + g.Expect(result[0].MachineID).To(Equal("10131")) + g.Expect(result[0].Location).To(Equal("machine:localhost,process:6601,member:storage-4")) + g.Expect(result[0].Role).To(Equal("CoherenceServer")) + + result, err = decodeDepartedMembers(valid2) + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(result).To(Not(BeNil())) + g.Expect(len(result)).To(Equal(2)) + g.Expect(result[0].NodeID).To(Equal("4")) + g.Expect(result[0].TimeStamp).To(Equal("2024-03-26 08:11:00.537")) + g.Expect(result[0].Address).To(Equal("127.0.0.1:50250")) + g.Expect(result[0].MachineID).To(Equal("10131")) + g.Expect(result[0].Location).To(Equal("machine:localhost,process:6601,member:storage-4")) + g.Expect(result[0].Role).To(Equal("CoherenceServer")) + + g.Expect(result[1].NodeID).To(Equal("3")) + g.Expect(result[1].TimeStamp).To(Equal("2024-03-26 08:11:00.536")) + g.Expect(result[1].Address).To(Equal("127.0.0.1:50259")) + g.Expect(result[1].MachineID).To(Equal("10135")) + g.Expect(result[1].Location).To(Equal("machine:localhost,process:6601,member:storage-5")) + g.Expect(result[1].Role).To(Equal("CoherenceServer1")) +} diff --git a/pkg/config/config_helper.go b/pkg/config/config_helper.go index 1e502e4..5e9f956 100644 --- a/pkg/config/config_helper.go +++ b/pkg/config/config_helper.go @@ -12,12 +12,13 @@ package config // Cluster is a structure to display cluster details for 'describe cluster'. type Cluster struct { - ClusterName string `json:"clusterName"` - ClusterSize int `json:"clusterSize"` - LicenseMode string `json:"licenseMode"` - Version string `json:"version"` - Running bool `json:"running"` - MembersDepartureCount int `json:"membersDepartureCount"` + ClusterName string `json:"clusterName"` + ClusterSize int `json:"clusterSize"` + LicenseMode string `json:"licenseMode"` + Version string `json:"version"` + Running bool `json:"running"` + MembersDepartureCount int `json:"membersDepartureCount"` + MembersDeparted []string `json:"membersDeparted"` } // Members contains an array of member objects. @@ -25,6 +26,16 @@ type Members struct { Members []Member `json:"items"` } +// DepartedMembers contains a decoded departed member from cluster. +type DepartedMembers struct { + NodeID string `json:"nodeId"` + TimeStamp string `json:"timeStamp"` + Address string `json:"address"` + MachineID string `json:"machineID"` + Location string `json:"location"` + Role string `json:"role"` +} + // NetworkStats is used to decode network stats call for a member. type NetworkStats struct { ViewerStatistics []string `json:"viewerStatistics"` diff --git a/pkg/fetcher/http_fetcher.go b/pkg/fetcher/http_fetcher.go index 81b4036..c2b1a5a 100644 --- a/pkg/fetcher/http_fetcher.go +++ b/pkg/fetcher/http_fetcher.go @@ -646,7 +646,7 @@ func (h HTTPFetcher) GetCachePartitions(serviceName, cacheName string) ([]byte, // GetPersistenceCoordinator retrieves persistence coordinator details. func (h HTTPFetcher) GetPersistenceCoordinator(serviceName string) ([]byte, error) { result, err := httpGetRequest(h, servicesPath+getSafeServiceName(h, serviceName)+"/persistence?links=") - if err != nil { + if err != nil && !strings.Contains(err.Error(), errorCode404) { return constants.EmptyByte, utils.GetError("cannot get persistence coordinator for service "+serviceName, err) } return result, nil diff --git a/scripts/run-compat-ce.sh b/scripts/run-compat-ce.sh index 9661993..0ec3b9b 100755 --- a/scripts/run-compat-ce.sh +++ b/scripts/run-compat-ce.sh @@ -1,7 +1,7 @@ #!/bin/bash # -# Copyright (c) 2021, 2023 Oracle and/or its affiliates. +# Copyright (c) 2021, 2024 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl. # @@ -12,11 +12,11 @@ set -e echo "Coherence CE 22.06.7" COHERENCE_BASE_IMAGE=gcr.io/distroless/java17 COHERENCE_VERSION=22.06.7 make clean build-test-images test-e2e-standalone -echo "Coherence CE 23.09.1" -COHERENCE_BASE_IMAGE=gcr.io/distroless/java17 COHERENCE_VERSION=23.09.1 make clean build-test-images test-e2e-standalone +echo "Coherence CE 24.03" +COHERENCE_BASE_IMAGE=gcr.io/distroless/java17 COHERENCE_VERSION=24.03 make clean build-test-images test-e2e-standalone -echo "Coherence CE 23.09.1 with Executor" -COHERENCE_BASE_IMAGE=gcr.io/distroless/java17 PROFILES=,executor COHERENCE_VERSION=23.09.1 make clean build-test-images test-e2e-standalone +echo "Coherence CE 24.03 with Executor" +COHERENCE_BASE_IMAGE=gcr.io/distroless/java17 PROFILES=,executor COHERENCE_VERSION=24.03 make clean build-test-images test-e2e-standalone echo "Coherence CE 14.1.1-0-16" COHERENCE_VERSION=14.1.1-0-16 make clean build-test-images test-e2e-standalone diff --git a/test/common/common.go b/test/common/common.go index e2092e2..af95c46 100644 --- a/test/common/common.go +++ b/test/common/common.go @@ -213,7 +213,7 @@ func RunTestMemberCommands(t *testing.T) { test_utils.EnsureCommandContainsAll(g, t, cliCmd, nodeID, configArg, file, "get", "members", "-c", context.ClusterName) - // test default output format + // test get network-stats test_utils.EnsureCommandContainsAll(g, t, cliCmd, nodeID, configArg, file, "get", "network-stats", "-c", context.ClusterName) @@ -256,6 +256,10 @@ func RunTestMemberCommands(t *testing.T) { test_utils.EnsureCommandContains(g, t, cliCmd, "n/a", configArg, file, "get", "members", "-o", "jsonpath=$.items[0].rackName", "-c", "cluster1") + // test departed members + test_utils.EnsureCommandContainsAll(g, t, cliCmd, "NODE ID,TIMESTAMP,ADDRESS,LOCATION,MACHINE ID,ROLE", + configArg, file, "get", "members", "-D", "-c", context.ClusterName, "-o", "table") + // remove the cluster entries test_utils.EnsureCommandContains(g, t, cliCmd, context.ClusterName, configArg, file, "remove", "cluster", "cluster1") }