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

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

package io.hyperfoil.core.steps;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import io.hyperfoil.api.config.Benchmark;
import io.hyperfoil.api.config.BenchmarkBuilder;
import io.hyperfoil.api.config.Http;
import io.hyperfoil.api.config.MappingListBuilder;
import io.hyperfoil.api.config.SLA;
import io.hyperfoil.api.config.SLABuilder;
import io.hyperfoil.api.config.Sequence;
import io.hyperfoil.api.connection.HttpRequest;
import io.hyperfoil.api.session.Access;
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.http.CookieAppender;
import io.hyperfoil.core.session.IntVar;
import io.hyperfoil.core.session.ObjectVar;
import io.hyperfoil.core.session.SessionFactory;
import io.hyperfoil.function.SerializableSupplier;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.api.connection.HttpConnectionPool;
import io.hyperfoil.api.connection.HttpRequestWriter;
import io.hyperfoil.api.http.HttpMethod;
import io.hyperfoil.api.config.Step;
import io.hyperfoil.api.session.Session;
import io.hyperfoil.api.config.BaseSequenceBuilder;
import io.hyperfoil.core.builders.BaseStepBuilder;
import io.hyperfoil.api.session.ResourceUtilizer;
import io.hyperfoil.core.util.Util;
import io.hyperfoil.function.SerializableBiConsumer;
import io.hyperfoil.function.SerializableBiFunction;
import io.hyperfoil.function.SerializableFunction;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

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

   final HttpMethod method;
   final SerializableFunction baseUrl;
   final SerializableFunction pathGenerator;
   final SerializableBiFunction bodyGenerator;
   final SerializableBiConsumer[] headerAppenders;
   final SerializableBiFunction statisticsSelector;
   final long timeout;
   final HttpResponseHandlersImpl handler;
   final SLA[] sla;

   public HttpRequestStep(SerializableSupplier sequence, HttpMethod method,
                          SerializableFunction baseUrl,
                          SerializableFunction pathGenerator,
                          SerializableBiFunction bodyGenerator,
                          SerializableBiConsumer[] headerAppenders,
                          SerializableBiFunction statisticsSelector,
                          long timeout, HttpResponseHandlersImpl handler, SLA[] sla) {
      super(sequence);
      this.method = method;
      this.baseUrl = baseUrl;
      this.pathGenerator = pathGenerator;
      this.bodyGenerator = bodyGenerator;
      this.headerAppenders = headerAppenders;
      this.statisticsSelector = statisticsSelector;
      this.timeout = timeout;
      this.handler = handler;
      this.sla = sla;
   }

   @Override
   public boolean invoke(Session session) {
      HttpRequest request = session.httpRequestPool().acquire();
      if (request == null) {
         log.warn("#{} Request pool too small; increase it to prevent blocking.", session.uniqueId());
         return false;
      }

      String baseUrl = this.baseUrl == null ? null : this.baseUrl.apply(session);
      String path = pathGenerator.apply(session);
      if (baseUrl == null && (path.startsWith("http://") || path.startsWith("https://"))) {
         for (String url : session.httpDestinations().baseUrls()) {
            if (path.startsWith(url)) {
               baseUrl = url;
            }
         }
         if (baseUrl == null) {
            log.error("Cannot access {}: no base url configured", path);
            return true;
         }
         path = path.substring(baseUrl.length());
      }
      String statsName = null;
      if (statisticsSelector != null) {
         statsName = statisticsSelector.apply(baseUrl, path);
      }
      if (statsName == null) {
         statsName = sequence().name();
      }
      Statistics statistics = session.statistics(id(), statsName);
      SequenceInstance sequence = session.currentSequence();
      request.baseUrl = baseUrl;
      request.method = method;
      request.path = path;
      request.start(handler, sequence, statistics);

      HttpConnectionPool connectionPool = session.httpDestinations().getConnectionPool(baseUrl);
      if (!connectionPool.request(request, headerAppenders, bodyGenerator)) {
         request.setCompleted();
         session.httpRequestPool().release(request);
         // 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();
         return false;
      }
      long blockedTime = sequence.getBlockedTime();
      if (blockedTime > 0) {
         request.statistics().incrementBlockedTime(blockedTime);
      }
      // Set up timeout only after successful request
      if (timeout > 0) {
         // TODO alloc!
         request.setTimeout(timeout, TimeUnit.MILLISECONDS);
      } else {
         Benchmark benchmark = session.phase().benchmark();
         Http http = baseUrl == null ? benchmark.defaultHttp() : benchmark.http().get(baseUrl);
         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();
      return true;
   }

   @Override
   public void reserve(Session session) {
      handler.reserve(session);
   }

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

   public static class Builder extends BaseStepBuilder {
      private HttpMethod method;
      private StringGeneratorBuilder baseUrl;
      private StringGeneratorBuilder pathGenerator;
      private BodyGeneratorBuilder bodyGenerator;
      private List> headerAppenders = new ArrayList<>();
      private SerializableBiFunction statisticsSelector;
      private long timeout = Long.MIN_VALUE;
      private HttpResponseHandlersImpl.Builder handler = new HttpResponseHandlersImpl.Builder(this);
      private boolean sync = true;
      private SLABuilder.ListBuilder sla = null;

      public Builder(BaseSequenceBuilder parent) {
         super(parent);
      }

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

      // Methods below allow more brevity in the YAML
      public Builder GET(String path) {
         return method(HttpMethod.GET).path(path);
      }

      public StringGeneratorImplBuilder GET() {
         return method(HttpMethod.GET).path();
      }

      public Builder HEAD(String path) {
         return method(HttpMethod.HEAD).path(path);
      }

      public StringGeneratorImplBuilder HEAD() {
         return method(HttpMethod.HEAD).path();
      }

      public Builder POST(String path) {
         return method(HttpMethod.POST).path(path);
      }

      public StringGeneratorImplBuilder POST() {
         return method(HttpMethod.POST).path();
      }

      public Builder PUT(String path) {
         return method(HttpMethod.PUT).path(path);
      }

      public StringGeneratorImplBuilder PUT() {
         return method(HttpMethod.PUT).path();
      }

      public Builder DELETE(String path) {
         return method(HttpMethod.DELETE).path(path);
      }

      public StringGeneratorImplBuilder DELETE() {
         return method(HttpMethod.DELETE).path();
      }

      public Builder OPTIONS(String path) {
         return method(HttpMethod.OPTIONS).path(path);
      }

      public StringGeneratorImplBuilder OPTIONS() {
         return method(HttpMethod.OPTIONS).path();
      }

      public Builder PATCH(String path) {
         return method(HttpMethod.PATCH).path(path);
      }

      public StringGeneratorImplBuilder PATCH() {
         return method(HttpMethod.PATCH).path();
      }

      public Builder TRACE(String path) {
         return method(HttpMethod.TRACE).path(path);
      }

      public StringGeneratorImplBuilder TRACE() {
         return method(HttpMethod.TRACE).path();
      }

      public Builder CONNECT(String path) {
         return method(HttpMethod.CONNECT).path(path);
      }

      public StringGeneratorImplBuilder CONNECT() {
         return method(HttpMethod.CONNECT).path();
      }

      public Builder baseUrl(String baseUrl) {
         return baseUrl(session -> baseUrl);
      }

      public Builder baseUrl(SerializableFunction baseUrlGenerator) {
         return baseUrl(() -> baseUrlGenerator);
      }

      public StringGeneratorImplBuilder baseUrl() {
         StringGeneratorImplBuilder builder = new StringGeneratorImplBuilder<>(this, false);
         baseUrl(builder);
         return builder;
      }

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

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

      public StringGeneratorImplBuilder path() {
         StringGeneratorImplBuilder builder = new StringGeneratorImplBuilder<>(this, false);
         pathGenerator(builder);
         return builder;
      }

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

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

      public Builder body(String string) {
         ByteBuf buf = Unpooled.wrappedBuffer(string.getBytes(StandardCharsets.UTF_8));
         return bodyGenerator((s, c) -> buf);
      }

      public BodyBuilder body() {
         return new BodyBuilder(this);
      }

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

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

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

      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;
      }

      public Builder timeout(String timeout) {
         return timeout(io.hyperfoil.util.Util.parseToMillis(timeout), TimeUnit.MILLISECONDS);
      }

      public Builder statistics(String name) {
         return statistics((baseUrl, path) -> name);
      }

      public Builder statistics(SerializableBiFunction selector) {
         this.statisticsSelector = selector;
         return this;
      }

      public PathStatisticsSelector statistics() {
         PathStatisticsSelector selector = new PathStatisticsSelector();
         this.statisticsSelector = selector;
         return selector;
      }

      public HttpResponseHandlersImpl.Builder handler() {
         return handler;
      }

      public Builder sync(boolean sync) {
         this.sync = sync;
         return this;
      }

      public SLABuilder.ListBuilder sla() {
         if (sla == null) {
            sla = new SLABuilder.ListBuilder<>(this);
         }
         return sla;
      }

      @Override
      public void prepareBuild() {
         if (endStep().endSequence().endScenario().endPhase().ergonomics().repeatCookies()) {
            headerAppender(new CookieAppender());
         }
         if (sync) {
            String var = String.format("%s_sync_%08x", endStep().name(), ThreadLocalRandom.current().nextInt());
            Access access = SessionFactory.access(var);
            endStep().insertBefore(this).step(new SyncRequestIncrementStep(var));
            handler.onCompletion(s -> access.addToInt(s, -1));
            endStep().insertAfter(this).step(new AwaitIntStep(var, x -> x == 0));
         }
         handler.prepareBuild();
      }

      @Override
      public List build(SerializableSupplier sequence) {
         BenchmarkBuilder simulation = endStep().endSequence().endScenario().endPhase();
         String guessedBaseUrl = null;
         boolean checkBaseUrl = true;
         SerializableFunction baseUrl = this.baseUrl != null ? this.baseUrl.build() : null;
         SerializableFunction pathGenerator = this.pathGenerator != null ? this.pathGenerator.build() : null;
         try {
            guessedBaseUrl = baseUrl == null ? null : baseUrl.apply(null);
         } catch (Throwable e) {
            checkBaseUrl = false;
         }
         if (checkBaseUrl && !simulation.validateBaseUrl(guessedBaseUrl)) {
            String guessedPath = "";
            try {
               guessedPath = pathGenerator.apply(null);
            } catch (Throwable e) {}
            if (baseUrl == null) {
               throw new BenchmarkDefinitionException(String.format("%s to /%s is invalid as we don't have a default route set.", method, guessedPath));
            } else {
               throw new BenchmarkDefinitionException(String.format("%s to /%s is invalid - no HTTP configuration defined.", method, this.baseUrl, guessedPath));
            }
         }
         SerializableBiConsumer[] headerAppenders =
               this.headerAppenders.isEmpty() ? null : this.headerAppenders.toArray(new SerializableBiConsumer[0]);

         SLA[] sla = this.sla != null ? this.sla.build() : null;
         SerializableBiFunction bodyGenerator = this.bodyGenerator != null ? this.bodyGenerator.build() : null;

         return Collections.singletonList(new HttpRequestStep(sequence, method, baseUrl, pathGenerator, bodyGenerator, headerAppenders, statisticsSelector, timeout, handler.build(), sla));
      }

      @Override
      public void addCopyTo(BaseSequenceBuilder newParent) {
         Builder newBuilder = new Builder(newParent)
               .method(method)
               .baseUrl(baseUrl)
               .pathGenerator(pathGenerator)
               .bodyGenerator(bodyGenerator)
               .statistics(statisticsSelector)
               .sync(sync);
         if (timeout > 0) {
            newBuilder.timeout(timeout, TimeUnit.MILLISECONDS);
         }
         newBuilder.handler().readFrom(handler);
      }

      @Override
      public boolean canBeLocated() {
         return true;
      }
   }

   private static class SyncRequestIncrementStep implements Step, ResourceUtilizer {
      private final Access var;

      public SyncRequestIncrementStep(String var) {
         this.var = SessionFactory.access(var);
      }

      @Override
      public boolean invoke(Session s) {
         if (var.isSet(s)) {
            if (var.getInt(s) == 0) {
               s.fail(new IllegalStateException("Synchronous HTTP request executed multiple times."));
            } else {
               var.addToInt(s, 1);
            }
         } else {
            var.setInt(s, 1);
         }
         return true;
      }

      @Override
      public void reserve(Session session) {
         var.declareInt(session);
      }
   }

   public static class HeadersBuilder extends PairBuilder.OfString implements PartialBuilder {
      private final Builder parent;

      public HeadersBuilder(Builder builder) {
         this.parent = builder;
      }

      @Override
      public void accept(String header, String value) {
         parent.headerAppenders.add((session, writer) -> writer.putHeader(header, value));
      }

      public Builder endHeaders() {
         return parent;
      }

      @Override
      public PartialHeadersBuilder withKey(java.lang.String key) {
         return new PartialHeadersBuilder(parent, key);
      }
   }

   public static class PartialHeadersBuilder {
      private final Builder parent;
      private final String header;

      private PartialHeadersBuilder(Builder parent, String header) {
         this.parent = parent;
         this.header = header;
      }

      public PartialHeadersBuilder var(String var) {
         Access access = SessionFactory.access(var);
         parent.headerAppenders.add((session, writer) -> {
            Object value = access.getObject(session);
            if (value instanceof CharSequence) {
               writer.putHeader(header, (CharSequence) value);
            } else {
               log.error("#{} Cannot convert variable {}: {} to CharSequence", session.uniqueId(), access, value);
            }
         });
         return this;
      }
   }

   public interface BodyGeneratorBuilder {
      SerializableBiFunction build();
   }

   public static class BodyBuilder {
      private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
      private final Builder parent;

      private BodyBuilder(Builder parent) {
         this.parent = parent;
      }

      public BodyBuilder var(String var) {
         Access access = SessionFactory.access(var);
         parent.bodyGenerator((session, connection) -> {
            Object value = access.getObject(session);
            if (value instanceof ByteBuf) {
               return (ByteBuf) value;
            } else if (value instanceof String){
               String str = (String) value;
               return Util.string2byteBuf(str, connection.context().alloc().buffer(str.length()));

            } else {
               log.error("#{} Cannot encode request body from var {}: {}", session.uniqueId(), access, value);
               return null;
            }
         });
         return this;
      }

      public BodyBuilder pattern(String pattern) {
         Pattern p = new Pattern(pattern, false);
         parent.bodyGenerator((session, connection) -> {
            String str = p.apply(session);
            return Util.string2byteBuf(str, connection.context().alloc().buffer(str.length()));
         });
         return this;
      }

      public BodyBuilder text(String text) {
         byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
         parent.bodyGenerator(((session, connection) -> Unpooled.wrappedBuffer(bytes)));
         return this;
      }

      public FormBuilder form() {
         FormBuilder builder = new FormBuilder();
         parent.headerAppender((session, writer) -> writer.putHeader(HttpHeaderNames.CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED));
         parent.bodyGenerator(builder);
         return builder;
      }

      public Builder endBody() {
         return parent;
      }
   }

   private static class FormBuilder extends PairBuilder.OfString implements BodyGeneratorBuilder, MappingListBuilder {
      private final ArrayList inputs = new ArrayList<>();

      @Override
      public FormInputBuilder addItem() {
         FormInputBuilder input = new FormInputBuilder();
         inputs.add(input);
         return input;
      }

      @Override
      public SerializableBiFunction build() {
         return new FormGenerator(inputs.stream().map(FormInputBuilder::build).toArray(SerializableBiConsumer[]::new));
      }

      @Override
      public void accept(String name, String value) {
         inputs.add(new FormInputBuilder().name(name).value(value));
      }
   }

   private static class FormGenerator implements SerializableBiFunction {
      private final SerializableBiConsumer[] inputs;

      private FormGenerator(SerializableBiConsumer[] inputs) {
         this.inputs = inputs;
      }

      @Override
      public ByteBuf apply(Session session, Connection connection) {
         if (inputs.length == 0) {
            return Unpooled.EMPTY_BUFFER;
         }
         ByteBuf buffer = connection.context().alloc().buffer();
         inputs[0].accept(session, buffer);
         for (int i = 1; i < inputs.length; ++i) {
            buffer.ensureWritable(1);
            buffer.writeByte('&');
            inputs[i].accept(session, buffer);
         }
         return buffer;
      }
   }

   public static class FormInputBuilder {
      private String name;
      private String value;
      private String var;
      private String pattern;

      public SerializableBiConsumer build() {
         if (value != null && var != null && pattern != null) {
            throw new BenchmarkDefinitionException("Form input: Must set only one of 'value', 'var', 'pattern'");
         } else if (value == null && var == null && pattern == null) {
            throw new BenchmarkDefinitionException("Form input: Must set one of 'value' or 'var' or 'pattern'");
         } else if (name == null) {
            throw new BenchmarkDefinitionException("Form input: 'name' must be set.");
         }
         try {
            byte[] nameBytes = URLEncoder.encode(name, StandardCharsets.UTF_8.name()).getBytes(StandardCharsets.UTF_8);
            if (value != null) {
               byte[] valueBytes = URLEncoder.encode(value, StandardCharsets.UTF_8.name()).getBytes(StandardCharsets.UTF_8);
               return (session, buf) -> buf.writeBytes(nameBytes).writeByte('=').writeBytes(valueBytes);
            } else if (var != null) {
               String myVar = this.var; // avoid this capture
               Access access = SessionFactory.access(var);
               return (session, buf) -> {
                  buf.writeBytes(nameBytes).writeByte('=');
                  Session.Var var = access.getVar(session);
                  if (!var.isSet()) {
                     throw new IllegalStateException("Variable " + myVar + " was not set yet!");
                  }
                  if (var instanceof IntVar) {
                     Util.intAsText2byteBuf(var.intValue(), buf);
                  } else if (var instanceof ObjectVar) {
                     Object o = var.objectValue();
                     if (o == null) {
                        // keep it empty
                     } else if (o instanceof byte[]) {
                        buf.writeBytes((byte[]) o);
                     } else {
                        Util.urlEncode(o.toString(), buf);
                     }
                  } else {
                     throw new IllegalStateException();
                  }
               };
            } else {
               return new Pattern(this.pattern, true);
            }
         } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
         }
      }

      public FormInputBuilder name(String name) {
         this.name = name;
         return this;
      }

      public FormInputBuilder value(String value) {
         this.value = value;
         return this;
      }

      public FormInputBuilder var(String var) {
         this.var = var;
         return this;
      }

      public FormInputBuilder pattern(String pattern) {
         this.pattern = pattern;
         return this;
      }
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy