All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.hyperfoil.http.steps.HttpRequestStep Maven / Gradle / Ivy

There is a newer version: 0.27.1
Show newest version
package io.hyperfoil.http.steps;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.kohsuke.MetaInfServices;

import io.hyperfoil.api.BenchmarkExecutionException;
import io.hyperfoil.api.config.Benchmark;
import io.hyperfoil.api.config.BuilderBase;
import io.hyperfoil.core.util.Util;
import io.hyperfoil.http.config.Http;
import io.hyperfoil.api.config.InitFromParam;
import io.hyperfoil.api.config.Locator;
import io.hyperfoil.api.config.Name;
import io.hyperfoil.api.config.PhaseBuilder;
import io.hyperfoil.core.builders.SLA;
import io.hyperfoil.core.builders.SLABuilder;
import io.hyperfoil.api.config.ScenarioBuilder;
import io.hyperfoil.api.config.SequenceBuilder;
import io.hyperfoil.api.config.StepBuilder;
import io.hyperfoil.api.config.Visitor;
import io.hyperfoil.http.HttpRequestPool;
import io.hyperfoil.http.api.HttpDestinationTable;
import io.hyperfoil.http.api.HttpRequest;
import io.hyperfoil.api.processor.HttpRequestProcessorBuilder;
import io.hyperfoil.api.session.Access;
import io.hyperfoil.api.session.Action;
import io.hyperfoil.api.session.SequenceInstance;
import io.hyperfoil.api.statistics.Statistics;
import io.hyperfoil.core.generators.Pattern;
import io.hyperfoil.core.generators.StringGeneratorBuilder;
import io.hyperfoil.core.generators.StringGeneratorImplBuilder;
import io.hyperfoil.core.handlers.StoreProcessor;
import io.hyperfoil.http.config.HttpErgonomics;
import io.hyperfoil.http.config.HttpPluginBuilder;
import io.hyperfoil.http.config.HttpPluginConfig;
import io.hyperfoil.http.handlers.FilterHeaderHandler;
import io.hyperfoil.http.cookie.CookieAppender;
import io.hyperfoil.core.http.GzipInflatorProcessor;
import io.hyperfoil.http.HttpUtil;
import io.hyperfoil.http.UserAgentAppender;
import io.hyperfoil.core.session.SessionFactory;
import io.hyperfoil.core.steps.DelaySessionStartStep;
import io.hyperfoil.core.steps.PathMetricSelector;
import io.hyperfoil.core.steps.StatisticsStep;
import io.hyperfoil.core.util.BitSetResource;
import io.hyperfoil.core.util.DoubleIncrementBuilder;
import io.hyperfoil.core.util.Unique;
import io.netty.buffer.ByteBuf;
import io.hyperfoil.api.config.PairBuilder;
import io.hyperfoil.api.config.PartialBuilder;
import io.hyperfoil.api.connection.Connection;
import io.hyperfoil.api.config.BenchmarkDefinitionException;
import io.hyperfoil.http.api.HttpConnectionPool;
import io.hyperfoil.http.api.HttpRequestWriter;
import io.hyperfoil.http.api.HttpMethod;
import io.hyperfoil.api.config.Step;
import io.hyperfoil.api.session.Session;
import io.hyperfoil.core.builders.BaseStepBuilder;
import io.hyperfoil.api.session.ResourceUtilizer;
import io.hyperfoil.function.SerializableBiConsumer;
import io.hyperfoil.function.SerializableBiFunction;
import io.hyperfoil.function.SerializableFunction;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.util.AsciiString;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

public class HttpRequestStep extends StatisticsStep implements ResourceUtilizer, SLA.Provider {
   private static final Logger log = LoggerFactory.getLogger(HttpRequestStep.class);
   private static final boolean trace = log.isTraceEnabled();

   final SerializableFunction method;
   final SerializableFunction authority;
   final SerializableFunction pathGenerator;
   final SerializableBiFunction bodyGenerator;
   final SerializableBiConsumer[] headerAppenders;
   @Visitor.Ignore
   private final boolean injectHostHeader;
   final SerializableBiFunction metricSelector;
   final long timeout;
   final HttpResponseHandlersImpl handler;
   final SLA[] sla;

   public HttpRequestStep(int stepId,
                          SerializableFunction method,
                          SerializableFunction authority,
                          SerializableFunction pathGenerator,
                          SerializableBiFunction bodyGenerator,
                          SerializableBiConsumer[] headerAppenders,
                          boolean injectHostHeader,
                          SerializableBiFunction metricSelector,
                          long timeout, HttpResponseHandlersImpl handler, SLA[] sla) {
      super(stepId);
      this.method = method;
      this.authority = authority;
      this.pathGenerator = pathGenerator;
      this.bodyGenerator = bodyGenerator;
      this.headerAppenders = headerAppenders;
      this.injectHostHeader = injectHostHeader;
      this.metricSelector = metricSelector;
      this.timeout = timeout;
      this.handler = handler;
      this.sla = sla;
   }

   @Override
   public boolean invoke(Session session) {
      SequenceInstance sequence = session.currentSequence();
      HttpRequest request = HttpRequestPool.get(session).acquire();
      if (request == null) {
         log.warn("#{} Request pool too small; increase it to prevent blocking.", session.uniqueId());
         return false;
      }
      request.method = method.apply(session);
      HttpConnectionPool connectionPool;
      String path;
      String authority;
      try {
         authority = this.authority == null ? null : this.authority.apply(session);
         path = pathGenerator.apply(session);
         boolean isHttp;
         HttpDestinationTable destinations = HttpDestinationTable.get(session);
         if (authority == null && ((isHttp = path.startsWith(HttpUtil.HTTP_PREFIX)) || path.startsWith(HttpUtil.HTTPS_PREFIX))) {
            for (String hostPort : destinations.authorities()) {
               if (HttpUtil.authorityMatch(path, hostPort, isHttp)) {
                  authority = hostPort;
                  break;
               }
            }
            if (authority == null) {
               log.error("Cannot access {}: no base url configured", path);
               return true;
            }
            path = path.substring(prefixLength(isHttp) + authority.length());
         }
         String metric = destinations.hasSingleDestination() ?
               metricSelector.apply(null, path) : metricSelector.apply(authority, path);
         Statistics statistics = session.statistics(id(), metric);
         request.path = path;
         request.start(handler, sequence, statistics);

         connectionPool = destinations.getConnectionPool(authority);
         if (connectionPool == null) {
            session.fail(new BenchmarkExecutionException("There is no connection pool with authority '" + authority +
                  "', available pools are: " + Arrays.asList(destinations.authorities())));
            return false;
         }
         request.authority = authority == null ? connectionPool.clientPool().authority() : authority;
      } catch (Throwable t) {
         // If any error happens we still need to release the request
         // The request is either IDLE or RUNNING - we need to make it running, otherwise we could not release it
         if (!request.isRunning()) {
            request.start(sequence, null);
         }
         request.setCompleted();
         request.release();
         throw t;
      }
      if (!connectionPool.request(request, headerAppenders, injectHostHeader, bodyGenerator, false)) {
         request.setCompleted();
         request.release();
         // TODO: when the phase is finished, max duration is not set and the connection cannot be obtained
         // we'll be waiting here forever. Maybe there should be a (default) timeout to obtain the connection.
         connectionPool.registerWaitingSession(session);
         sequence.setBlockedTimestamp();
         request.statistics().incrementBlockedCount(request.startTimestampMillis());
         return false;
      }
      long blockedTime = sequence.getBlockedTime();
      if (blockedTime > 0) {
         request.statistics().incrementBlockedTime(request.startTimestampMillis(), blockedTime);
      }
      if (request.isCompleted()) {
         // When the request handlers call Session.stop() due to a failure it does not make sense to continue
         request.release();
         return true;
      }
      // Set up timeout only after successful request
      if (timeout > 0) {
         // TODO alloc!
         request.setTimeout(timeout, TimeUnit.MILLISECONDS);
      } else {
         Benchmark benchmark = session.phase().benchmark();
         HttpPluginConfig httpConfig = benchmark.plugin(HttpPluginConfig.class);
         Http http = authority == null ? httpConfig.defaultHttp() : httpConfig.http().get(authority);
         long timeout = http.requestTimeout();
         if (timeout > 0) {
            request.setTimeout(timeout, TimeUnit.MILLISECONDS);
         }
      }

      if (trace) {
         log.trace("#{} sent to {} request on {}", session.uniqueId(), path, request.connection());
      }
      request.statistics().incrementRequests(request.startTimestampMillis());
      return true;
   }

   private int prefixLength(boolean isHttp) {
      return isHttp ? HttpUtil.HTTP_PREFIX.length() : HttpUtil.HTTPS_PREFIX.length();
   }

   @Override
   public void reserve(Session session) {
      ResourceUtilizer.reserve(session, authority, pathGenerator, bodyGenerator);
      ResourceUtilizer.reserve(session, (Object[]) headerAppenders);
      handler.reserve(session);
   }

   @Override
   public SLA[] sla() {
      return sla;
   }

   /**
    * Issues a HTTP request and registers handlers for the response.
    */
   @MetaInfServices(StepBuilder.class)
   @Name("httpRequest")
   public static class Builder extends BaseStepBuilder {
      private int stepId = -1;
      private HttpMethodBuilder method;
      private StringGeneratorBuilder authority;
      private StringGeneratorBuilder path;
      private BodyGeneratorBuilder body;
      private final List>> headerAppenders = new ArrayList<>();
      private boolean injectHostHeader = true;
      private SerializableBiFunction metricSelector;
      private long timeout = Long.MIN_VALUE;
      private final HttpResponseHandlersImpl.Builder handler = new HttpResponseHandlersImpl.Builder(this);
      private boolean sync = true;
      private SLABuilder.ListBuilder sla = null;
      private CompensationBuilder compensation;
      private CompressionBuilder compression = new CompressionBuilder(this);

      /**
       * HTTP method used for the request.
       *
       * @param method HTTP method.
       * @return Self.
       */
      public Builder method(HttpMethod method) {
         return method(() -> new HttpMethodBuilder.Provided(method));
      }

      public Builder method(HttpMethodBuilder method) {
         this.method = method;
         return this;
      }

      /**
       * Issue HTTP GET request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder GET(String path) {
         return method(HttpMethod.GET).path().pattern(path).end();
      }

      /**
       * Issue HTTP GET request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder GET() {
         return method(HttpMethod.GET).path();
      }

      /**
       * Issue HTTP HEAD request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder HEAD(String path) {
         return method(HttpMethod.HEAD).path().pattern(path).end();
      }

      /**
       * Issue HTTP HEAD request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder HEAD() {
         return method(HttpMethod.HEAD).path();
      }

      /**
       * Issue HTTP POST request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder POST(String path) {
         return method(HttpMethod.POST).path().pattern(path).end();
      }

      /**
       * Issue HTTP POST request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder POST() {
         return method(HttpMethod.POST).path();
      }

      /**
       * Issue HTTP PUT request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder PUT(String path) {
         return method(HttpMethod.PUT).path().pattern(path).end();
      }

      /**
       * Issue HTTP PUT request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder PUT() {
         return method(HttpMethod.PUT).path();
      }

      /**
       * Issue HTTP DELETE request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder DELETE(String path) {
         return method(HttpMethod.DELETE).path().pattern(path).end();
      }

      /**
       * Issue HTTP DELETE request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder DELETE() {
         return method(HttpMethod.DELETE).path();
      }

      /**
       * Issue HTTP OPTIONS request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder OPTIONS(String path) {
         return method(HttpMethod.OPTIONS).path().pattern(path).end();
      }

      /**
       * Issue HTTP OPTIONS request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder OPTIONS() {
         return method(HttpMethod.OPTIONS).path();
      }

      /**
       * Issue HTTP PATCH request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder PATCH(String path) {
         return method(HttpMethod.PATCH).path().pattern(path).end();
      }

      /**
       * Issue HTTP PATCH request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder PATCH() {
         return method(HttpMethod.PATCH).path();
      }

      /**
       * Issue HTTP TRACE request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder TRACE(String path) {
         return method(HttpMethod.TRACE).path().pattern(path).end();
      }

      /**
       * Issue HTTP TRACE request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder TRACE() {
         return method(HttpMethod.TRACE).path();
      }

      /**
       * Issue HTTP CONNECT request to given path.
       *
       * @param path HTTP path, a pattern replacing ${sessionvar} with variable contents.
       * @return Self.
       */
      public Builder CONNECT(String path) {
         return method(HttpMethod.CONNECT).path().pattern(path).end();
      }

      /**
       * Issue HTTP CONNECT request to given path.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder CONNECT() {
         return method(HttpMethod.CONNECT).path();
      }

      /**
       * HTTP authority (host:port) this request should target. Must match one of the entries in http section.
       *
       * @param authority Host:port.
       * @return Self.
       */
      public Builder authority(String authority) {
         return authority(() -> new Pattern(authority, false));
      }

      public Builder authority(SerializableFunction authorityGenerator) {
         return authority(() -> authorityGenerator);
      }

      /**
       * HTTP authority (host:port) this request should target. Must match one of the entries in http section.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder authority() {
         StringGeneratorImplBuilder builder = new StringGeneratorImplBuilder<>(this, false);
         authority(builder);
         return builder;
      }

      public Builder authority(StringGeneratorBuilder authority) {
         this.authority = authority;
         return this;
      }

      StringGeneratorBuilder getAuthority() {
         return authority;
      }

      public Builder path(String path) {
         return path(() -> new Pattern(path, false));
      }

      /**
       * HTTP path (absolute or relative), including query and fragment.
       *
       * @return Builder.
       */
      public StringGeneratorImplBuilder path() {
         StringGeneratorImplBuilder builder = new StringGeneratorImplBuilder<>(this, false);
         path(builder);
         return builder;
      }

      public Builder path(SerializableFunction pathGenerator) {
         return path(() -> pathGenerator);
      }

      public Builder path(StringGeneratorBuilder builder) {
         if (this.path != null) {
            throw new BenchmarkDefinitionException("Path generator already set.");
         }
         this.path = builder;
         return this;
      }

      /**
       * HTTP request body (possibly a pattern).
       *
       * @param string Request body.
       * @return Self.
       */
      public Builder body(String string) {
         return body().pattern(string).endBody();
      }

      /**
       * HTTP request body.
       *
       * @return Builder.
       */
      public BodyBuilder body() {
         return new BodyBuilder(this);
      }

      public Builder body(SerializableBiFunction bodyGenerator) {
         return body(() -> bodyGenerator);
      }

      public Builder body(BodyGeneratorBuilder bodyGenerator) {
         if (this.body != null) {
            throw new BenchmarkDefinitionException("Body generator already set.");
         }
         this.body = bodyGenerator;
         return this;
      }

      BodyGeneratorBuilder bodyBuilder() {
         return body;
      }

      public Builder headerAppender(SerializableBiConsumer headerAppender) {
         headerAppenders.add(() -> headerAppender);
         return this;
      }

      public Builder headerAppenders(Collection>> appenders) {
         headerAppenders.addAll(appenders);
         return this;
      }

      List>> headerAppenders() {
         return Collections.unmodifiableList(headerAppenders);
      }

      /**
       * HTTP headers sent in the request.
       *
       * @return Builder.
       */
      public HeadersBuilder headers() {
         return new HeadersBuilder(this);
      }

      public Builder timeout(long timeout, TimeUnit timeUnit) {
         if (timeout <= 0) {
            throw new BenchmarkDefinitionException("Timeout must be positive!");
         } else if (this.timeout != Long.MIN_VALUE) {
            throw new BenchmarkDefinitionException("Timeout already set!");
         }
         this.timeout = timeUnit.toMillis(timeout);
         return this;
      }

      /**
       * Request timeout - after this time the request will be marked as failed and connection will be closed.
       * 

* Defaults to value set globally in http section. * * @param timeout Timeout. * @return Self. */ public Builder timeout(String timeout) { return timeout(Util.parseToMillis(timeout), TimeUnit.MILLISECONDS); } /** * Requests statistics will use this metric name. * * @param name Metric name. * @return Self. */ public Builder metric(String name) { return metric(new ProvidedMetricSelector(name)); } public Builder metric(SerializableBiFunction selector) { this.metricSelector = selector; return this; } /** * Allows categorizing request statistics into metrics based on the request path. * * @return Builder. */ public PathMetricSelector metric() { PathMetricSelector selector = new PathMetricSelector(); this.metricSelector = selector; return selector; } /** * HTTP response handlers. * * @return Builder. */ public HttpResponseHandlersImpl.Builder handler() { return handler; } /** * This request is synchronous; execution of the sequence does not continue until the full response * is received. If this step is executed from multiple parallel instances of this sequence the progress * of all sequences is blocked until there is a request in flight without response. *

* Default is true. * * @param sync Synchronous? * @return Self. */ public Builder sync(boolean sync) { this.sync = sync; return this; } /** * List of SLAs the requests are subject to. * * @return Builder. */ public SLABuilder.ListBuilder sla() { if (sla == null) { sla = new SLABuilder.ListBuilder<>(this); } return sla; } /** * Configures additional metric compensated for coordinated omission. * * @return Builder. */ public CompensationBuilder compensation() { return this.compensation = new CompensationBuilder(this); } /** * Request server to respond with compressed entity using specified content encoding. * * @param encoding Encoding. Currently supports only gzip. * @return Self. */ public Builder compression(String encoding) { compression().encoding(encoding); return this; } /** * Configure response compression. * * @return Builder. */ public CompressionBuilder compression() { return compression; } @Override public int id() { assert stepId >= 0; return stepId; } @Override public void prepareBuild() { stepId = StatisticsStep.nextId(); Locator locator = Locator.current(); HttpErgonomics ergonomics = locator.benchmark().plugin(HttpPluginBuilder.class).ergonomics(); if (ergonomics.repeatCookies()) { headerAppender(new CookieAppender()); } if (ergonomics.userAgentFromSession()) { headerAppender(new UserAgentAppender()); } if (sync) { // We need to perform this in prepareBuild() because the completion handlers must not be modified // in the build() method. Alternative would be caching the key and returning the wrapping steps // in the list. BeforeSyncRequestStep beforeSyncRequestStep = new BeforeSyncRequestStep(); locator.sequence().insertBefore(locator).step(beforeSyncRequestStep); handler.onCompletion(new ReleaseSyncAction(beforeSyncRequestStep)); locator.sequence().insertAfter(locator).step(new AfterSyncRequestStep(beforeSyncRequestStep)); } if (metricSelector == null) { String sequenceName = Locator.current().sequence().name(); metricSelector = new ProvidedMetricSelector(sequenceName); } if (compensation != null) { compensation.prepareBuild(); } compression.prepareBuild(); handler.prepareBuild(); } @Override public List build() { String guessedAuthority = null; boolean checkAuthority = true; SerializableFunction authority = this.authority != null ? this.authority.build() : null; SerializableFunction pathGenerator = this.path != null ? this.path.build() : null; try { guessedAuthority = authority == null ? null : authority.apply(null); } catch (Throwable e) { checkAuthority = false; } if (checkAuthority && !Locator.current().benchmark().plugin(HttpPluginBuilder.class).validateAuthority(guessedAuthority)) { String guessedPath = ""; try { if (pathGenerator != null) { guessedPath = pathGenerator.apply(null); } } catch (Throwable e) { } if (authority == null) { throw new BenchmarkDefinitionException(String.format("%s to %s is invalid as we don't have a default route set.", method, guessedPath)); } else if (!guessedAuthority.contains(":")) { throw new BenchmarkDefinitionException(String.format("%s to %s%s is invalid - did you forget the port number?.", method, guessedAuthority, guessedPath)); } else { throw new BenchmarkDefinitionException(String.format("%s to %s%s is invalid - no HTTP configuration defined.", method, guessedAuthority, guessedPath)); } } @SuppressWarnings("unchecked") SerializableBiConsumer[] headerAppenders = this.headerAppenders.isEmpty() ? null : this.headerAppenders.stream().map(Supplier::get).toArray(SerializableBiConsumer[]::new); SLA[] sla = this.sla != null ? this.sla.build() : SLABuilder.DEFAULT; SerializableBiFunction bodyGenerator = this.body != null ? this.body.build() : null; HttpRequestStep step = new HttpRequestStep(stepId, method.build(), authority, pathGenerator, bodyGenerator, headerAppenders, injectHostHeader, metricSelector, timeout, handler.build(), sla); return Collections.singletonList(step); } private static class ProvidedMetricSelector implements SerializableBiFunction { private final String name; private ProvidedMetricSelector(String name) { this.name = name; } @Override public String apply(String authority, String path) { return name; } } private static class PrefixMetricSelector implements SerializableBiFunction { private final String prefix; private final SerializableBiFunction delegate; private PrefixMetricSelector(String prefix, SerializableBiFunction delegate) { this.prefix = prefix; this.delegate = delegate; } @Override public String apply(String authority, String path) { return prefix + delegate.apply(authority, path); } } private static class ReleaseSyncAction implements Action { @Visitor.Ignore private final BeforeSyncRequestStep beforeSyncRequestStep; public ReleaseSyncAction(BeforeSyncRequestStep beforeSyncRequestStep) { this.beforeSyncRequestStep = beforeSyncRequestStep; } @Override public void run(Session s) { s.getResource(beforeSyncRequestStep).set(s.currentSequence().index()); } } } private static class BeforeSyncRequestStep implements Step, ResourceUtilizer, Session.ResourceKey { @Override public boolean invoke(Session s) { BitSetResource resource = s.getResource(this); resource.clear(s.currentSequence().index()); return true; } @Override public void reserve(Session session) { int concurrency = session.currentSequence().definition().concurrency(); session.declareResource(this, () -> new BitSetResource(concurrency), true); } } private static class AfterSyncRequestStep implements Step { private final Session.ResourceKey key; private AfterSyncRequestStep(Session.ResourceKey key) { this.key = key; } @Override public boolean invoke(Session session) { BitSetResource resource = session.getResource(key); return resource.get(session.currentSequence().index()); } } public static class HeadersBuilder extends PairBuilder.OfString implements PartialBuilder { private final Builder parent; public HeadersBuilder(Builder builder) { this.parent = builder; } public HeadersBuilder header(CharSequence header, CharSequence value) { warnIfUsingHostHeader(header); parent.headerAppender(new StaticHeaderWriter(header, value)); return this; } /** * Use header name (e.g. Content-Type) as key and value (possibly a pattern). */ @Override public void accept(String header, String value) { withKey(header).pattern(value); } public Builder endHeaders() { return parent; } /** * Use header name (e.g. Content-Type) as key and specify value in the mapping. */ @Override public PartialHeadersBuilder withKey(String key) { warnIfUsingHostHeader(key); return new PartialHeadersBuilder(this, key); } private void warnIfUsingHostHeader(CharSequence key) { if (key.toString().equalsIgnoreCase("host")) { log.warn("Setting `host` header explicitly is not recommended. Use the HTTP host and adjust actual target using `addresses` property."); parent.injectHostHeader = false; } } } private static class StaticHeaderWriter implements SerializableBiConsumer { private final CharSequence header; private final CharSequence value; private StaticHeaderWriter(CharSequence header, CharSequence value) { this.header = header; this.value = value; } @Override public void accept(Session session, HttpRequestWriter writer) { writer.putHeader(header, value); } } /** * Specifies value that should be sent in headers. */ public static class PartialHeadersBuilder implements InitFromParam { private final HeadersBuilder parent; private final String header; private boolean added; private PartialHeadersBuilder(HeadersBuilder parent, String header) { this.parent = parent; this.header = header; } @Override public PartialHeadersBuilder init(String param) { return pattern(param); } /** * Load header value from session variable. * * @param var Variable name. * @return Self. */ public PartialHeadersBuilder fromVar(String var) { ensureOnce(); parent.parent.headerAppenders.add(() -> new FromVarHeaderWriter(header, SessionFactory.access(var))); return this; } /** * Load header value using a pattern. * * @param patternString Pattern to be encoded, e.g. foo${variable}bar${another-variable} * @return Builder. */ public PartialHeadersBuilder pattern(String patternString) { ensureOnce(); parent.parent.headerAppenders.add(() -> new PatternHeaderWriter(header, new Pattern(patternString, false))); return this; } private void ensureOnce() { if (added) { throw new BenchmarkDefinitionException("Trying to add header " + header + " twice. Use only one of: fromVar, pattern"); } added = true; } public HeadersBuilder end() { return parent; } private static class PatternHeaderWriter implements SerializableBiConsumer { private final String header; private final Pattern pattern; public PatternHeaderWriter(String header, Pattern pattern) { this.header = header; this.pattern = pattern; } @Override public void accept(Session session, HttpRequestWriter writer) { writer.putHeader(header, pattern.apply(session)); } } } public interface BodyGeneratorBuilder extends BuilderBase { SerializableBiFunction build(); } private static class FromVarHeaderWriter implements SerializableBiConsumer { private final CharSequence header; private final Access fromVar; public FromVarHeaderWriter(CharSequence header, Access fromVar) { this.fromVar = fromVar; this.header = header; } @Override public void accept(Session session, HttpRequestWriter writer) { Object value = fromVar.getObject(session); if (value instanceof CharSequence) { writer.putHeader(header, (CharSequence) value); } else { log.error("#{} Cannot convert variable {}: {} to CharSequence", session.uniqueId(), fromVar, value); } } } public static class CompensationBuilder { private static final String DELAY_SESSION_START = "__delay-session-start"; private final Builder parent; public SerializableBiFunction metricSelector; public double targetRate; public double targetRateIncrement; private DoubleIncrementBuilder targetRateBuilder; public CompensationBuilder(Builder parent) { this.parent = parent; } /** * Desired rate of new virtual users per second. This is similar to constantRate.usersPerSec * phase settings but works closer to legacy benchmark drivers by fixing the concurrency. * * @param targetRate Used for calculating the period of each virtual user. * @return Self. */ public CompensationBuilder targetRate(double targetRate) { this.targetRate = targetRate; return this; } /** * Desired rate of new virtual users per second. This is similar to constantRate.usersPerSec * phase settings but works closer to legacy benchmark drivers by fixing the concurrency. * * @return Builder. */ public DoubleIncrementBuilder targetRate() { return targetRateBuilder = new DoubleIncrementBuilder((base, inc) -> { this.targetRate = base; this.targetRateIncrement = inc; }); } /** * Metric name for the compensated results. * * @param name Metric name. * @return Self. */ public CompensationBuilder metric(String name) { this.metricSelector = new Builder.ProvidedMetricSelector(name); return this; } /** * Configure a custom metric for the compensated results. * * @return Builder. */ public PathMetricSelector metric() { PathMetricSelector metricSelector = new PathMetricSelector(); this.metricSelector = metricSelector; return metricSelector; } public void prepareBuild() { if (targetRateBuilder != null) { targetRateBuilder.apply(); } ScenarioBuilder scenario = Locator.current().scenario(); PhaseBuilder phaseBuilder = scenario.endScenario(); if (!(phaseBuilder instanceof PhaseBuilder.Always)) { throw new BenchmarkDefinitionException("delaySessionStart step makes sense only in phase type 'always'"); } if (!scenario.hasSequence(DELAY_SESSION_START)) { List prev = scenario.resetInitialSequences(); scenario.initialSequence(DELAY_SESSION_START) .step(new DelaySessionStartStep(prev.stream().map(SequenceBuilder::name).toArray(String[]::new), targetRate, targetRateIncrement, true)); } else { log.warn("Scenario for phase {} contains multiple compensating HTTP requests: make sure that all use the same rate.", phaseBuilder.name()); } parent.handler.onCompletion(new CompensatedResponseRecorder.Builder().metric(metricSelector)); } public Builder end() { return parent; } } public static class CompensatedResponseRecorder implements Action { private final int stepId; private final SerializableBiFunction metricSelector; public CompensatedResponseRecorder(int stepId, SerializableBiFunction metricSelector) { this.stepId = stepId; this.metricSelector = metricSelector; } @Override public void run(Session session) { HttpRequest request = (HttpRequest) session.currentRequest(); String metric = metricSelector.apply(request.authority, request.path); Statistics statistics = session.statistics(stepId, metric); DelaySessionStartStep.Holder holder = session.getResource(DelaySessionStartStep.KEY); long startTimeMs = holder.lastStartTime(); statistics.incrementRequests(startTimeMs); if (request.cacheControl.wasCached) { statistics.addCacheHit(startTimeMs); } else { long now = System.currentTimeMillis(); log.trace("#{} Session start {}, now {}, diff {}", session.uniqueId(), startTimeMs, now, now - startTimeMs); statistics.recordResponse(startTimeMs, 0, TimeUnit.MILLISECONDS.toNanos(now - startTimeMs)); } } public static class Builder implements Action.Builder { private SerializableBiFunction metricSelector; @Override public Action build() { HttpRequestStep.Builder stepBuilder = (HttpRequestStep.Builder) Locator.current().step(); SerializableBiFunction metricSelector = this.metricSelector; if (metricSelector == null) { metricSelector = new HttpRequestStep.Builder.PrefixMetricSelector("compensated-", stepBuilder.metricSelector); } return new CompensatedResponseRecorder(stepBuilder.id(), metricSelector); } public Builder metric(SerializableBiFunction metricSelector) { this.metricSelector = metricSelector; return this; } } } public static class CompressionBuilder implements BuilderBase { private final Builder parent; private String encoding; private CompressionType type = CompressionType.CONTENT_ENCODING; public CompressionBuilder() { this(null); } public CompressionBuilder(Builder parent) { this.parent = parent; } /** * Encoding used for Accept-Encoding/TE header. The only currently supported is gzip. * * @param encoding Content encoding. * @return Self. */ public CompressionBuilder encoding(String encoding) { this.encoding = encoding; return this; } /** * Type of compression (resource vs. transfer based). * * @param type Compression type. * @return Self. */ public CompressionBuilder type(CompressionType type) { this.type = type; return this; } public Builder end() { return parent; } public void prepareBuild() { if (encoding == null) { // ignore } else if (!encoding.equalsIgnoreCase("gzip")) { throw new BenchmarkDefinitionException("The only supported compression encoding is 'gzip'"); } else { Unique encoding = new Unique(Locator.current().sequence().rootSequence().concurrency() > 0); AsciiString expectedHeader; if (type == CompressionType.CONTENT_ENCODING) { parent.headerAppender(new StaticHeaderWriter(HttpHeaderNames.ACCEPT_ENCODING.toString(), this.encoding)); expectedHeader = HttpHeaderNames.CONTENT_ENCODING; } else if (type == CompressionType.TRANSFER_ENCODING) { parent.headerAppender(new StaticHeaderWriter(HttpHeaderNames.TE, this.encoding)); expectedHeader = HttpHeaderNames.TRANSFER_ENCODING; } else { throw new BenchmarkDefinitionException("Unexpected compression type: " + type); } parent.handler.header(new FilterHeaderHandler.Builder() .header().equalTo(expectedHeader.toString()).end() .processor(HttpRequestProcessorBuilder.adapt(new StoreProcessor.Builder().toVar(encoding)))); parent.handler.wrapBodyHandlers(handlers -> new GzipInflatorProcessor.Builder().processors(handlers).encodingVar(encoding)); } } } public enum CompressionType { /** * Use Accept-Encoding in request and expect Content-Encoding in response. */ CONTENT_ENCODING, /** * Use TE in request and expect Transfer-Encoding in response. */ TRANSFER_ENCODING } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy