parent
ddc57ba3ca
commit
3e73f4b4ed
|
@ -1,7 +1,6 @@
|
||||||
package org.bench4q.agent.api;
|
package org.bench4q.agent.api;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -21,7 +20,6 @@ import org.bench4q.share.models.agent.RunScenarioModel;
|
||||||
import org.bench4q.share.models.agent.RunScenarioResultModel;
|
import org.bench4q.share.models.agent.RunScenarioResultModel;
|
||||||
import org.bench4q.share.models.agent.StopTestModel;
|
import org.bench4q.share.models.agent.StopTestModel;
|
||||||
import org.bench4q.share.models.agent.TestBriefStatusModel;
|
import org.bench4q.share.models.agent.TestBriefStatusModel;
|
||||||
import org.bench4q.share.models.agent.UpdatePopulationModel;
|
|
||||||
import org.bench4q.share.models.agent.statistics.AgentBriefStatusModel;
|
import org.bench4q.share.models.agent.statistics.AgentBriefStatusModel;
|
||||||
import org.bench4q.share.models.agent.statistics.AgentBehaviorsBriefModel;
|
import org.bench4q.share.models.agent.statistics.AgentBehaviorsBriefModel;
|
||||||
import org.bench4q.share.models.agent.statistics.AgentPageBriefModel;
|
import org.bench4q.share.models.agent.statistics.AgentPageBriefModel;
|
||||||
|
@ -247,32 +245,20 @@ public class TestController {
|
||||||
if (scenarioContext == null) {
|
if (scenarioContext == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
scenarioContext.setEndDate(new Date(System.currentTimeMillis()));
|
|
||||||
System.out.println("when before stop, classId:"
|
System.out.println("when before stop, classId:"
|
||||||
+ scenarioContext.getExecutor().toString());
|
+ scenarioContext.getExecutor().toString());
|
||||||
scenarioContext.getExecutor().shutdown();
|
|
||||||
scenarioContext.getExecutor().shutdownNow();
|
|
||||||
System.out.println("when after stop, classId:"
|
System.out.println("when after stop, classId:"
|
||||||
+ scenarioContext.getExecutor().toString());
|
+ scenarioContext.getExecutor().toString());
|
||||||
scenarioContext.setFinished(true);
|
scenarioContext.stop();
|
||||||
clean(runId);
|
clean();
|
||||||
return new StopTestModel(true);
|
return new StopTestModel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/clean/{runId}", method = RequestMethod.GET)
|
@RequestMapping(value = "/clean", method = RequestMethod.GET)
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public CleanTestResultModel clean(@PathVariable UUID runId) {
|
public CleanTestResultModel clean() {
|
||||||
this.getScenarioEngine().getRunningTests().remove(runId);
|
|
||||||
System.gc();
|
System.gc();
|
||||||
return new CleanTestResultModel(true);
|
return new CleanTestResultModel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/updatePopulation/{runId}/{requiredLoad}", method = {
|
|
||||||
RequestMethod.POST, RequestMethod.GET })
|
|
||||||
@ResponseBody
|
|
||||||
public UpdatePopulationModel updatePopulation(@PathVariable UUID runId,
|
|
||||||
@PathVariable int requiredLoad) {
|
|
||||||
this.getScenarioEngine().updatePopulation(runId, requiredLoad);
|
|
||||||
return new UpdatePopulationModel(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,24 +11,41 @@ import java.util.TimerTask;
|
||||||
import org.bench4q.share.exception.Bench4QRunTimeException;
|
import org.bench4q.share.exception.Bench4QRunTimeException;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;
|
||||||
|
/*
|
||||||
|
* Segments in Schedule is sorted asc by Segment.start.time
|
||||||
|
*/
|
||||||
public class Schedule extends Observable {
|
public class Schedule extends Observable {
|
||||||
private static final int SCHEDULE_CYCLE = 3000;
|
private static final int SCHEDULE_CYCLE = 3000;
|
||||||
private final List<Segment> segments;
|
private final List<Segment> segments;
|
||||||
private final long beginTime;
|
private final long beginTime;
|
||||||
|
private volatile boolean reachEnd;
|
||||||
|
|
||||||
public List<Segment> getSegments() {
|
public List<Segment> getSegments() {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Schedule(List<Segment> segments){
|
public boolean hasReachEnd() {
|
||||||
|
return this.reachEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reachEnd() {
|
||||||
|
this.reachEnd = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Schedule(List<Segment> segments) {
|
||||||
if (segments == null || segments.size() == 0) {
|
if (segments == null || segments.size() == 0) {
|
||||||
throw new Bench4QRunTimeException("Can't init a schedul with zero segment");
|
throw new Bench4QRunTimeException(
|
||||||
|
"Can't init a schedul with zero segment");
|
||||||
}
|
}
|
||||||
this.segments = segments;
|
this.segments = segments;
|
||||||
this.beginTime = System.currentTimeMillis();
|
this.beginTime = System.currentTimeMillis();
|
||||||
|
this.reachEnd = false;
|
||||||
beginSchedul();
|
beginSchedul();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getScheduleRange() {
|
||||||
|
return this.getSegments().get(this.getSegments().size()).end.getTime()
|
||||||
|
- this.getSegments().get(0).start.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beginSchedul() {
|
private void beginSchedul() {
|
||||||
|
@ -39,18 +56,41 @@ public class Schedule extends Observable {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
Segment segment = getSegment(time);
|
Segment segment = getSegment(time);
|
||||||
if (segment == null) {
|
if (segment == null) {
|
||||||
//exceed the range of execute, should let the context stop the test
|
// exceed the range of execute, should let the context stop
|
||||||
|
// the test
|
||||||
|
notifyObservers(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
segment.loadFor(time - beginTime);
|
notifyObservers(segment.loadFor(time - beginTime));
|
||||||
}
|
}
|
||||||
}, 0, SCHEDULE_CYCLE);
|
}, 0, SCHEDULE_CYCLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
//get the segment by binary search
|
// get the segment by binary search
|
||||||
protected Segment getSegment(long time) {
|
public Segment getSegment(long time) {
|
||||||
|
if (this.getSegments() == null || this.getSegments().size() < 1
|
||||||
return null;
|
|| time < this.getSegments().get(0).start.getTime()) {
|
||||||
|
throw new Bench4QRunTimeException(
|
||||||
|
"can't getSegment when segments' size is LT 2");
|
||||||
|
}
|
||||||
|
if (time >= this.getSegments().get(this.getSegments().size() - 1).end
|
||||||
|
.getTime()) {
|
||||||
|
this.reachEnd();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int begin = 0, end = this.getSegments().size(), mid = (begin + end) / 2;
|
||||||
|
while (begin <= end) {
|
||||||
|
Segment midSegment = this.getSegments().get(mid);
|
||||||
|
if (midSegment.end.getTime() < time) {
|
||||||
|
begin = mid + 1;
|
||||||
|
} else if (midSegment.start.getTime() > time) {
|
||||||
|
end = mid - 1;
|
||||||
|
} else {
|
||||||
|
return midSegment;
|
||||||
|
}
|
||||||
|
mid = (begin + end) / 2;
|
||||||
|
}
|
||||||
|
throw new Bench4QRunTimeException("Should not come to this place");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Segment {
|
public static class Segment {
|
||||||
|
@ -62,16 +102,21 @@ public class Schedule extends Observable {
|
||||||
public Segment(Point startPoint, Point endPoint) {
|
public Segment(Point startPoint, Point endPoint) {
|
||||||
this.start = startPoint.copy();
|
this.start = startPoint.copy();
|
||||||
this.end = endPoint.copy();
|
this.end = endPoint.copy();
|
||||||
long timeDifference = this.end.getTime() / 1000 - this.start.getTime() / 1000;
|
long timeDifference = this.end.getTime() / 1000
|
||||||
if (timeDifference < 0 || this.start.getTime() < 0l || this.end.getTime() < 0l) {
|
- this.start.getTime() / 1000;
|
||||||
throw new Bench4QRunTimeException("The end time in TestScehdul cannot be less than start time");
|
if (timeDifference < 0 || this.start.getTime() < 0l
|
||||||
|
|| this.end.getTime() < 0l) {
|
||||||
|
throw new Bench4QRunTimeException(
|
||||||
|
"The end time in TestScehdul cannot be less than start time");
|
||||||
}
|
}
|
||||||
this.growthUnit = (float) (timeDifference == 0 ? 0 : (double)(this.end.getLoad() - this.start.getLoad())
|
this.growthUnit = (float) (timeDifference == 0 ? 0
|
||||||
/ (double)(timeDifference));
|
: (double) (this.end.getLoad() - this.start.getLoad())
|
||||||
|
/ (double) (timeDifference));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int loadFor(long timeFromBegin){
|
public int loadFor(long timeFromBegin) {
|
||||||
if (timeFromBegin < this.start.getTime() || timeFromBegin > this.end.getTime()) {
|
if (timeFromBegin < this.start.getTime()
|
||||||
|
|| timeFromBegin > this.end.getTime()) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
long diffFromStart = timeFromBegin - this.start.getTime();
|
long diffFromStart = timeFromBegin - this.start.getTime();
|
||||||
|
@ -80,7 +125,7 @@ public class Schedule extends Observable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Point {
|
public static class Point {
|
||||||
//This time is the relative value from begin
|
// This time is the relative value from begin
|
||||||
private final long time;
|
private final long time;
|
||||||
private final int load;
|
private final int load;
|
||||||
|
|
||||||
|
@ -93,6 +138,10 @@ public class Schedule extends Observable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Point(long time, int load) {
|
public Point(long time, int load) {
|
||||||
|
if (load < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Load can't be negtive number!");
|
||||||
|
}
|
||||||
this.time = time;
|
this.time = time;
|
||||||
this.load = load;
|
this.load = load;
|
||||||
}
|
}
|
||||||
|
@ -103,8 +152,8 @@ public class Schedule extends Observable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Schedule build(ScheduleModel scheduleModel) {
|
public static Schedule build(ScheduleModel scheduleModel) {
|
||||||
Schedule schedule = new Schedule(extractSegments(scheduleModel.getPoints()));
|
Schedule schedule = new Schedule(
|
||||||
|
extractSegments(scheduleModel.getPoints()));
|
||||||
return schedule;
|
return schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +168,7 @@ public class Schedule extends Observable {
|
||||||
public int compare(Point o1, Point o2) {
|
public int compare(Point o1, Point o2) {
|
||||||
return (int) (o1.getTime() - o2.getTime());
|
return (int) (o1.getTime() - o2.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
List<Segment> result = new LinkedList<Schedule.Segment>();
|
List<Segment> result = new LinkedList<Schedule.Segment>();
|
||||||
for (int i = 0; i < points.size() - 1; i++) {
|
for (int i = 0; i < points.size() - 1; i++) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.bench4q.agent.scenario.engine;
|
package org.bench4q.agent.scenario.engine;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Observable;
|
||||||
|
import java.util.Observer;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
@ -12,8 +14,9 @@ import org.bench4q.agent.datacollector.DataCollector;
|
||||||
import org.bench4q.agent.datacollector.impl.ScenarioResultCollector;
|
import org.bench4q.agent.datacollector.impl.ScenarioResultCollector;
|
||||||
import org.bench4q.agent.plugin.PluginManager;
|
import org.bench4q.agent.plugin.PluginManager;
|
||||||
import org.bench4q.agent.scenario.Scenario;
|
import org.bench4q.agent.scenario.Scenario;
|
||||||
|
import org.bench4q.agent.scenario.Schedule;
|
||||||
|
|
||||||
public class ScenarioContext {
|
public class ScenarioContext extends Observable implements Observer {
|
||||||
private static final long keepAliveTime = 10;
|
private static final long keepAliveTime = 10;
|
||||||
private UUID testId;
|
private UUID testId;
|
||||||
private Date startDate;
|
private Date startDate;
|
||||||
|
@ -96,6 +99,7 @@ public class ScenarioContext {
|
||||||
ScenarioContext scenarioContext = buildScenarioContextWithoutScenario(
|
ScenarioContext scenarioContext = buildScenarioContextWithoutScenario(
|
||||||
testId, poolSize, pluginManager);
|
testId, poolSize, pluginManager);
|
||||||
scenarioContext.setScenario(scenario);
|
scenarioContext.setScenario(scenario);
|
||||||
|
scenario.getSchedule().addObserver(scenarioContext);
|
||||||
return scenarioContext;
|
return scenarioContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +131,7 @@ public class ScenarioContext {
|
||||||
*
|
*
|
||||||
* @param requiredLoad
|
* @param requiredLoad
|
||||||
*/
|
*/
|
||||||
void updatePopulation(int requiredLoad) {
|
public void updatePopulation(int requiredLoad) {
|
||||||
this.getExecutor().setCorePoolSize(requiredLoad);
|
this.getExecutor().setCorePoolSize(requiredLoad);
|
||||||
this.getExecutor().setMaximumPoolSize(requiredLoad);
|
this.getExecutor().setMaximumPoolSize(requiredLoad);
|
||||||
}
|
}
|
||||||
|
@ -146,4 +150,20 @@ public class ScenarioContext {
|
||||||
addTask();
|
addTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Observable o, Object arg) {
|
||||||
|
Schedule schedule = (Schedule) o;
|
||||||
|
if (schedule.hasReachEnd()) {
|
||||||
|
stop();
|
||||||
|
}else {
|
||||||
|
this.updatePopulation((Integer) arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop(){
|
||||||
|
this.setFinished(true);
|
||||||
|
this.setEndDate(new Date());
|
||||||
|
this.getExecutor().shutdownNow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.bench4q.agent.scenario.engine;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.bench4q.agent.plugin.PluginManager;
|
import org.bench4q.agent.plugin.PluginManager;
|
||||||
import org.bench4q.agent.scenario.Scenario;
|
import org.bench4q.agent.scenario.Scenario;
|
||||||
|
@ -10,7 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class ScenarioEngine {
|
public class ScenarioEngine{
|
||||||
private Map<UUID, ScenarioContext> runningTests;
|
private Map<UUID, ScenarioContext> runningTests;
|
||||||
private Logger logger = Logger.getLogger(ScenarioEngine.class);
|
private Logger logger = Logger.getLogger(ScenarioEngine.class);
|
||||||
private PluginManager pluginManager;
|
private PluginManager pluginManager;
|
||||||
|
@ -71,9 +72,4 @@ public class ScenarioEngine {
|
||||||
scenarioContext.initTasks();
|
scenarioContext.initTasks();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updatePopulation(UUID testId, int requiredLoad) {
|
|
||||||
ScenarioContext context = this.getRunningTests().get(testId);
|
|
||||||
context.updatePopulation(requiredLoad);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.bench4q.agent.scenario.engine;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
|
||||||
|
public class Supervisor {
|
||||||
|
private Timer timer;
|
||||||
|
|
||||||
|
public Supervisor(ScenarioContext context){
|
||||||
|
timer = new Timer();
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package org.bench4q.agent.test;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -18,7 +19,9 @@ import org.bench4q.share.models.agent.RunScenarioModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.BatchModel;
|
import org.bench4q.share.models.agent.scriptrecord.BatchModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.BehaviorModel;
|
import org.bench4q.share.models.agent.scriptrecord.BehaviorModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.PageModel;
|
import org.bench4q.share.models.agent.scriptrecord.PageModel;
|
||||||
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.UsePluginModel;
|
import org.bench4q.share.models.agent.scriptrecord.UsePluginModel;
|
||||||
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
public abstract class TestBase {
|
public abstract class TestBase {
|
||||||
|
@ -105,6 +108,12 @@ public abstract class TestBase {
|
||||||
batch.getBehaviors().add(behavior);
|
batch.getBehaviors().add(behavior);
|
||||||
}
|
}
|
||||||
page.getBatches().add(batch);
|
page.getBatches().add(batch);
|
||||||
|
ScheduleModel scheduleModel = new ScheduleModel();
|
||||||
|
List<PointModel> points = new LinkedList<ScheduleModel.PointModel>();
|
||||||
|
points.add(new PointModel(0, 0));
|
||||||
|
points.add(new PointModel(10000, 10));
|
||||||
|
scheduleModel.setPoints(points);
|
||||||
|
runScenarioModel.setScheduleModel(scheduleModel);
|
||||||
runScenarioModel.getPages().add(page);
|
runScenarioModel.getPages().add(page);
|
||||||
return runScenarioModel;
|
return runScenarioModel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class Test_ScenarioEngine extends TestBase {
|
||||||
new ArrayList<ParameterModel>()))),
|
new ArrayList<ParameterModel>()))),
|
||||||
100, this.pluginManager);
|
100, this.pluginManager);
|
||||||
this.getScenarioEngine().getRunningTests().put(testId, scenarioContext);
|
this.getScenarioEngine().getRunningTests().put(testId, scenarioContext);
|
||||||
this.getScenarioEngine().updatePopulation(testId, 20);
|
scenarioContext.updatePopulation(20);
|
||||||
assertEquals(20, scenarioContext.getExecutor().getMaximumPoolSize());
|
assertEquals(20, scenarioContext.getExecutor().getMaximumPoolSize());
|
||||||
System.out.println(scenarioContext.getExecutor().getActiveCount());
|
System.out.println(scenarioContext.getExecutor().getActiveCount());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ package org.bench4q.agent.test.scenario.engine;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import org.bench4q.agent.scenario.Schedule;
|
import org.bench4q.agent.scenario.Schedule;
|
||||||
|
import org.bench4q.agent.scenario.Schedule.Segment;
|
||||||
import org.bench4q.share.exception.Bench4QRunTimeException;
|
import org.bench4q.share.exception.Bench4QRunTimeException;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
|
||||||
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;
|
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;
|
||||||
|
@ -72,4 +74,14 @@ public class Test_Shedule {
|
||||||
int load = schedule.getSegments().get(0).loadFor(500 * 1000);
|
int load = schedule.getSegments().get(0).loadFor(500 * 1000);
|
||||||
assertEquals(75, load);
|
assertEquals(75, load);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_getSegment(){
|
||||||
|
ScheduleModel model = new ScheduleModel();
|
||||||
|
model.getPoints().add(new PointModel(1000000, 100));
|
||||||
|
model.getPoints().add(new PointModel(0, 50));
|
||||||
|
Schedule schedule = Schedule.build(model);
|
||||||
|
Segment segment = schedule.getSegment(500 * 1000);
|
||||||
|
assertNotNull(segment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue