-
Notifications
You must be signed in to change notification settings - Fork 16
WIP Support HttpClient based scenarios #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
de4c564 to
ab4acd2
Compare
ab4acd2 to
b9028b5
Compare
| } | ||
|
|
||
| fun initHttpClient(userName: String, password: String) { | ||
| synchronized(lock) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure If It's needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
|
|
||
| loadTest.run() | ||
|
|
||
| TODO("assert") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can I retrieve action metrics?
b9028b5 to
05b4303
Compare
When JPT detects a regression and the regression is on a backend side, then it's handy to test only the backend side, to avoid the browser's noise. I had this problem twice and two persons asked me about the feature. As there was no dedicated API, I've been creating a custom scenario, and translate web driver to HttpClient inside of a custom action. This approach has two disadvantages: - It requires expert-level knowledge about JPT - Each VU needs to run a real browser, even if it's not used. It limits the The goal of this change is to expose API for lightweight HttpClient based scenarios.
05b4303 to
a518658
Compare
| import java.net.URI | ||
| import java.util.concurrent.Future | ||
|
|
||
| interface HttpClientScenario { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to expose only the new implementation of Scenario which uses HttpClient? Something similar to JiraCoreScenario. In that case, our clients can decide which implementation to use without changing the interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current API, the client can choose both scenario and browser. HttpClientScenario binds browser with a scenario, and the new API is needed:
Scenario <---> Webdriver based browser
HttpClientScenario <----> HttpClient (the dependency is hidden in implementation)
| ) | ||
|
|
||
| private fun getScenario(): Class<out Scenario> { | ||
| HttpClientScenarioAdapter.scenarioClass = httpClientScenario |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you are manually managing the state of Adapter. Is there a place to improve?
e.g. what if httpClientScenario is null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's never null. But it's a global! 😱 Global state mutations not only are hard to understand, not only are hostile to concurrency, but also do not work across JVMs. We need a test, which runs across JVMs. Ie. a shadow JAR is ran via java -jar ... rather than calling main directly from JUnit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dagguh You're right I have a wrong test that didn't catch this problem!
| import java.util.concurrent.Future | ||
| import java.util.logging.Level | ||
|
|
||
| internal class HttpClientWebDriver : RemoteWebDriver() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file could be deleted when we decide to expose only a new implementation of Scenario (which uses HttpClient internally).
Reductio ad absurdum:
Should we add JMH microbenchmark API just because someone considers some factors as noise? In other words, instead of recapturing the same metrics multiple times with different tools, just collect all of them and cross-reference them to filter out the noise in post-processing.. |
|
The only good reason for HTTP-based traffic is when customer's Jira is actually being used by non-frontend, e.g. bots or scripts: https://ecosystem.atlassian.net/browse/JPERF-126 |
|
@dagguh You're right. We don't cover REST APIs in our performance tests. Not all the APIs are used from the front end. |
| ) | ||
|
|
||
| private fun getScenario(): Class<out Scenario> { | ||
| HttpClientScenarioAdapter.scenarioClass = httpClientScenario |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's never null. But it's a global! 😱 Global state mutations not only are hard to understand, not only are hostile to concurrency, but also do not work across JVMs. We need a test, which runs across JVMs. Ie. a shadow JAR is ran via java -jar ... rather than calling main directly from JUnit.
| return httpClient | ||
| } | ||
|
|
||
| fun initHttpClient(userName: String, password: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After construction, the object should be fully functional. "init" and "set" methods introduce mutability, which introduce unnecessary concurrency and readability problems.
Create the client externally, pass it via the constructor.
| } | ||
|
|
||
| override fun <X : Any?> getScreenshotAs(outputType: OutputType<X>?): X { | ||
| return outputType!!.convertFromBase64Png("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RemoteWebDriver doesn't do null checks. We can also skip that by declaring the parameter as non-null.
| import java.util.concurrent.Future | ||
| import java.util.logging.Level | ||
|
|
||
| internal class HttpClientWebDriver : RemoteWebDriver() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP client pretending to be a WebDriver sounds like a good way to introduce a ton of mental overhead and bugs. We're promising abilities to take screenshots, take page source, access elements, etc. We fail most of these promises in runtime. This fake complexity spreads outwards, infecting the Browser and VirtualUserBehavior APIs.
Instead, we should generalize existing APIs. I didn't dig deep, but it seems the common structure is:
- actual traffic generator - WebDriver or HTTP client
** gotta be closeable
** injectable supplier -BrowserSPI for WD, aSupplier<CloseableHttpClient>for HTTP - diagnostics - screenshot/page source/URL for WD, HTTP request/response dump for HTTP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an internal debt, but you're right, let's try to generalize the APIs instead of trying to fit into the existing one.
02d7e27 to
d8d14fb
Compare
d8d14fb to
6699a05
Compare
| ): VirtualUserBehavior = Builder(this).load(load).build() | ||
|
|
||
| @Deprecated("TODO - can we provide more type safe builder?") | ||
| // TODO detect when the new scenario is used with parameters that are no longer supported in new Scenario |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we should keep the old builder and introduce a new one, without the non-supported parameters.
| import java.util.List; | ||
|
|
||
| /** | ||
| * IT doesn't need to be thread safe. Each VU will have own copy of the scenario and own Thread |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should mark it as @NotThreadSafe
We could mention it's thread-confined
| /** | ||
| * IT doesn't need to be thread safe. Each VU will have own copy of the scenario and own Thread | ||
| */ | ||
| public abstract class Scenario { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not an interface anymore, so we don't have the Kotlin default method problems, so we can write it in Kotlin
| } else if (scenarioPackage == "com.atlassian.performance.tools.jiraactions.api.scenario") { | ||
| LoadTest(options).run() | ||
| } else { | ||
| throw Exception("not implemented") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Printing the unknown package name would help
| } else { | ||
| LoadTest(options).run() | ||
| val scenarioPackage = options.behavior.scenario.packageName | ||
| if (scenarioPackage == "com.atlassian.performance.tools.virtualusers.lib.api") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when
| * Applies load on a Jira via page objects. Explores the instance to learn about data and choose pages to visit. | ||
| * Wanders preset Jira pages with different proportions of each page. Their order is random. | ||
| */ | ||
| internal class NewExploratoryVirtualUser( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new Scenario should be able to express old features. So we should be able to write a bridge from old to new Scenario and only consume the new one. No need to copy all of the internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy-paste is just an intermediate state. I didn't want to be focused on backwards compatibility while designing the new API. I'm going to refactor it later.
| limit: Int = Int.MAX_VALUE | ||
| ) : Action { | ||
| companion object { | ||
| private val limitCounter = AtomicInteger(Int.MAX_VALUE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't have a global limit counter before. Each VU had its own limit.
| driver.close() | ||
| } | ||
|
|
||
| override fun getActions(): List<Action> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a pattern for partially reusable/customizable scenarios. E.g. reuse this getActions, but with a different Browser.
I imagine, we could inject a Browser like this:
open class SimpleWebdriverScenario(
private val browser: Browser,
private val target: VirtualUserTarget,
private val meter: ActionMeter
) : Scenario(
target,
meter
) {
constructor(
target: VirtualUserTarget,
meter: ActionMeter
) : this(
HeadlessChromeBrowser(),
target,
meter
)
}
class CustomizedWebdriverScenario(
private val target: VirtualUserTarget,
private val meter: ActionMeter
) : SimpleWebdriverScenario(
FirefoxBrowser(),
target,
meter
)
Alternatively we could extract each method into a "util" and let the user stitch a new scenario class from these components.
WDYT @wyrzyk?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can support the injection of the browser, but it makes API focused on one load generator type. The new VUB builder will have to support Browser parameter which will be only usable in this kind of scenarios.
If we look at how the browser has been changed during the last years in our scenarios/tests, it was mainly bumping the version and one switch from chrome to chromium. We never needed the flexibility of decoupled load generator and scenario.
Beside generic scenarios, we provide in jira-actions and jira-software-actions scenario is part of the test code. We have a couple of different scenarios in our internal tests. For our internal tests, it's even easier to create an instance of a browser instead of passing it via the builder.
It also makes testing scenarios easier because the class provides all the capabilities to perform the test. All that needs to be provided is VUT and action meter.
If we really want to provide a way to reuse scenario with a different browser, then it doesn't have to be simple. It's not the main or common use case. We can put all the responsibility to provide the cross-browser scenario on the client's code. We could just provide passing properties to VU. It could look like:
class SimpleCrossBrowserScenario(
private val target: VirtualUserTarget,
private val meter: ActionMeter
) : Scenario(
target,
meter
) {
val driver = SystemPropertyDriverFactory().getDriver()
}
class SystemPropertyDriverFactory {
fun getDriver(): WebDriver {
return when (System.getProperty("browser")) {
"firefox" -> new Firefox ()
"chrome" -> new Chrome ()
}
}
}
- It removes the mental overhead of the browser from the basic usages of VUB.
- Creator of the scenario controls supported browsers/load generators (just by using a common webdriver API there's no guarantee the scenario will run with all the browsers and all the configurations).
- It's up to the creator of the scenario to provide API (properties needed to configure load generator).
- It can be used for different's kind of generators. For example, to provide for HTTP based generators on HttpClient and OkHttp. For DB based generators (different JDBC implementations) etc.
- Properties can also be used to pass different information like admin credentials. It doesn't have to be part of VU API. It can be a Scenario's API. It's hard to provide a generic way for authorisation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't mention VUB or VUT. Did you see my code snippet suggestion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see now. We always use 2 parameters constructor, so there's no VUB or VUT problem. This way it's easier to use different browsers, but still, we don't have evidence that the feature is needed.
It's also more prone to errors. It's hard to enforce inheritance only the specific way.
This will not work as expected:
class CustomizedWebdriverScenario(
private val browser: Browser,
private val target: VirtualUserTarget,
private val meter: ActionMeter
) : SimpleWebdriverScenario(
browser,
target,
meter
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it could be a WTF moment.
It's also more prone to errors.
More prone to errors in comparison to SystemPropertyDriverFactory?
How would we pass the properties to the VU JVMs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more prone to errors because it adds WTF moment to the basic scenario implementation journey instead of adding WTF moment to an imaginary scenario without a real need.
More prone to errors in comparison to SystemPropertyDriverFactory?
How would we pass the properties to the VU JVMs?
SystemPropertyDriverFactory is just one example of implementation. We can pass params via VUB (command line parameters).
We can also use the properties object as a 3rd parameter to the scenario. It would be passed via VUB to the scenario and will allow scenarios to implement own API (for example. load generators support, credentials support).
| adminPassword = "admin" | ||
| ) | ||
|
|
||
| override fun before() { //todo don't use hardcoded credentials |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- todo don't use hardcoded credentials
| import com.atlassian.performance.tools.virtualusers.lib.api.Scenario | ||
| import java.util.concurrent.atomic.AtomicInteger | ||
|
|
||
| // TODO show that RTE can be disabled via setup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- TODO show that RTE can be disabled via setup
| ) : Scenario( | ||
| virtualUserTarget, meter | ||
| ) { | ||
| private val driver: CloseableRemoteWebDriver = HeadlessChromeBrowser().start() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constructors shouldn't do real work or heavy-lifting like starting processes.
I might be able to create an instance, but never reach before or getActions (due to an if or a throw).
| password = "admin" | ||
| ), | ||
| behavior = VirtualUserBehavior.Builder( | ||
| SimpleWebdriverScenario::class.java |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also have an example of a HTTP scenario
When JPT detects a regression and the regression is on a backend side, then
it's handy to test only the backend side, to avoid the browser's noise.
I had this problem twice and two persons asked me about the feature. As there was
no dedicated API, I've been creating a custom scenario, and translate web driver
to HttpClient inside of a custom action.
This approach has two disadvantages:
The goal of this change is to expose API for lightweight HttpClient based scenarios.