Skip to content

Commit c754edc

Browse files
authored
Merge pull request #16172 from shoshanatech/socket-release-all-2
Release waiting processes when destroying a socket (without ThreadedFFI dependency)
2 parents 9e40a62 + 82d4d7b commit c754edc

File tree

2 files changed

+136
-76
lines changed

2 files changed

+136
-76
lines changed

src/Network-Kernel/Socket.class.st

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -465,11 +465,18 @@ Socket >> connectTo: hostAddress port: port [
465465
Socket >> connectTo: hostAddress port: port waitForConnectionFor: timeout [
466466
"Initiate a connection to the given port at the given host
467467
address. Waits until the connection is established or time outs."
468+
468469
self connectNonBlockingTo: hostAddress port: port.
469470
self
470471
waitForConnectionFor: timeout
471-
ifTimedOut: [ConnectionTimedOut signal: 'Cannot connect to '
472-
, (NetNameResolver stringFromAddress: hostAddress) , ':' , port asString]
472+
ifClosed: [
473+
ConnectionClosed signal: 'Connection aborted to '
474+
, (NetNameResolver stringFromAddress: hostAddress) , ':'
475+
, port asString ]
476+
ifTimedOut: [
477+
ConnectionTimedOut signal: 'Cannot connect to '
478+
, (NetNameResolver stringFromAddress: hostAddress) , ':'
479+
, port asString ]
473480
]
474481

475482
{ #category : 'connection open/close' }
@@ -489,18 +496,30 @@ Socket >> dataAvailable [
489496

490497
{ #category : 'initialize - destroy' }
491498
Socket >> destroy [
492-
"Destroy this socket. Its connection, if any, is aborted and its resources are freed. Do nothing if the socket has already been destroyed (i.e., if its socketHandle is nil)."
499+
"Destroy this socket. Its connection, if any, is aborted and its resources are freed.
500+
Any processes waiting on the socket are freed immediately, but it is up to them to
501+
recognize that the socket has been destroyed.
502+
Do nothing if the socket has already been destroyed (i.e., if its socketHandle is nil)."
493503

494-
socketHandle
495-
ifNotNil: [
496-
self isValid
497-
ifTrue: [ self primSocketDestroy: socketHandle ].
498-
Smalltalk unregisterExternalObject: semaphore.
499-
Smalltalk unregisterExternalObject: readSemaphore.
500-
Smalltalk unregisterExternalObject: writeSemaphore.
501-
socketHandle := nil.
502-
readSemaphore := writeSemaphore := semaphore := nil.
503-
self unregister ]
504+
socketHandle ifNotNil: [
505+
| saveSemaphores |
506+
self isValid ifTrue: [ self primSocketDestroy: socketHandle ].
507+
socketHandle := nil.
508+
Smalltalk unregisterExternalObject: semaphore.
509+
Smalltalk unregisterExternalObject: readSemaphore.
510+
Smalltalk unregisterExternalObject: writeSemaphore.
511+
"Stash the semaphores and nil them before signaling to make sure
512+
no caller gets a chance to wait on them again and block forever."
513+
saveSemaphores := {
514+
semaphore.
515+
readSemaphore.
516+
writeSemaphore }.
517+
semaphore := readSemaphore := writeSemaphore := nil.
518+
"A single #signal should be sufficient, as multiple processes trying to
519+
read or write at once will result in undefined behavior anyway as their
520+
data gets all mixed up together."
521+
saveSemaphores do: [ :each | each signal ].
522+
self unregister ]
504523
]
505524

506525
{ #category : 'receiving' }
@@ -1334,6 +1353,7 @@ Socket >> retryIfWaitingForConnection: aBlock [
13341353
ifTrue: [
13351354
self
13361355
waitForConnectionFor: Socket standardTimeout
1356+
ifClosed: nil
13371357
ifTimedOut: nil.
13381358
aBlock value ]
13391359
ifFalse: [ e pass ] ]
@@ -1529,15 +1549,19 @@ Socket >> setPort: port [
15291549

15301550
{ #category : 'queries' }
15311551
Socket >> socketError [
1532-
^self primSocketError: socketHandle
1552+
1553+
^ socketHandle ifNotNil: [ self primSocketError: socketHandle ]
15331554
]
15341555

15351556
{ #category : 'queries' }
15361557
Socket >> socketErrorMessage [
15371558

1538-
^ [ OSPlatform current getErrorMessage: self socketError ]
1539-
on: Error
1540-
do: [ 'Error code: ' , self socketError printString ]
1559+
^ self socketError
1560+
ifNil: [ 'Socket destroyed, cannot retrieve error message' ]
1561+
ifNotNil: [ :err |
1562+
[ OSPlatform current getErrorMessage: err ]
1563+
on: Error
1564+
do: [ 'Error code: ' , err printString ] ]
15411565
]
15421566

15431567
{ #category : 'accessing' }
@@ -1569,40 +1593,57 @@ Socket >> unregister [
15691593
{ #category : 'waiting' }
15701594
Socket >> waitForAcceptFor: timeout [
15711595
"Wait and accept an incoming connection. Return nil if it fails"
1572-
^ self waitForAcceptFor: timeout ifTimedOut: [nil]
1596+
1597+
^ self waitForAcceptFor: timeout ifClosed: nil ifTimedOut: nil
15731598
]
15741599

15751600
{ #category : 'waiting' }
1576-
Socket >> waitForAcceptFor: timeout ifTimedOut: timeoutBlock [
1601+
Socket >> waitForAcceptFor: timeout ifClosed: closedBlock ifTimedOut: timeoutBlock [
15771602
"Wait and accept an incoming connection"
1578-
self waitForConnectionFor: timeout ifTimedOut: [^timeoutBlock value].
1579-
^self accept
1603+
1604+
self
1605+
waitForConnectionFor: timeout
1606+
ifClosed: [ ^ closedBlock value ]
1607+
ifTimedOut: [ ^ timeoutBlock value ].
1608+
^ self accept
15801609
]
15811610

15821611
{ #category : 'waiting' }
15831612
Socket >> waitForConnectionFor: timeout [
15841613
"Wait up until the given deadline for a connection to be established. Return true if it is established by the deadline, false if not."
15851614

1586-
^self
1587-
waitForConnectionFor: timeout
1588-
ifTimedOut: [ConnectionTimedOut signal: 'Failed to connect in ', timeout asString, ' seconds']
1615+
^ self
1616+
waitForConnectionFor: timeout
1617+
ifClosed: [
1618+
ConnectionClosed signal: (socketHandle
1619+
ifNil: [ 'Socket destroyed while connecting' ]
1620+
ifNotNil: [
1621+
'Connection aborted or failed: ' , self socketErrorMessage ]) ]
1622+
ifTimedOut: [
1623+
ConnectionTimedOut signal:
1624+
'Failed to connect in ' , timeout asString , ' seconds' ]
15891625
]
15901626

15911627
{ #category : 'waiting' }
1592-
Socket >> waitForConnectionFor: timeout ifTimedOut: timeoutBlock [
1593-
"Wait up until the given deadline for a connection to be established. Return true if it is established by the deadline, false if not."
1628+
Socket >> waitForConnectionFor: timeout ifClosed: closedBlock ifTimedOut: timeoutBlock [
1629+
"Wait up until the given deadline for a connection to be established.
1630+
Evaluate closedBlock if the connection is closed locally,
1631+
or timeoutBlock if the deadline expires.
1632+
1633+
We should separately detect the case of a connection being refused here as well."
15941634

1595-
| startTime msecsDelta msecsEllapsed status |
1635+
| startTime msecsDelta msecsElapsed status |
15961636
startTime := Time millisecondClockValue.
15971637
msecsDelta := (timeout * 1000) truncated.
1638+
1639+
[
15981640
status := self primSocketConnectionStatus: socketHandle.
1599-
[(status = WaitingForConnection) and: [(msecsEllapsed := Time millisecondsSince: startTime) < msecsDelta]]
1600-
whileTrue: [
1601-
semaphore waitTimeoutMilliseconds: msecsDelta - msecsEllapsed.
1602-
status := self primSocketConnectionStatus: socketHandle].
1641+
status == WaitingForConnection and: [
1642+
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta ] ]
1643+
whileTrue: [ semaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ].
16031644

1604-
status = Connected ifFalse: [^timeoutBlock value].
1605-
^ true
1645+
status == WaitingForConnection ifTrue: [ ^ timeoutBlock value ].
1646+
status == Connected ifFalse: [ ^ closedBlock value ]
16061647
]
16071648

16081649
{ #category : 'waiting' }
@@ -1628,7 +1669,9 @@ Socket >> waitForDataFor: timeout [
16281669

16291670
{ #category : 'waiting' }
16301671
Socket >> waitForDataFor: timeout ifClosed: closedBlock ifTimedOut: timedOutBlock [
1631-
"Wait for the given nr of seconds for data to arrive."
1672+
"Wait for the given nr of seconds for data to arrive.
1673+
If it does not, execute <timedOutBlock>. If the connection
1674+
is closed before any data arrives, execute <closedBlock>."
16321675

16331676
| startTime msecsDelta msecsElapsed |
16341677
startTime := Time millisecondClockValue.
@@ -1637,21 +1680,17 @@ Socket >> waitForDataFor: timeout ifClosed: closedBlock ifTimedOut: timedOutBloc
16371680
self isConnected ifFalse: [ ^ closedBlock value ].
16381681
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta
16391682
ifFalse: [ ^ timedOutBlock value ].
1640-
self readSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ]
1683+
readSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ]
16411684
]
16421685

16431686
{ #category : 'waiting' }
16441687
Socket >> waitForDataIfClosed: closedBlock [
16451688
"Wait indefinitely for data to arrive. This method will block until
16461689
data is available or the socket is closed."
16471690

1648-
[true]
1649-
whileTrue: [
1650-
self dataAvailable
1651-
ifTrue: [^self].
1652-
self isConnected
1653-
ifFalse: [^closedBlock value].
1654-
self readSemaphore wait]
1691+
[ self dataAvailable ] whileFalse: [
1692+
self isConnected ifFalse: [ ^ closedBlock value ].
1693+
readSemaphore wait ]
16551694
]
16561695

16571696
{ #category : 'waiting' }
@@ -1662,22 +1701,23 @@ Socket >> waitForDisconnectionFor: timeout [
16621701
(e.g., because he has called 'close' to send a close request to the other end)
16631702
before calling this method."
16641703

1665-
| startTime msecsDelta status |
1704+
| startTime msecsDelta msecsElapsed status |
16661705
startTime := Time millisecondClockValue.
16671706
msecsDelta := (timeout * 1000) truncated.
1707+
1708+
[
16681709
status := self primSocketConnectionStatus: socketHandle.
1669-
[((status == Connected) or: [(status == ThisEndClosed)]) and:
1670-
[(Time millisecondsSince: startTime) < msecsDelta]] whileTrue: [
1671-
self discardReceivedData.
1672-
self readSemaphore waitTimeoutMilliseconds:
1673-
(msecsDelta - (Time millisecondsSince: startTime) max: 0).
1674-
status := self primSocketConnectionStatus: socketHandle].
1710+
(status == Connected or: [ status == ThisEndClosed ]) and: [
1711+
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta ] ]
1712+
whileTrue: [
1713+
self discardReceivedData.
1714+
readSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ].
16751715
^ status ~= Connected
16761716
]
16771717

16781718
{ #category : 'waiting' }
16791719
Socket >> waitForSendDoneFor: timeout [
1680-
"Wait up until the given deadline for the current send operation to complete.
1720+
"Wait up until the given deadline for the current send operation to complete.
16811721
Raise an exception if the timeout expires or the connection is closed before sending finishes."
16821722

16831723
^ self
@@ -1691,18 +1731,21 @@ Socket >> waitForSendDoneFor: timeout ifClosed: closedBlock ifTimedOut: timedOut
16911731
"Wait up until the given deadline for the current send operation to complete.
16921732
If it does not, execute <timedOutBlock>. If the connection is closed before
16931733
the send completes, execute <closedBlock>."
1694-
| startTime msecsDelta msecsElapsed sendDone |
1695-
1734+
1735+
| startTime msecsDelta msecsElapsed |
16961736
startTime := Time millisecondClockValue.
16971737
msecsDelta := (timeout * 1000) truncated.
16981738
"Connection end and final data can happen fast, so test in this order"
1699-
[ sendDone := self sendDone ] whileFalse: [
1739+
[ self sendDone ] whileFalse: [
17001740
self isConnected ifFalse: [ ^ closedBlock value ].
17011741
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta
17021742
ifFalse: [ ^ timedOutBlock value ].
1703-
self writeSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ].
1704-
1705-
^ sendDone
1743+
writeSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ].
1744+
1745+
"For backward compatibility with Pharo <= 11, return a boolean indicating
1746+
whether the send is completed. The loop will only terminate when this
1747+
is the case, so simply return true."
1748+
^ true
17061749
]
17071750

17081751
{ #category : 'accessing' }

src/Network-Tests/SocketStreamTest.class.st

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ SocketStreamTest >> classToBeTested [
1919
^ SocketStream
2020
]
2121

22+
{ #category : 'stream protocol' }
23+
SocketStreamTest >> closeServerAndSendOnceFromClient [
24+
25+
serverStream close.
26+
"The first send after the other end is closed will not have problems as long as it fits within the send buffer."
27+
self
28+
shouldnt: [
29+
clientStream
30+
nextPutAll: 'A line of text';
31+
flush ]
32+
raise: NetworkError.
33+
"Wait for the state of the underlying socket to update--normally this happens near-instantly,
34+
but when running tests in a batch of other sockets-related tests, e.g. in CI, often a subsequent send
35+
will still succeed without the wait, causing the test to fail."
36+
(Delay forMilliseconds: 100) wait
37+
]
38+
2239
{ #category : 'running' }
2340
SocketStreamTest >> setUp [
2441
| listener clientSocket serverSocket |
@@ -48,13 +65,25 @@ SocketStreamTest >> tearDown [
4865

4966
{ #category : 'stream protocol' }
5067
SocketStreamTest >> testFlushLargeMessageOtherEndClosed [
51-
"A large send will be broken into chunks and fail on the second one."
52-
68+
"A large send should be broken into chunks and fail once the TCP send buffer
69+
is full. Ensure we actually exceed the size of the send buffer, but also try
70+
to reduce it first so we don't need an excessively large message. Some platforms
71+
(Linux) have minimum values for this setting that prevent us from relying on
72+
lowering it alone. Attempt to set a 65KiB buffer and double whatever we get."
73+
74+
| bufferSize |
75+
"Windows seems to accept arbitrarily large send() payloads regardless of the
76+
send buffer size, so we can't force a wait other than by calling #flush twice,
77+
which would defeat the whole purpose of the test.
78+
Skip on Windows for now as this is not a regression."
79+
OSPlatform current isWindows ifTrue: [ self skip ].
80+
clientStream socket setOption: 'SO_SNDBUF' value: 2 ** 16.
81+
bufferSize := (clientStream socket getOption: 'SO_SNDBUF') second.
5382
serverStream close.
5483
self
55-
should: [ "256kiB"
84+
should: [
5685
clientStream
57-
nextPutAll: (String new: 2 ** 18 withAll: $a);
86+
nextPutAll: (String new: bufferSize * 2 withAll: $a);
5887
flush ]
5988
raise: ConnectionClosed
6089
]
@@ -63,15 +92,7 @@ SocketStreamTest >> testFlushLargeMessageOtherEndClosed [
6392
SocketStreamTest >> testFlushOtherEndClosed [
6493
"Ensure that #flush properly raises an error when called when the other end is closed."
6594

66-
serverStream close.
67-
"The first send after the other end is closed will not have problems as long as it fits within the send buffer."
68-
self
69-
shouldnt: [
70-
clientStream
71-
nextPutAll: 'A line of text';
72-
flush ]
73-
raise: NetworkError.
74-
"The next send will wait for the first to finish and discover the error condition."
95+
self closeServerAndSendOnceFromClient.
7596
self
7697
should: [
7798
clientStream
@@ -105,15 +126,11 @@ SocketStreamTest >> testNextIntoCloseNonSignaling [
105126

106127
{ #category : 'stream protocol' }
107128
SocketStreamTest >> testNextPutAllFlushOtherEndClosed [
108-
"#nextPutAllFlush: does its own error handling, so needs to be tested separately.
109-
Having some content in the buffer first means that the direct send of the second buffer will encounter an error."
129+
"#nextPutAllFlush: does its own error handling, so needs to be tested separately."
110130

111-
serverStream close.
131+
self closeServerAndSendOnceFromClient.
112132
self
113-
should: [
114-
clientStream
115-
nextPutAll: 'A line of text';
116-
nextPutAllFlush: 'Another line of text' ]
133+
should: [ clientStream nextPutAllFlush: 'Another line of text' ]
117134
raise: ConnectionClosed
118135
]
119136

0 commit comments

Comments
 (0)