diff --git a/source/Stargate-API-Skeleton-Tests/ConcurrentConnectionsTestApplication.class.st b/source/Stargate-API-Skeleton-Tests/ConcurrentConnectionsTestApplication.class.st new file mode 100644 index 0000000..1c1e8a3 --- /dev/null +++ b/source/Stargate-API-Skeleton-Tests/ConcurrentConnectionsTestApplication.class.st @@ -0,0 +1,40 @@ +" +I'm a testing application used to check the concurrent connections threshold configuration parameter behavior. +" +Class { + #name : 'ConcurrentConnectionsTestApplication', + #superclass : 'StargateApplication', + #category : 'Stargate-API-Skeleton-Tests', + #package : 'Stargate-API-Skeleton-Tests' +} + +{ #category : 'accessing' } +ConcurrentConnectionsTestApplication class >> commandName [ + + ^ 'concurrent-connections-test' +] + +{ #category : 'accessing' } +ConcurrentConnectionsTestApplication class >> description [ + + ^'A Test API making the request processing to wait a certain amount of time' +] + +{ #category : 'initialization' } +ConcurrentConnectionsTestApplication class >> initialize [ + + + self initializeVersion +] + +{ #category : 'private' } +ConcurrentConnectionsTestApplication class >> projectName [ + + ^ 'Stargate' +] + +{ #category : 'private - accessing' } +ConcurrentConnectionsTestApplication >> controllersToInstall [ + + ^ { DelayedRESTfulController new } +] diff --git a/source/Stargate-API-Skeleton-Tests/DelayedRESTfulController.class.st b/source/Stargate-API-Skeleton-Tests/DelayedRESTfulController.class.st new file mode 100644 index 0000000..135c58f --- /dev/null +++ b/source/Stargate-API-Skeleton-Tests/DelayedRESTfulController.class.st @@ -0,0 +1,46 @@ +" +I'm a example of a RESTful controller: +- only implementing GET operations +- not paginating collections +- not using hypermedia +" +Class { + #name : 'DelayedRESTfulController', + #superclass : 'SingleResourceRESTfulController', + #instVars : [ + 'requestHandler' + ], + #category : 'Stargate-API-Skeleton-Tests', + #package : 'Stargate-API-Skeleton-Tests' +} + +{ #category : 'routes' } +DelayedRESTfulController >> declareWaitRoute [ + + ^ RouteSpecification handling: #GET at: self endpoint evaluating: [ :httpRequest :requestContext | + 100 milliSeconds wait. + ZnResponse ok: ( ZnEntity text: 'OK' ) + ] +] + +{ #category : 'initialization' } +DelayedRESTfulController >> initialize [ + + super initialize. + requestHandler := RESTfulRequestHandlerBuilder new + handling: 'wait'; + createEntityTagHashingEncodedResource; + build +] + +{ #category : 'private' } +DelayedRESTfulController >> requestHandler [ + + ^ requestHandler +] + +{ #category : 'private' } +DelayedRESTfulController >> typeIdConstraint [ + + ^ IsObject +] diff --git a/source/Stargate-API-Skeleton-Tests/StargateApplicationStackTraceDumperTest.class.st b/source/Stargate-API-Skeleton-Tests/StargateApplicationStackTraceDumperTest.class.st index 519b83b..0474586 100644 --- a/source/Stargate-API-Skeleton-Tests/StargateApplicationStackTraceDumperTest.class.st +++ b/source/Stargate-API-Skeleton-Tests/StargateApplicationStackTraceDumperTest.class.st @@ -1,23 +1,24 @@ Class { - #name : #StargateApplicationStackTraceDumperTest, - #superclass : #TestCase, - #category : #'Stargate-API-Skeleton-Tests' + #name : 'StargateApplicationStackTraceDumperTest', + #superclass : 'TestCase', + #category : 'Stargate-API-Skeleton-Tests', + #package : 'Stargate-API-Skeleton-Tests' } -{ #category : #accessing } +{ #category : 'accessing' } StargateApplicationStackTraceDumperTest class >> defaultTimeLimit [ ^5 minute ] -{ #category : #running } +{ #category : 'running' } StargateApplicationStackTraceDumperTest >> setUp [ super setUp. StargateApplication logsDirectory ensureCreateDirectory ] -{ #category : #'tests - application' } +{ #category : 'tests - application' } StargateApplicationStackTraceDumperTest >> testStackTraceDumper [ | dumper result | diff --git a/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st b/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st index 3c556ee..800e7b0 100644 --- a/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st +++ b/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st @@ -2,23 +2,24 @@ A StargateAPISkeletonTest is a test class for testing the behavior of StargateAPISkeleton " Class { - #name : #StargateApplicationTest, - #superclass : #TestCase, + #name : 'StargateApplicationTest', + #superclass : 'TestCase', #instVars : [ 'application', 'port', 'baseUrl' ], - #category : #'Stargate-API-Skeleton-Tests' + #category : 'Stargate-API-Skeleton-Tests', + #package : 'Stargate-API-Skeleton-Tests' } -{ #category : #accessing } +{ #category : 'accessing' } StargateApplicationTest class >> defaultTimeLimit [ ^5 minute ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> assert: string isLineEndingInsensitiveEqualsTo: anotherString [ self @@ -26,13 +27,13 @@ StargateApplicationTest >> assert: string isLineEndingInsensitiveEqualsTo: anoth equals: ( anotherString withLineEndings: String lf ) ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> baseUrl [ ^ baseUrl ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> newClient [ ^ ZnClient new @@ -41,31 +42,53 @@ StargateApplicationTest >> newClient [ yourself ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> orderVersion1dot0dot0MediaType [ ^ 'application/vnd.stargate.order+json;version=1.0.0' asMediaType ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> petVersion1dot0dot0MediaType [ ^ 'application/vnd.stargate.pet+json;version=1.0.0' asMediaType ] -{ #category : #running } +{ #category : 'private' } +StargateApplicationTest >> run: count concurrentInstancesOf: block [ + + | results semaphore | + + semaphore := Semaphore new. + + results := OrderedCollection new. + + count timesRepeat: [ + LanguagePlatform current + fork: [ + results add: block value. + semaphore signal + ] + named: 'Concurrently running' + at: Processor activePriority + ]. + count timesRepeat: [ semaphore wait ]. + ^ results +] + +{ #category : 'running' } StargateApplicationTest >> runCase [ self shouldnt: [ super runCase ] raise: Exit ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> secret [ ^ 'secret' ] -{ #category : #running } +{ #category : 'running' } StargateApplicationTest >> setUp [ super setUp. @@ -73,7 +96,7 @@ StargateApplicationTest >> setUp [ StargateApplication logsDirectory ensureCreateDirectory ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> start: aLaunchpadApplication withAll: arguments [ String streamContents: [ :stream | @@ -93,7 +116,18 @@ StargateApplicationTest >> start: aLaunchpadApplication withAll: arguments [ ] ] -{ #category : #private } +{ #category : 'private' } +StargateApplicationTest >> startConcurrentConnectionsApp [ + + self start: ConcurrentConnectionsTestApplication withAll: { + '--stargate.public-url=http://localhost:<1p>' expandMacrosWith: port. + '--stargate.port=<1p>' expandMacrosWith: port. + '--stargate.operations-secret=<1s>' expandMacrosWith: self secret. + '--stargate.concurrent-connections-threshold=1' }. + baseUrl := application configuration stargate publicURL +] + +{ #category : 'private' } StargateApplicationTest >> startPetStore [ self start: PetStoreApplication withAll: { @@ -103,7 +137,7 @@ StargateApplicationTest >> startPetStore [ baseUrl := application configuration petStore stargate publicURL ] -{ #category : #private } +{ #category : 'private' } StargateApplicationTest >> startSouthAmericanCurrencies [ self start: SouthAmericanCurrenciesApplication withAll: { @@ -113,14 +147,15 @@ StargateApplicationTest >> startSouthAmericanCurrencies [ baseUrl := application configuration stargate publicURL ] -{ #category : #running } +{ #category : 'running' } StargateApplicationTest >> tearDown [ application ifNotNil: [ :theApplication | theApplication stop ]. + ZnOptions resetGlobalDefault. super tearDown ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testBadRequest [ self startPetStore. @@ -136,7 +171,34 @@ StargateApplicationTest >> testBadRequest [ withExceptionDo: [ :error | self assert: error response isBadRequest ] ] -{ #category : #'tests - api' } +{ #category : 'tests - options' } +StargateApplicationTest >> testConcurrentConnectionsThreshold [ + + | responses | + + self startConcurrentConnectionsApp. + + responses := self run: 2 concurrentInstancesOf: [ + self newClient + enforceHttpSuccess: false; + url: self baseUrl / 'wait' asUrl; + get; + response + ]. + + self assert: responses size equals: 2. + responses + detect: [ :response | response isSuccess ] + ifFound: [ :response | self assert: response contents equals: 'OK' ] + ifNone: [ self fail ]. + responses + detect: [ :response | response code = 503 ] + ifFound: [ :response | + self assert: response contents trimBoth equals: 'Too many concurrent connections' ] + ifNone: [ self fail ] +] + +{ #category : 'tests - api' } StargateApplicationTest >> testConflict [ self startPetStore. @@ -165,7 +227,7 @@ StargateApplicationTest >> testConflict [ ] ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testCreatePet [ | response json | @@ -193,7 +255,7 @@ StargateApplicationTest >> testCreatePet [ assert: json selfLocation equals: response location ] -{ #category : #'tests - application' } +{ #category : 'tests - application' } StargateApplicationTest >> testFileReferenceToDumpStackTrace [ | segments | @@ -206,7 +268,7 @@ StargateApplicationTest >> testFileReferenceToDumpStackTrace [ assert: ( segments last endsWith: '.dump' ) ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testGetCurrencies [ | currencies | @@ -217,7 +279,7 @@ StargateApplicationTest >> testGetCurrencies [ self assert: currencies size equals: 11 ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testGetPets [ | json | @@ -230,7 +292,7 @@ StargateApplicationTest >> testGetPets [ assert: json links size equals: 1 ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testMethodNotAllowed [ self startPetStore. @@ -249,7 +311,7 @@ StargateApplicationTest >> testMethodNotAllowed [ ] ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testNotAcceptable [ self startPetStore. @@ -267,7 +329,7 @@ StargateApplicationTest >> testNotAcceptable [ ] ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testNotFound [ self startPetStore. @@ -276,7 +338,7 @@ StargateApplicationTest >> testNotFound [ withExceptionDo: [ :error | self assert: error response isNotFound ] ] -{ #category : #'tests - application' } +{ #category : 'tests - application' } StargateApplicationTest >> testPrintHelpOn [ | help | @@ -286,7 +348,7 @@ StargateApplicationTest >> testPrintHelpOn [ self assert: help isLineEndingInsensitiveEqualsTo: ('NAME pet-store [<1s>] - A RESTful API for Pet stores SYNOPSYS - pet-store --pet-store.stargate.public-url=% --pet-store.stargate.port=% --pet-store.stargate.operations-secret=% + pet-store --pet-store.stargate.public-url=% --pet-store.stargate.port=% --pet-store.stargate.operations-secret=% [--pet-store.stargate.concurrent-connections-threshold=%] PARAMETERS --pet-store.stargate.public-url=% Public URL where the API is deployed. Used to create hypermedia links. @@ -294,6 +356,8 @@ PARAMETERS Listening port. --pet-store.stargate.operations-secret=% Secret key for checking JWT signatures. + --pet-store.stargate.concurrent-connections-threshold=% + Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed. Defaults to 32. ENVIRONMENT PET_STORE__STARGATE__PUBLIC_URL Public URL where the API is deployed. Used to create hypermedia links. @@ -301,10 +365,12 @@ ENVIRONMENT Listening port. PET_STORE__STARGATE__OPERATIONS_SECRET Secret key for checking JWT signatures. + PET_STORE__STARGATE__CONCURRENT_CONNECTIONS_THRESHOLD + Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed. Defaults to 32. ' expandMacrosWith: PetStoreApplication version) ] -{ #category : #'tests - application' } +{ #category : 'tests - application' } StargateApplicationTest >> testProjectName [ self @@ -312,7 +378,7 @@ StargateApplicationTest >> testProjectName [ assert: SouthAmericanCurrenciesApplication projectName equals: 'Stargate' ] -{ #category : #'tests - api' } +{ #category : 'tests - api' } StargateApplicationTest >> testUnsupportedMediaType [ self startPetStore. diff --git a/source/Stargate-API-Skeleton-Tests/package.st b/source/Stargate-API-Skeleton-Tests/package.st index 659914a..5950d36 100644 --- a/source/Stargate-API-Skeleton-Tests/package.st +++ b/source/Stargate-API-Skeleton-Tests/package.st @@ -1 +1 @@ -Package { #name : #'Stargate-API-Skeleton-Tests' } +Package { #name : 'Stargate-API-Skeleton-Tests' } diff --git a/source/Stargate-API-Skeleton/StargateApplication.class.st b/source/Stargate-API-Skeleton/StargateApplication.class.st index cd73775..2faa02f 100644 --- a/source/Stargate-API-Skeleton/StargateApplication.class.st +++ b/source/Stargate-API-Skeleton/StargateApplication.class.st @@ -88,18 +88,28 @@ StargateApplication class >> stackTraceDumpExtension [ StargateApplication class >> stargateConfigurationParameters [ ^ Array - with: ( MandatoryConfigurationParameter named: 'Public URL' + with: ( MandatoryConfigurationParameter + named: 'Public URL' describedBy: 'Public URL where the API is deployed. Used to create hypermedia links' inside: self sectionsForStargateConfiguration convertingWith: #asUrl ) - with: ( MandatoryConfigurationParameter named: 'Port' + with: ( MandatoryConfigurationParameter + named: 'Port' describedBy: 'Listening port' inside: self sectionsForStargateConfiguration convertingWith: #asNumber ) - with: ( MandatoryConfigurationParameter named: 'Operations Secret' + with: ( MandatoryConfigurationParameter + named: 'Operations Secret' describedBy: 'Secret key for checking JWT signatures' inside: self sectionsForStargateConfiguration convertingWith: #asByteArray ) asSensitive + with: ( OptionalConfigurationParameter + named: 'Concurrent Connections Threshold' + describedBy: + 'Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed' + inside: self sectionsForStargateConfiguration + defaultingTo: 32 + convertingWith: #asNumber ) ] { #category : 'accessing' } @@ -111,10 +121,12 @@ StargateApplication class >> version [ { #category : 'private - accessing' } StargateApplication >> apiConfiguration [ - ^ Array with: #serverUrl -> self stargateConfiguration publicURL - with: #port -> self stargateConfiguration port - with: #debugMode -> self isDebugModeEnabled - with: #operations -> self operationsConfiguration + ^ { + #serverUrl -> self stargateConfiguration publicURL. + #port -> self stargateConfiguration port. + #maximumNumberOfConcurrentConnections -> self stargateConfiguration concurrentConnectionsThreshold. + #debugMode -> self isDebugModeEnabled. + #operations -> self operationsConfiguration } ] { #category : 'private - accessing' } @@ -181,6 +193,14 @@ StargateApplication >> basicStop [ super basicStop ] +{ #category : 'private - activation/deactivation' } +StargateApplication >> configureConcurrentConnectionsThreshold [ + + ZnOptions globalDefault + at: #maximumNumberOfConcurrentConnections + put: self stargateConfiguration concurrentConnectionsThreshold +] + { #category : 'private - activation/deactivation' } StargateApplication >> configureGlobalErrorHandlerIn: api [ @@ -233,7 +253,10 @@ StargateApplication >> installAndStart: api [ LogRecord emitInfo: 'Installing API' during: [ api install ]; - emitInfo: 'Starting API' during: [ api start ] + emitInfo: 'Starting API' during: [ + self configureConcurrentConnectionsThreshold. + api start + ] ] { #category : 'private - activation/deactivation' }