Skip to content

Commit

Permalink
Release waiting processes on socket when that one is destroyed
Browse files Browse the repository at this point in the history
This fixes the issue pharo-project/pharo#15975 presented in older version of pharo
  • Loading branch information
jvanecek committed Aug 29, 2024
1 parent 073c3b0 commit 8efb901
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 0 deletions.
10 changes: 10 additions & 0 deletions source/Ansible-Pharo-Pending-Patches/Semaphore.extension.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Extension { #name : 'Semaphore' }

{ #category : '*Ansible-Pharo-Pending-Patches' }
Semaphore >> waitTimeoutMilliseconds: anInteger [
"Wait on this semaphore for up to the given number of milliseconds, then timeout.
Return true if the deadline expired, false otherwise."
| d |
d := DelayWaitTimeout new setDelay: (anInteger max: 0) forSemaphore: self.
^d wait
]
145 changes: 145 additions & 0 deletions source/Ansible-Pharo-Pending-Patches/Socket.extension.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
Extension { #name : 'Socket' }

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> connectTo: hostAddress port: port waitForConnectionFor: timeout [
"Initiate a connection to the given port at the given host
address. Waits until the connection is established or time outs."

self connectNonBlockingTo: hostAddress port: port.
self
waitForConnectionFor: timeout
ifClosed: [
ConnectionClosed signal: 'Connection aborted to '
, (NetNameResolver stringFromAddress: hostAddress) , ':'
, port asString ]
ifTimedOut: [
ConnectionTimedOut signal: 'Cannot connect to '
, (NetNameResolver stringFromAddress: hostAddress) , ':'
, port asString ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> destroy [
"Destroy this socket. Its connection, if any, is aborted and its resources are freed.
Any processes waiting on the socket are freed immediately, but it is up to them to
recognize that the socket has been destroyed.
Do nothing if the socket has already been destroyed (i.e., if its socketHandle is nil)."

socketHandle ifNotNil: [
| saveSemaphores |
self isValid ifTrue: [ self primSocketDestroy: socketHandle ].
socketHandle := nil.
Smalltalk unregisterExternalObject: semaphore.
Smalltalk unregisterExternalObject: readSemaphore.
Smalltalk unregisterExternalObject: writeSemaphore.
"Stash the semaphores and nil them before signaling to make sure
no caller gets a chance to wait on them again and block forever."
saveSemaphores := {
semaphore.
readSemaphore.
writeSemaphore }.
semaphore := readSemaphore := writeSemaphore := nil.
"A single #signal should be sufficient, as multiple processes trying to
read or write at once will result in undefined behavior anyway as their
data gets all mixed up together."
saveSemaphores do: [ :each | each signal ].
self unregister ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> retryIfWaitingForConnection: aBlock [

^ aBlock
on: ExpectedSocketFailure
do: [ :e |
self isWaitingForConnection
ifTrue: [
self
waitForConnectionFor: Socket standardTimeout
ifClosed: nil
ifTimedOut: nil.
aBlock value ]
ifFalse: [ e pass ] ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> socketError [

^ socketHandle ifNotNil: [ self primSocketError: socketHandle ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> socketErrorMessage [

^ self socketError
ifNil: [ 'Socket destroyed, cannot retrieve error message' ]
ifNotNil: [ :err |
[ OSPlatform current getErrorMessage: err ]
on: Error
do: [ 'Error code: ' , err printString ] ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> waitForAcceptFor: timeout ifClosed: closedBlock ifTimedOut: timeoutBlock [
"Wait and accept an incoming connection"

self
waitForConnectionFor: timeout
ifClosed: [ ^ closedBlock value ]
ifTimedOut: [ ^ timeoutBlock value ].
^ self accept
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> waitForConnectionFor: timeout [
"Wait up until the given deadline for a connection to be established. Return true if it is established by the deadline, false if not."

^ self
waitForConnectionFor: timeout
ifClosed: [
ConnectionClosed signal: (socketHandle
ifNil: [ 'Socket destroyed while connecting' ]
ifNotNil: [
'Connection aborted or failed: ' , self socketErrorMessage ]) ]
ifTimedOut: [
ConnectionTimedOut signal:
'Failed to connect in ' , timeout asString , ' seconds' ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> waitForConnectionFor: timeout ifClosed: closedBlock ifTimedOut: timeoutBlock [
"Wait up until the given deadline for a connection to be established.
Evaluate closedBlock if the connection is closed locally,
or timeoutBlock if the deadline expires.
We should separately detect the case of a connection being refused here as well."

| startTime msecsDelta msecsElapsed status |
startTime := Time millisecondClockValue.
msecsDelta := (timeout * 1000) truncated.

[
status := self primSocketConnectionStatus: socketHandle.
status == WaitingForConnection and: [
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta ] ]
whileTrue: [ semaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ].

status == WaitingForConnection ifTrue: [ ^ timeoutBlock value ].
status == Connected ifFalse: [ ^ closedBlock value ]
]

{ #category : '*Ansible-Pharo-Pending-Patches' }
Socket >> waitForDataFor: timeout ifClosed: closedBlock ifTimedOut: timedOutBlock [
"Wait for the given nr of seconds for data to arrive.
If it does not, execute <timedOutBlock>. If the connection
is closed before any data arrives, execute <closedBlock>."

| startTime msecsDelta msecsElapsed |
startTime := Time millisecondClockValue.
msecsDelta := (timeout * 1000) truncated.
[ self dataAvailable ] whileFalse: [
self isConnected ifFalse: [ ^ closedBlock value ].
(msecsElapsed := Time millisecondsSince: startTime) < msecsDelta
ifFalse: [ ^ timedOutBlock value ].
readSemaphore waitTimeoutMilliseconds: msecsDelta - msecsElapsed ]
]

0 comments on commit 8efb901

Please sign in to comment.