Skip to content

Commit

Permalink
Merge pull request #209 from 3pillarlabs/develop
Browse files Browse the repository at this point in the history
Merge PR #207, PR #208, and PR #210 to master
  • Loading branch information
sayantam committed Nov 15, 2020
2 parents 684b911 + 3052ad7 commit 7df34ee
Show file tree
Hide file tree
Showing 24 changed files with 835 additions and 360 deletions.
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3.2'
services:
web:
image: "hailstorm3/hailstorm-web-client:1.4.5"
image: "hailstorm3/hailstorm-web-client:1.5.6"
ports:
- "8080:80"
networks:
Expand All @@ -22,7 +22,7 @@ services:
- "start.sh"

hailstorm-api:
image: "hailstorm3/hailstorm-api:1.0.14"
image: "hailstorm3/hailstorm-api:1.0.15"
ports:
- "4567:8080"
environment:
Expand Down
12 changes: 12 additions & 0 deletions docs/_posts/2020-11-12-update-aws-agent-threads.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
layout: post
title: "Update Number of Threads per AWS Instance"
date: 2020-05-17 19:30:00 +0530
categories: jekyll update
---

The latest release of **Hailstorm** added the following feature:

- It is now possible to update the number of threads per agent (EC2 instance) in an AWS cluster.
Earlier, the entire cluster needed to be disabled, and a new cluster needed to be created. This
features saves effort and time when cluster properties need to be changed during a load test.
2 changes: 1 addition & 1 deletion hailstorm-api/app/api/clusters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
request.body.rewind
# @type [Hash]
data = JSON.parse(request.body.read)
data.each_pair { |key, value| matched_cluster_cfg.send("#{key}=", value) }
data.each_pair { |key, value| matched_cluster_cfg.send("#{key.underscore}=", value) }
project_config.update!(stringified_config: deep_encode(hailstorm_config))

JSON.dump(
Expand Down
2 changes: 1 addition & 1 deletion hailstorm-api/app/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
# Version
module Hailstorm
module Api
VERSION = '1.0.14'
VERSION = '1.0.15'
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Feature: Generate load from AWS
When I wait for load generation to stop
Then 2 tests should exist

@smoke
@end-to-end
Scenario: Start test for 30 threads
When I reconfigure the project
Expand All @@ -70,6 +71,7 @@ Feature: Generate load from AWS
Then 1 test should be running
And 2 load agents should exist

@smoke
@end-to-end
Scenario: Stop the test with 30 threads
When I wait for load generation to stop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,26 @@ When("I configure JMeter with following properties", function(dataTable: {
});

When("configure following amazon clusters", function(dataTable: {
hashes: () => { region: string; maxThreadsPerAgent: number }[];
hashes: () => { region: string; maxThreadsPerAgent: string | undefined }[];
}) {
if (amazonConfig.choose()) {
const clusters = dataTable.hashes();
for (const cluster of clusters) {
amazonConfig.createCluster(cluster);
const clusters = dataTable.hashes();
for (const cluster of clusters) {
const attrs: {
region: string;
maxThreadsPerAgent: number | undefined
} = {
region: cluster.region,
maxThreadsPerAgent: undefined
};

if (cluster.maxThreadsPerAgent) {
attrs.maxThreadsPerAgent = parseInt(cluster.maxThreadsPerAgent);
}

if (amazonConfig.choose()) {
amazonConfig.createCluster(attrs);
} else if (attrs.maxThreadsPerAgent) {
amazonConfig.updateCluster({maxThreadsPerAgent: attrs.maxThreadsPerAgent});
}
}

Expand Down
17 changes: 11 additions & 6 deletions hailstorm-web-client/e2e/features/support/pages/AmazonConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ export class AmazonConfig extends ClusterConfig {
get regionOptions() { return $$('//a[@role="AWSRegionOption"]'); }
get advancedMode() { return $('=Advanced Mode'); }
get maxThreadsPerInstance() { return $('//input[@name="maxThreadsByInstance"]'); }
get updateButton() { return $('//button[@role="Update Cluster"]'); }

choose() {
return this.chooseCluster(this.awsLink);
}

createCluster({ region, maxThreadsPerAgent }: { region: string; maxThreadsPerAgent: number; }) {
createCluster({ region, maxThreadsPerAgent }: { region: string; maxThreadsPerAgent: number | undefined; }) {
browser.waitUntil(() => this.editRegion.isDisplayed(), 10000);
const awsCredentials: { accessKey: string; secretKey: string; } = yaml.parse(
fs.readFileSync(path.resolve("data/keys.yml"), "utf8")
Expand All @@ -27,14 +28,11 @@ export class AmazonConfig extends ClusterConfig {
browser.pause(250);
this.secretKey.setValue(awsCredentials.secretKey);
browser.pause(250);
this.selectRegion(region);
if (maxThreadsPerAgent) {
this.advancedMode.click();
browser.waitUntil(() => this.maxThreadsPerInstance.isExisting());
this.maxThreadsPerInstance.setValue(maxThreadsPerAgent);
browser.pause(250);
this.updateCluster({maxThreadsPerAgent});
}

this.selectRegion(region);
const submitBtn = $('button*=Save');
submitBtn.waitForEnabled(15000);
submitBtn.click();
Expand All @@ -51,4 +49,11 @@ export class AmazonConfig extends ClusterConfig {
levelTwo.click();
browser.waitUntil(() => $(`//input[@value="${levelTwoRegion}"]`).isExisting(), 10000);
}

updateCluster({maxThreadsPerAgent}: {maxThreadsPerAgent: number}) {
this.maxThreadsPerInstance.setValue(maxThreadsPerAgent.toString());
browser.pause(250);
browser.waitUntil(() => this.updateButton.isEnabled(), 500);
this.updateButton.click();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,8 @@ export class ProjectWorkspace {
}

abortAfter({ seconds }: { seconds: number; }) {
const abortButton = this.abortButton;
setTimeout(() => {
abortButton.click();
}, seconds * 1000);
browser.pause(seconds * 1000);
this.abortButton.click();
}

projectIdFromUrl(): string {
Expand Down
7 changes: 7 additions & 0 deletions hailstorm-web-client/e2e/wdio.aws.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const devConf = require('./wdio.conf');
devConf.config.baseUrl = 'http://localhost:8080';
devConf.config.specs = ['./features/amazon_cloud_load_generation.feature']
devConf.config.cucumberOpts.failFast = true;
devConf.config.logLevel = 'info';
// devConf.config.cucumberOpts.tagExpression = '@focus';
exports.config = devConf.config;
2 changes: 1 addition & 1 deletion hailstorm-web-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hailstorm-web-client",
"version": "1.4.5",
"version": "1.5.6",
"private": true,
"dependencies": {
"date-fns": "^2.6.0",
Expand Down
145 changes: 145 additions & 0 deletions hailstorm-web-client/src/ClusterConfiguration/AWSForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { AmazonCluster, Cluster, Project } from '../domain';
import { AWSForm } from './AWSForm';
import { AWSInstanceChoiceOption, AWSRegionList } from './domain';
import { AWSEC2PricingService } from '../services/AWSEC2PricingService';
import { fireEvent, render, wait } from '@testing-library/react';
import { AWSRegionService } from '../services/AWSRegionService';
import { mount } from 'enzyme';
import { ClusterService } from '../services/ClusterService';

describe('<AWSForm />', () => {
const activeProject: Project = {
id: 1,
code: 'a',
title: 'A',
autoStop: true,
running: false,
jmeter: {
files: [
{ id: 10, name: 'a.jmx', properties: new Map([["foo", "10"]]) },
{ id: 11, name: 'a.csv', dataFile: true }
]
},
clusters: []
};

const dispatch = jest.fn();

function createComponent() {
return (
<AWSForm
{...{activeProject, dispatch}}
/>
)
}

let fetchPricing: Promise<AWSInstanceChoiceOption[]>;
let fetchRegions: Promise<AWSRegionList>;

beforeEach(() => {
fetchPricing = Promise.resolve([
new AWSInstanceChoiceOption({
instanceType: 'm3a.small', numInstances: 1, maxThreadsByInstance: 500, hourlyCostByInstance: 0.092
})
]);

jest.spyOn(AWSEC2PricingService.prototype, 'list').mockReturnValue(fetchPricing);
});

beforeEach(() => {
fetchRegions = Promise.resolve<AWSRegionList>({
regions: [
{
code: 'North America',
title: 'North America',
regions: [
{ code: 'us-east-1', title: 'Ohio, US East' }
]
}
],

defaultRegion: { code: 'us-east-1', title: 'Ohio, US East' }
});

jest.spyOn(AWSRegionService.prototype, 'list').mockReturnValue(fetchRegions);
});

it('should show form fields', async () => {
const utils = render(createComponent());
await fetchPricing;
await utils.findByTestId('AWS Access Key');
await utils.findByTestId('AWS Secret Key');
await utils.findByTestId('VPC Subnet');
await utils.findByTestId('Max. Users / Instance');
});


it('should show control for instance specifications', async () => {
const component = mount(createComponent());
await fetchRegions;
await fetchPricing;
component.update();
expect(component).toContainExactlyOneMatchingElement('AWSInstanceChoice');
});

it('should show estimated cost based on defaults', async () => {
const {findByText} = render(createComponent());
await fetchRegions;
await fetchPricing;
const message = await findByText(/Cluster Cost/i);
expect(message).toBeDefined();
});

it('should show control for AWS region', () => {
const component = mount(createComponent());
expect(component).toContainExactlyOneMatchingElement('AWSRegionChoice');
});


it('should validate cluster inputs', async () => {
const {findByText, findByTestId, findAllByText} = render(createComponent());
await fetchRegions;
await fetchPricing;
await findByText(/Cluster Cost/i);
const form = await findByTestId('AWSForm');
fireEvent.submit(form);
expect(dispatch).not.toBeCalled();
const messages = await findAllByText(/blank/i);
expect(messages.length).toEqual(2);
});

it('should save the cluster', async () => {
const savedCluster: AmazonCluster = {
id: 23,
code: 'singing-penguin-23',
title: '',
type: 'AWS',
accessKey: 'A',
secretKey: 'S',
region: 'us-east-1',
instanceType: 'm3a.small',
maxThreadsByInstance: 500
};

const createdCluster = Promise.resolve<Cluster>(savedCluster);
const createSpy = jest.spyOn(ClusterService.prototype, 'create').mockReturnValue(createdCluster);
const {findByTestId, findByText} = render(createComponent());
await fetchRegions;
await fetchPricing;
await findByText(/Cluster Cost/i);
const accessKey = await findByTestId('AWS Access Key');
const secretKey = await findByTestId('AWS Secret Key');
fireEvent.change(accessKey, {target: {value: savedCluster.accessKey}});
fireEvent.change(secretKey, {target: {value: savedCluster.secretKey}});
const save = await findByText(/save/i);
fireEvent.click(save);
await createdCluster;
await wait();

expect(createSpy).toBeCalled();
expect(dispatch).toBeCalled();
const action = dispatch.mock.calls[0][0] as {payload: Cluster};
expect(action.payload).toEqual(savedCluster);
});
});
14 changes: 3 additions & 11 deletions hailstorm-web-client/src/ClusterConfiguration/AWSForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Project } from '../domain';
import { RemoveClusterAction, SaveClusterAction } from './actions';
import { RemoveClusterAction, CreateClusterAction } from './actions';
import { Formik, Field, Form, FormikActions, ErrorMessage } from 'formik';
import { AWSInstanceChoice } from './AWSInstanceChoice';
import { AWSInstanceChoiceOption, AWSRegionList } from './domain';
Expand Down Expand Up @@ -51,7 +51,7 @@ export function AWSForm({ dispatch, activeProject }: {
instanceType: selectedInstanceType!.instanceType,
maxThreadsByInstance: selectedInstanceType!.maxThreadsByInstance
})
.then((createdCluster) => dispatch(new SaveClusterAction(createdCluster)))
.then((createdCluster) => dispatch(new CreateClusterAction(createdCluster)))
.catch((reason) => console.error(reason))
.finally(() => actions.setSubmitting(false));
};
Expand Down Expand Up @@ -121,19 +121,11 @@ export function AWSForm({ dispatch, activeProject }: {
<AWSInstanceChoice
onChange={handleAWSInstanceChange}
regionCode={awsRegion}
{...{ fetchPricing, setHourlyCostByCluster }}
{...{ fetchPricing, setHourlyCostByCluster, hourlyCostByCluster }}
disabled={isSubmitting}
/>
</div>
</div>
<div className="field">
{hourlyCostByCluster ? (
<div className="message is-info">
<div className="message-body">
<strong>Estimated Hourly Cost for the Cluster: ${hourlyCostByCluster.toFixed(4)}</strong>
</div>
</div>) : null}
</div>
</>) : null}
</div>
<ClusterFormFooter {...{dispatch}} disabled={isSubmitting || !isValid} />
Expand Down
Loading

0 comments on commit 7df34ee

Please sign in to comment.