Skip to content

Commit 32a0c01

Browse files
juliancarrivick-ibmJonathanLevi
authored andcommitted
Run BDD Compose files intelligently
When bringing up containers with docker compose during BDD testing a time.sleep call is not very efficient. This commit replaces this with an intelligent probing of the containers to determine when they are ready. It determines this by checking the output of the netstat command on each container for listening or connected TCP connections. If a peer is detected, it validates that the number of peers that it has connected to is equal to the number peers detected among the peers. Once all containers are detected as ready or the timeout period elapses, execution continues or aborts respectively. Some functions in peer_basic_impl.py have been moved into new files to avoid circular dependencies. In combination with change 419 in this change BDDTests complete approximately 4 minutes faster. I have had to disable TLS tests in this change too (currently there is only one) on the advice of @JonathonLevi as the certificate used has expired and stops peers using TLS from connecting to each other (meaning they never are 'ready'). This is a stop gap measure until FAB-10 is implemented. Change-Id: I4e5b433746ca92243278acfc61734a14704ce806 Signed-off-by: Julian Carrivick <[email protected]>
1 parent bb23dbd commit 32a0c01

8 files changed

+343
-125
lines changed

bddtests/.behaverc

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ tags=~@issue_767
66
~@issue_1565
77
~@issue_RBAC_TCERT_With_Attributes
88
~@sdk
9+
~@TLS

bddtests/docker-compose-1-empty.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
empty:
22
image: hyperledger/fabric-src
3-
command: bash -c "sleep inf"
3+
# TCP Listen on a port to satisfy the container 'ready' condition
4+
command: nc -k -l 50000

bddtests/peer_basic.feature

+35-28
Original file line numberDiff line numberDiff line change
@@ -628,19 +628,16 @@ Feature: Network of Peers
628628
| docker-compose-4-consensus-noops.yml | 60 |
629629

630630

631-
# @doNotDecompose
632-
# @wip
633-
Scenario: basic startup of 3 validating peers
634-
Given we compose "docker-compose-3.yml"
635-
When requesting "/chain" from "vp0"
636-
Then I should get a JSON response with "height" = "1"
631+
Scenario: basic startup of 3 validating peers
632+
Given we compose "docker-compose-3.yml"
633+
When requesting "/chain" from "vp0"
634+
Then I should get a JSON response with "height" = "1"
637635

638-
@TLS
639-
# @doNotDecompose
640-
Scenario: basic startup of 2 validating peers using TLS
641-
Given we compose "docker-compose-2-tls-basic.yml"
642-
When requesting "/chain" from "vp0"
643-
Then I should get a JSON response with "height" = "1"
636+
@TLS
637+
Scenario: basic startup of 2 validating peers using TLS
638+
Given we compose "docker-compose-2-tls-basic.yml"
639+
When requesting "/chain" from "vp0"
640+
Then I should get a JSON response with "height" = "1"
644641

645642

646643
Scenario Outline: 4 peers and 1 membersrvc, consensus still works if one backup replica fails
@@ -895,7 +892,8 @@ Feature: Network of Peers
895892

896893

897894
@issue_1091
898-
Scenario Outline: chaincode example02 with 4 peers and 1 membersrvc, issue #1019 (out of date peer)
895+
@doNotDecompose
896+
Scenario Outline: chaincode example02 with 4 peers and 1 membersrvc, issue #1091 (out of date peer)
899897

900898
Given we compose "<ComposeFile>"
901899
And I register with CA supplying username "binhn" and secret "7avZQLwcUe9q" on peers:
@@ -938,22 +936,24 @@ Feature: Network of Peers
938936
# Now start vp3 again
939937
Given I start peers:
940938
| vp3 |
941-
And I wait "15" seconds
942939

943-
# Invoke 8 more txs, this will trigger a state transfer, but it cannot complete
940+
# Invoke some more txs, this will trigger a state transfer, but it cannot complete
944941
When I invoke chaincode "example2" function name "invoke" on "vp0" "8" times
945942
|arg1|arg2|arg3|
946943
| a | b | 10 |
947944
Then I should have received a transactionID
948945
Then I wait up to "<WaitTime>" seconds for transaction to be committed to peers:
949946
| vp0 | vp1 | vp2 |
950-
# wait a bit to make sure the state is invalid on vp3
951-
Then I wait "20" seconds
952947
When I query chaincode "example2" function name "query" with value "a" on peers:
953948
| vp0 | vp1 | vp2 |
954-
Then I should get a JSON response from peers with "result.message" = "21"
955949
| vp0 | vp1 | vp2 |
956-
When I unconditionally query chaincode "example2" function name "query" with value "a" on peers:
950+
Then I should get a JSON response from peers with "result.message" = "21"
951+
952+
# Force VP3 to attempt to sync with the rest of the peers
953+
When I invoke chaincode "example2" function name "invoke" on "vp3"
954+
|arg1|arg2|arg3|
955+
| a | b | 10 |
956+
And I unconditionally query chaincode "example2" function name "query" with value "a" on peers:
957957
| vp3 |
958958
Then I should get a JSON response from peers with "error.data" = "Error when querying chaincode: Error: state may be inconsistent, cannot query"
959959
| vp3 |
@@ -1159,36 +1159,43 @@ Scenario: chaincode example02 with 4 peers, two stopped
11591159
| a | 100 | b | 200 |
11601160
Then I should have received a chaincode name
11611161
Then I wait up to "60" seconds for transaction to be committed to peers:
1162-
| vp0 | vp1 | vp2 |
1162+
| vp0 | vp1 | vp2 | vp3 |
11631163

11641164
When I query chaincode "example2" function name "query" with value "a" on peers:
1165-
| vp0 | vp1 | vp2 | vp3 |
1165+
| vp0 | vp1 | vp2 | vp3 |
11661166
Then I should get a JSON response from peers with "result.message" = "100"
1167-
| vp0 | vp1 | vp2 | vp3 |
1167+
| vp0 | vp1 | vp2 | vp3 |
11681168

11691169
Given I stop peers:
1170-
| vp2 | vp3 |
1170+
| vp2 | vp3 |
11711171

11721172
When I invoke chaincode "example2" function name "invoke" on "vp0"
11731173
|arg1|arg2|arg3|
11741174
| a | b | 10 |
11751175
Then I should have received a transactionID
11761176

11771177
Given I start peers:
1178-
| vp3 |
1179-
And I wait "15" seconds
1178+
| vp3 |
1179+
1180+
# Make sure vp3 catches up first
1181+
Then I wait up to "60" seconds for transaction to be committed to peers:
1182+
| vp0 | vp1 | vp3 |
1183+
When I query chaincode "example2" function name "query" with value "a" on peers:
1184+
| vp0 | vp1 | vp3 |
1185+
Then I should get a JSON response from peers with "result.message" = "90"
1186+
| vp0 | vp1 | vp3 |
11801187

11811188
When I invoke chaincode "example2" function name "invoke" on "vp0" "9" times
11821189
|arg1|arg2|arg3|
11831190
| a | b | 10 |
11841191
Then I should have received a transactionID
11851192
Then I wait up to "60" seconds for transaction to be committed to peers:
1186-
| vp0 | vp1 | vp3 |
1193+
| vp0 | vp1 | vp3 |
11871194

11881195
When I query chaincode "example2" function name "query" with value "a" on peers:
1189-
| vp0 | vp1 | vp3 |
1196+
| vp0 | vp1 | vp3 |
11901197
Then I should get a JSON response from peers with "result.message" = "0"
1191-
| vp0 | vp1 | vp3 |
1198+
| vp0 | vp1 | vp3 |
11921199

11931200
@issue_1874b
11941201
#@doNotDecompose

bddtests/steps/bdd_compose_util.py

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#
2+
# Copyright IBM Corp. 2016 All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
import os, time, re, requests
18+
19+
from bdd_rest_util import buildUrl, CORE_REST_PORT
20+
from bdd_json_util import getAttributeFromJSON
21+
from bdd_test_util import cli_call, bdd_log
22+
23+
class ContainerData:
24+
def __init__(self, containerName, ipAddress, envFromInspect, composeService):
25+
self.containerName = containerName
26+
self.ipAddress = ipAddress
27+
self.envFromInspect = envFromInspect
28+
self.composeService = composeService
29+
30+
def getEnv(self, key):
31+
envValue = None
32+
for val in self.envFromInspect:
33+
if val.startswith(key):
34+
envValue = val[len(key):]
35+
break
36+
if envValue == None:
37+
raise Exception("ENV key not found ({0}) for container ({1})".format(key, self.containerName))
38+
return envValue
39+
40+
def __str__(self):
41+
return "{} - {}".format(self.containerName, self.ipAddress)
42+
43+
def __repr__(self):
44+
return self.__str__()
45+
46+
def getDockerComposeFileArgsFromYamlFile(compose_yaml):
47+
parts = compose_yaml.split()
48+
args = []
49+
for part in parts:
50+
args = args + ["-f"] + [part]
51+
return args
52+
53+
def parseComposeOutput(context):
54+
"""Parses the compose output results and set appropriate values into context. Merges existing with newly composed."""
55+
# Use the prefix to get the container name
56+
containerNamePrefix = os.path.basename(os.getcwd()) + "_"
57+
containerNames = []
58+
for l in context.compose_error.splitlines():
59+
tokens = l.split()
60+
bdd_log(tokens)
61+
if 1 < len(tokens):
62+
thisContainer = tokens[1]
63+
if containerNamePrefix not in thisContainer:
64+
thisContainer = containerNamePrefix + thisContainer + "_1"
65+
if thisContainer not in containerNames:
66+
containerNames.append(thisContainer)
67+
68+
bdd_log("Containers started: ")
69+
bdd_log(containerNames)
70+
# Now get the Network Address for each name, and set the ContainerData onto the context.
71+
containerDataList = []
72+
for containerName in containerNames:
73+
output, error, returncode = \
74+
cli_call(["docker", "inspect", "--format", "{{ .NetworkSettings.IPAddress }}", containerName], expect_success=True)
75+
bdd_log("container {0} has address = {1}".format(containerName, output.splitlines()[0]))
76+
ipAddress = output.splitlines()[0]
77+
78+
# Get the environment array
79+
output, error, returncode = \
80+
cli_call(["docker", "inspect", "--format", "{{ .Config.Env }}", containerName], expect_success=True)
81+
env = output.splitlines()[0][1:-1].split()
82+
83+
# Get the Labels to access the com.docker.compose.service value
84+
output, error, returncode = \
85+
cli_call(["docker", "inspect", "--format", "{{ .Config.Labels }}", containerName], expect_success=True)
86+
labels = output.splitlines()[0][4:-1].split()
87+
dockerComposeService = [composeService[27:] for composeService in labels if composeService.startswith("com.docker.compose.service:")][0]
88+
bdd_log("dockerComposeService = {0}".format(dockerComposeService))
89+
bdd_log("container {0} has env = {1}".format(containerName, env))
90+
containerDataList.append(ContainerData(containerName, ipAddress, env, dockerComposeService))
91+
# Now merge the new containerData info with existing
92+
newContainerDataList = []
93+
if "compose_containers" in context:
94+
# Need to merge I new list
95+
newContainerDataList = context.compose_containers
96+
newContainerDataList = newContainerDataList + containerDataList
97+
98+
setattr(context, "compose_containers", newContainerDataList)
99+
bdd_log("")
100+
101+
def allContainersAreReadyWithinTimeout(context, timeout):
102+
timeoutTimestamp = time.time() + timeout
103+
formattedTime = time.strftime("%X", time.localtime(timeoutTimestamp))
104+
bdd_log("All containers should be up by {}".format(formattedTime))
105+
106+
allContainers = context.compose_containers
107+
108+
for container in allContainers:
109+
if not containerIsInitializedByTimestamp(container, timeoutTimestamp):
110+
return False
111+
112+
peersAreReady = peersAreReadyByTimestamp(context, allContainers, timeoutTimestamp)
113+
114+
if peersAreReady:
115+
bdd_log("All containers in ready state, ready to proceed")
116+
117+
return peersAreReady
118+
119+
def containerIsInitializedByTimestamp(container, timeoutTimestamp):
120+
while containerIsNotInitialized(container):
121+
if timestampExceeded(timeoutTimestamp):
122+
bdd_log("Timed out waiting for {} to initialize".format(container.containerName))
123+
return False
124+
125+
bdd_log("{} not initialized, waiting...".format(container.containerName))
126+
time.sleep(1)
127+
128+
bdd_log("{} now available".format(container.containerName))
129+
return True
130+
131+
def timestampExceeded(timeoutTimestamp):
132+
return time.time() > timeoutTimestamp
133+
134+
def containerIsNotInitialized(container):
135+
return not containerIsInitialized(container)
136+
137+
def containerIsInitialized(container):
138+
isReady = tcpPortsAreReady(container)
139+
isReady = isReady and restPortRespondsIfContainerIsPeer(container)
140+
141+
return isReady
142+
143+
def tcpPortsAreReady(container):
144+
netstatOutput = getContainerNetstatOutput(container.containerName)
145+
146+
for line in netstatOutput.splitlines():
147+
if re.search("ESTABLISHED|LISTEN", line):
148+
return True
149+
150+
bdd_log("No TCP connections are ready in container {}".format(container.containerName))
151+
return False
152+
153+
def getContainerNetstatOutput(containerName):
154+
command = ["docker", "exec", containerName, "netstat", "-atun"]
155+
stdout, stderr, returnCode = cli_call(command, expect_success=False)
156+
157+
return stdout
158+
159+
def restPortRespondsIfContainerIsPeer(container):
160+
containerName = container.containerName
161+
command = ["docker", "exec", containerName, "curl", "localhost:{}".format(CORE_REST_PORT)]
162+
163+
if containerIsPeer(container):
164+
stdout, stderr, returnCode = cli_call(command, expect_success=False)
165+
166+
if returnCode != 0:
167+
bdd_log("Connection to REST Port on {} failed".format(containerName))
168+
169+
return returnCode == 0
170+
171+
return True
172+
173+
def peersAreReadyByTimestamp(context, containers, timeoutTimestamp):
174+
peers = getPeerContainers(containers)
175+
bdd_log("Detected Peers: {}".format(peers))
176+
177+
for peer in peers:
178+
if not peerIsReadyByTimestamp(context, peer, peers, timeoutTimestamp):
179+
return False
180+
181+
return True
182+
183+
def getPeerContainers(containers):
184+
peers = []
185+
186+
for container in containers:
187+
if containerIsPeer(container):
188+
peers.append(container)
189+
190+
return peers
191+
192+
def containerIsPeer(container):
193+
# This is not an ideal way of detecting whether a container is a peer or not since
194+
# we are depending on the name of the container. Another way of detecting peers is
195+
# is to determine if the container is listening on the REST port. However, this method
196+
# may run before the listening port is ready. Hence, as along as the current
197+
# convention of vp[0-9] is adhered to this function will be good enough.
198+
return re.search("vp[0-9]+", container.containerName, re.IGNORECASE)
199+
200+
def peerIsReadyByTimestamp(context, peerContainer, allPeerContainers, timeoutTimestamp):
201+
while peerIsNotReady(context, peerContainer, allPeerContainers):
202+
if timestampExceeded(timeoutTimestamp):
203+
bdd_log("Timed out waiting for peer {}".format(peerContainer.containerName))
204+
return False
205+
206+
bdd_log("Peer {} not ready, waiting...".format(peerContainer.containerName))
207+
time.sleep(1)
208+
209+
bdd_log("Peer {} now available".format(peerContainer.containerName))
210+
return True
211+
212+
def peerIsNotReady(context, thisPeer, allPeers):
213+
return not peerIsReady(context, thisPeer, allPeers)
214+
215+
def peerIsReady(context, thisPeer, allPeers):
216+
connectedPeers = getConnectedPeersFromPeer(context, thisPeer)
217+
218+
if connectedPeers is None:
219+
return False
220+
221+
numPeers = len(allPeers)
222+
numConnectedPeers = len(connectedPeers)
223+
224+
if numPeers != numConnectedPeers:
225+
bdd_log("Expected {} peers, got {}".format(numPeers, numConnectedPeers))
226+
bdd_log("Connected Peers: {}".format(connectedPeers))
227+
bdd_log("Expected Peers: {}".format(allPeers))
228+
229+
return numPeers == numConnectedPeers
230+
231+
def getConnectedPeersFromPeer(context, thisPeer):
232+
url = buildUrl(context, thisPeer.ipAddress, "/network/peers")
233+
response = requests.get(url, headers={'Accept': 'application/json'}, verify=False)
234+
235+
if response.status_code != 200:
236+
return None
237+
238+
return getAttributeFromJSON("peers", response.json(), "There should be a peer json attribute")

bddtests/steps/bdd_json_util.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#
2+
# Copyright IBM Corp. 2016 All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
def getAttributeFromJSON(attribute, jsonObject, msg):
18+
return getHierarchyAttributesFromJSON(attribute.split("."), jsonObject, msg)
19+
20+
def getHierarchyAttributesFromJSON(attributes, jsonObject, msg):
21+
if len(attributes) > 0:
22+
assert attributes[0] in jsonObject, msg
23+
return getHierarchyAttributesFromJSON(attributes[1:], jsonObject[attributes[0]], msg)
24+
return jsonObject

0 commit comments

Comments
 (0)