Repository: avenga/couper
Pull request #624 (2022-11-11 13:46:02)

Send each proxy/request only once
Use existing proxy/request instead of creating a new one

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 075a9307..18ae93c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * CVE-2021-3538 related to our `request_id_format` option if switched to `uuid4`: replaced the underlying package to `github.com/google/uuid` ([#611](https://github.com/avenga/couper/pull/611))
   * Possible panic for nested [endpoint sequences](https://docs.couper.io/configuration/block/endpoint#endpoint-sequence) ([#618](https://github.com/avenga/couper/pull/618))
   * Cycle check for [endpoint sequences](https://docs.couper.io/configuration/block/endpoint#endpoint-sequence) ([#623](https://github.com/avenga/couper/pull/623))
+  * In [endpoint sequences](https://docs.couper.io/configuration/block/endpoint#endpoint-sequence) send requests only once ([#624](https://github.com/avenga/couper/pull/624))
 
 ---
 
diff --git a/config/runtime/endpoint.go b/config/runtime/endpoint.go
index c38ba0d8..0daaf06e 100644
--- a/config/runtime/endpoint.go
+++ b/config/runtime/endpoint.go
@@ -83,7 +83,7 @@ func newEndpointMap(srvConf *config.Server, serverOptions *server.Options) (endp
 	return endpoints, nil
 }
 
-func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint, apiConf *config.API,
+func NewEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint, apiConf *config.API,
 	serverOptions *server.Options, log *logrus.Entry, conf *config.Couper, memStore *cache.MemoryStore) (*handler.EndpointOptions, error) {
 	var errTpl *errors.Template
 
@@ -201,10 +201,11 @@ func resolveDependencies(proxies map[string]*producer.Proxy, requests map[string
 	var reqs producer.Requests
 	var ps producer.Proxies
 	var seqs producer.Parallel
+	roundtrips := map[string]producer.Roundtrip{}
 
 	// read from prepared config sequences
 	for _, seq := range items {
-		seqs = append(seqs, newRoundtrip(seq, proxies, requests))
+		seqs = append(seqs, newRoundtrip(seq, roundtrips, proxies, requests))
 	}
 
 proxyLeftovers:
@@ -235,6 +236,7 @@ reqLeftovers:
 }
 
 func newRoundtrip(seq *sequence.Item,
+	roundtrips map[string]producer.Roundtrip,
 	proxies map[string]*producer.Proxy,
 	requests map[string]*producer.Request) producer.Roundtrip {
 
@@ -243,20 +245,41 @@ func newRoundtrip(seq *sequence.Item,
 
 	var previous []string
 	if len(deps) > 1 { // more deps per item can be parallelized
-		var pl producer.Parallel
+		var names []string
+		for _, d := range deps {
+			names = append(names, d.Name)
+		}
+		k := fmt.Sprintf("%v", names)
 		for _, d := range deps {
-			pl = append(pl, newRoundtrip(d, proxies, requests))
 			previous = append(previous, d.Name)
 		}
-		rt = pl
+		if np, ok := roundtrips[k]; ok {
+			rt = np
+		} else {
+			var pl producer.Parallel
+			for _, d := range deps {
+				pl = append(pl, newRoundtrip(d, roundtrips, proxies, requests))
+			}
+			rt = &pl
+			roundtrips[k] = &pl
+		}
 	} else if len(deps) == 1 {
-		rt = newRoundtrip(deps[0], proxies, requests)
+		rt = newRoundtrip(deps[0], roundtrips, proxies, requests)
 		previous = append(previous, deps[0].Name)
 	}
 
-	leaf := newLeafRoundtrip(seq.Name, strings.Join(previous, ","), proxies, requests)
+	leaf := newLeafRoundtrip(seq.Name, strings.Join(previous, ","), roundtrips, proxies, requests)
 	if rt != nil {
-		return producer.Sequence{rt, leaf}
+		var names []string
+		names = append(names, rt.Names()...)
+		names = append(names, leaf.Names()...)
+		k := fmt.Sprintf("%v", names)
+		if ns, ok := roundtrips[k]; ok {
+			return ns
+		}
+		s := &producer.Sequence{rt, leaf}
+		roundtrips[k] = s
+		return s
 	}
 	return leaf
 }
@@ -265,25 +288,23 @@ func newRoundtrip(seq *sequence.Item,
 // producer.Proxies or producer.Requests,
 // no producer.Parallel or producer.Sequence
 func newLeafRoundtrip(name, previous string,
+	roundtrips map[string]producer.Roundtrip,
 	proxies map[string]*producer.Proxy,
 	requests map[string]*producer.Request) producer.Roundtrip {
+	if rt, ok := roundtrips[name]; ok {
+		return rt
+	}
 	if p, ok := proxies[name]; ok {
-		return producer.Proxies{
-			&producer.Proxy{
-				Content:          p.Content,
-				Name:             p.Name,
-				RoundTrip:        p.RoundTrip,
-				PreviousSequence: previous,
-			}}
+		p.PreviousSequence = previous
+		ps := &producer.Proxies{p}
+		roundtrips[name] = ps
+		return ps
 	}
 	if r, ok := requests[name]; ok {
-		return producer.Requests{
-			&producer.Request{
-				Backend:          r.Backend,
-				Context:          r.Context,
-				Name:             r.Name,
-				PreviousSequence: previous,
-			}}
+		r.PreviousSequence = previous
+		rs := &producer.Requests{r}
+		roundtrips[name] = rs
+		return rs
 	}
 	return nil
 }
diff --git a/config/runtime/error_handler.go b/config/runtime/error_handler.go
index 2b480431..5eee09e7 100644
--- a/config/runtime/error_handler.go
+++ b/config/runtime/error_handler.go
@@ -62,7 +62,7 @@ func newErrorHandler(ctx *hcl.EvalContext, conf *config.Couper, opts *protectedO
 				epConf.Response = &config.Response{Remain: emptyBody}
 			}
 
-			epOpts, err := newEndpointOptions(ctx, epConf, nil, opts.srvOpts, log, conf, opts.memStore)
+			epOpts, err := NewEndpointOptions(ctx, epConf, nil, opts.srvOpts, log, conf, opts.memStore)
 			if err != nil {
 				return nil, err
 			}
diff --git a/config/runtime/server.go b/config/runtime/server.go
index 9eb96dbb..8d96bd8b 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -255,7 +255,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				return nil, fmt.Errorf("endpoint path pattern required")
 			}
 
-			epOpts, err := newEndpointOptions(confCtx, endpointConf, parentAPI, serverOptions,
+			epOpts, err := NewEndpointOptions(confCtx, endpointConf, parentAPI, serverOptions,
 				log, conf, memStore)
 			if err != nil {
 				return nil, err
diff --git a/handler/endpoint.go b/handler/endpoint.go
index ae3d20d3..8d447934 100644
--- a/handler/endpoint.go
+++ b/handler/endpoint.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"runtime/debug"
 	"strconv"
+	"sync"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/sirupsen/logrus"
@@ -217,7 +218,7 @@ func (e *Endpoint) produce(req *http.Request) (producer.ResultMap, error) {
 			continue
 		}
 
-		tripCh <- trip.Produce(outreq)
+		tripCh <- trip.Produce(outreq, &sync.Map{})
 	}
 	close(tripCh)
 
diff --git a/handler/producer/produce.go b/handler/producer/produce.go
index 7e949732..19b75580 100644
--- a/handler/producer/produce.go
+++ b/handler/producer/produce.go
@@ -2,6 +2,7 @@ package producer
 
 import (
 	"net/http"
+	"sync"
 )
 
 var (
@@ -12,6 +13,7 @@ var (
 )
 
 type Roundtrip interface {
-	Produce(req *http.Request) chan *Result
+	Produce(req *http.Request, additionalChs *sync.Map) chan *Result
 	Len() int
+	Names() []string
 }
diff --git a/handler/producer/proxy.go b/handler/producer/proxy.go
index f04dbbaf..c29e50dd 100644
--- a/handler/producer/proxy.go
+++ b/handler/producer/proxy.go
@@ -24,7 +24,7 @@ type Proxy struct {
 
 type Proxies []*Proxy
 
-func (pr Proxies) Produce(clientReq *http.Request) chan *Result {
+func (pr Proxies) Produce(clientReq *http.Request, _ *sync.Map) chan *Result {
 	var currentName string // at least pre roundtrip
 	wg := &sync.WaitGroup{}
 	ctx := clientReq.Context()
@@ -93,3 +93,11 @@ func (pr Proxies) Produce(clientReq *http.Request) chan *Result {
 func (pr Proxies) Len() int {
 	return len(pr)
 }
+
+func (pr Proxies) Names() []string {
+	var names []string
+	for _, i := range pr {
+		names = append(names, i.Name)
+	}
+	return names
+}
diff --git a/handler/producer/request.go b/handler/producer/request.go
index 6703a5eb..ca7f2262 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -30,7 +30,7 @@ type Request struct {
 // Requests represents the producer <Requests> object.
 type Requests []*Request
 
-func (r Requests) Produce(req *http.Request) chan *Result {
+func (r Requests) Produce(req *http.Request, _ *sync.Map) chan *Result {
 	var currentName string // at least pre roundtrip
 	wg := &sync.WaitGroup{}
 	ctx := req.Context()
@@ -146,6 +146,14 @@ func (r Requests) Len() int {
 	return len(r)
 }
 
+func (r Requests) Names() []string {
+	var names []string
+	for _, i := range r {
+		names = append(names, i.Name)
+	}
+	return names
+}
+
 func withRoundTripName(ctx context.Context, name string) context.Context {
 	n := name
 	if n == "" {
diff --git a/handler/producer/sequence.go b/handler/producer/sequence.go
index c9db7a2e..45e90c13 100644
--- a/handler/producer/sequence.go
+++ b/handler/producer/sequence.go
@@ -2,7 +2,9 @@ package producer
 
 import (
 	"context"
+	"fmt"
 	"net/http"
+	"sync"
 
 	"go.opentelemetry.io/otel/trace"
 
@@ -16,16 +18,24 @@ type Sequence []Roundtrip
 // Parallel holds a list of items which get executed in parallel.
 type Parallel []Roundtrip
 
-func (p Parallel) Produce(req *http.Request) chan *Result {
-	return pipe(req, p, "parallel")
+func (p Parallel) Produce(req *http.Request, additionalSync *sync.Map) chan *Result {
+	return pipe(req, p, "parallel", additionalSync)
 }
 
 func (p Parallel) Len() int {
 	return len(p)
 }
 
-func (s Sequence) Produce(req *http.Request) chan *Result {
-	return pipe(req, s, "sequence")
+func (p Parallel) Names() []string {
+	var names []string
+	for _, i := range p {
+		names = append(names, i.Names()...)
+	}
+	return names
+}
+
+func (s Sequence) Produce(req *http.Request, additionalSync *sync.Map) chan *Result {
+	return pipe(req, s, "sequence", additionalSync)
 }
 
 func (s Sequence) Len() int {
@@ -36,9 +46,17 @@ func (s Sequence) Len() int {
 	return sum
 }
 
+func (s Sequence) Names() []string {
+	var names []string
+	for _, i := range s {
+		names = append(names, i.Names()...)
+	}
+	return names
+}
+
 // pipe calls the Roundtrip Interface on each given item and distinguishes between parallelism and trace kind.
 // The returned channel will be closed if this chain part has been ended.
-func pipe(req *http.Request, rt []Roundtrip, kind string) chan *Result {
+func pipe(req *http.Request, rt []Roundtrip, kind string, additionalSync *sync.Map) chan *Result {
 	var rootSpan trace.Span
 	ctx := req.Context()
 	if len(rt) > 0 {
@@ -52,12 +70,32 @@ func pipe(req *http.Request, rt []Roundtrip, kind string) chan *Result {
 	for _, srt := range rt {
 		rch := make(chan *Result, srt.Len())
 		allResults = append(allResults, rch)
+		k := fmt.Sprintf("%v", srt.Names())
+		if val, ok := additionalSync.Load(k); ok {
+			additionalChs := val.([]chan *Result)
+			// srt is already prepared to Produce(), so we can here just listen to the result(s)
+			ach := make(chan *Result, srt.Len())
+			additionalChs = append(additionalChs, ach)
+			additionalSync.Store(k, additionalChs)
+			switch kind {
+			case "parallel": // execute each sequence branch in parallel
+				go func() {
+					pipeResults(rch, ach)
+					close(rch)
+				}()
+			case "sequence": // one by one
+				pipeResults(rch, ach)
+				close(rch)
+			}
+			continue
+		}
+		additionalSync.Store(k, []chan *Result{})
 
 		switch kind {
 		case "parallel": // execute each sequence branch in parallel
-			go pipeResult(ctx, req, rch, srt)
+			go produceAndPipeResults(ctx, req, rch, srt, additionalSync)
 		case "sequence": // one by one
-			pipeResult(ctx, req, rch, srt)
+			produceAndPipeResults(ctx, req, rch, srt, additionalSync)
 		}
 	}
 
@@ -87,16 +125,39 @@ func pipe(req *http.Request, rt []Roundtrip, kind string) chan *Result {
 	return result
 }
 
-func pipeResult(ctx context.Context, req *http.Request, results chan *Result, rt Roundtrip) {
+func pipeResults(target, src chan *Result) {
+	for r := range src {
+		target <- r
+	}
+}
+
+func produceAndPipeResults(ctx context.Context, req *http.Request, results chan *Result, rt Roundtrip, additionalSync *sync.Map) {
 	outreq := req.WithContext(ctx)
 	defer close(results)
+	rs := rt.Produce(outreq, additionalSync)
+
+	k := fmt.Sprintf("%v", rt.Names())
+	var additionalChs []chan *Result
+	if val, ok := additionalSync.Load(k); ok {
+		additionalChs = val.([]chan *Result)
+	}
+	for _, ach := range additionalChs {
+		defer close(ach)
+	}
 
-	for r := range rt.Produce(outreq) {
+	for r := range rs {
 		select {
 		case <-ctx.Done():
-			results <- &Result{Err: ctx.Err()}
+			e := &Result{Err: ctx.Err()}
+			results <- e
+			for _, ach := range additionalChs {
+				ach <- e
+			}
 			return
 		case results <- r:
+			for _, ach := range additionalChs {
+				ach <- r
+			}
 		}
 	}
 }
diff --git a/handler/transport/token_request.go b/handler/transport/token_request.go
index 5244606e..b056b6ac 100644
--- a/handler/transport/token_request.go
+++ b/handler/transport/token_request.go
@@ -83,7 +83,7 @@ func (t *TokenRequest) requestToken(req *http.Request) (string, int64, error) {
 	ctx = context.WithValue(ctx, request.BufferOptions, eval.BufferResponse) // always read out a possible token
 	ctx = context.WithValue(ctx, request.TokenRequest, t.config.Name)        // set the name for variable mapping purposes
 	outreq, _ := http.NewRequestWithContext(ctx, req.Method, "", nil)
-	result := <-t.reqProducer.Produce(outreq)
+	result := <-t.reqProducer.Produce(outreq, nil)
 	if result.Err != nil {
 		return "", 0, fmt.Errorf("token request failed") // don't propagate token request roundtrip error
 	}


Test patch:
diff --git a/config/runtime/endpoint_test.go b/config/runtime/endpoint_test.go
new file mode 100644
index 00000000..6e51234c
--- /dev/null
+++ b/config/runtime/endpoint_test.go
@@ -0,0 +1,436 @@
+package runtime_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/avenga/couper/cache"
+	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/config/runtime/server"
+	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/handler/producer"
+	"github.com/avenga/couper/internal/test"
+)
+
+func TestEndpoint_Same_Requests(t *testing.T) {
+	helper := test.New(t)
+	hcl := `server {
+  api {
+    endpoint "/" {
+      request "r1" {
+        url = "/res1"
+        backend = "ba"
+      }
+
+      request "r2" {
+        url = "/res2"
+        backend = "ba"
+        json_body = backend_responses.r1.json_body
+      }
+
+      request {
+        url = "/"
+        backend = "bb"
+        json_body = [
+          backend_responses.r1.json_body
+          ,
+          backend_responses.r2.json_body
+        ]
+      }
+    }
+  }
+}
+definitions {
+  backend "ba" {
+    origin = "https://ba.example.com"
+  }
+  backend "bb" {
+    origin = "https://bb.example.com"
+  }
+}
+`
+	conf, err := configload.LoadBytes([]byte(hcl), "couper.hcl")
+	helper.Must(err)
+
+	log, _ := test.NewLogger()
+	logger := log.WithContext(context.TODO())
+
+	tmpStoreCh := make(chan struct{})
+	defer close(tmpStoreCh)
+
+	evalCtx := conf.Context.(*eval.Context)
+	confCtx := evalCtx.HCLContext()
+	srvConf := conf.Servers[0]
+	serverOptions, err := server.NewServerOptions(srvConf, logger)
+	helper.Must(err)
+
+	apiConf := srvConf.APIs[0]
+	endpointConf := apiConf.Endpoints[0]
+
+	endpointOptions, err := runtime.NewEndpointOptions(confCtx, endpointConf, apiConf, serverOptions, logger, conf, cache.New(logger, tmpStoreCh))
+	helper.Must(err)
+	sequences := endpointOptions.Sequences.(producer.Parallel)
+
+	var r1, r1_, r2 *producer.Requests
+	if s, ok := sequences[0].(*producer.Sequence); ok {
+		vs := *s
+		if p, ok := vs[0].(*producer.Parallel); ok {
+			vp := *p
+			if s, ok := vp[0].(*producer.Sequence); ok {
+				vs := *s
+				if r, ok := vs[0].(*producer.Requests); ok {
+					r1_ = r
+				}
+			}
+			if r, ok := vp[1].(*producer.Requests); ok {
+				r1 = r
+			}
+		}
+		if r, ok := vs[1].(*producer.Requests); ok {
+			r2 = r
+		}
+	}
+	if r1 == nil {
+		t.Error("expected r1 not to be nil")
+	}
+	if r1_ == nil {
+		t.Error("expected r1_ not to be nil")
+	}
+	if r2 == nil {
+		t.Error("expected r2 not to be nil")
+	}
+	if r1 != r1_ {
+		t.Error("expected same *producer.Requests")
+	}
+	if r1 == r2 {
+		t.Error("did not expect same *producer.Requests")
+	}
+}
+
+func TestEndpoint_Same_Proxies(t *testing.T) {
+	helper := test.New(t)
+	hcl := `server {
+  api {
+    endpoint "/" {
+      proxy "p1" {
+        backend "ba" {
+          path = "/res1"
+        }
+      }
+
+      request "r2" {
+        url = "/res2"
+        backend = "ba"
+        json_body = backend_responses.p1.json_body
+      }
+
+      proxy "p3" {
+        backend "ba" {
+          path = "/res3"
+        }
+      }
+
+      request {
+        url = "/"
+        backend = "bb"
+        json_body = [
+          backend_responses.p1.json_body
+          ,
+          backend_responses.r2.json_body
+          ,
+          backend_responses.p3.json_body
+        ]
+      }
+    }
+  }
+}
+definitions {
+  backend "ba" {
+    origin = "https://ba.example.com"
+  }
+  backend "bb" {
+    origin = "https://bb.example.com"
+  }
+}
+`
+	conf, err := configload.LoadBytes([]byte(hcl), "couper.hcl")
+	helper.Must(err)
+
+	log, _ := test.NewLogger()
+	logger := log.WithContext(context.TODO())
+
+	tmpStoreCh := make(chan struct{})
+	defer close(tmpStoreCh)
+
+	evalCtx := conf.Context.(*eval.Context)
+	confCtx := evalCtx.HCLContext()
+	srvConf := conf.Servers[0]
+	serverOptions, err := server.NewServerOptions(srvConf, logger)
+	helper.Must(err)
+
+	apiConf := srvConf.APIs[0]
+	endpointConf := apiConf.Endpoints[0]
+
+	endpointOptions, err := runtime.NewEndpointOptions(confCtx, endpointConf, apiConf, serverOptions, logger, conf, cache.New(logger, tmpStoreCh))
+	helper.Must(err)
+	sequences := endpointOptions.Sequences.(producer.Parallel)
+
+	var p1, p1_, p3 *producer.Proxies
+	if s, ok := sequences[0].(*producer.Sequence); ok {
+		vs := *s
+		if p, ok := vs[0].(*producer.Parallel); ok {
+			vp := *p
+			if p, ok := vp[0].(*producer.Proxies); ok {
+				p3 = p
+			}
+			if s, ok := vp[1].(*producer.Sequence); ok {
+				vs := *s
+				if p, ok := vs[0].(*producer.Proxies); ok {
+					p1_ = p
+				}
+			}
+			if p, ok := vp[2].(*producer.Proxies); ok {
+				p1 = p
+			}
+		}
+	}
+	if p1 == nil {
+		t.Error("expected p1 not to be nil")
+	}
+	if p1_ == nil {
+		t.Error("expected p1_ not to be nil")
+	}
+	if p3 == nil {
+		t.Error("expected p3 not to be nil")
+	}
+	if p1 != p1_ {
+		t.Error("expected same *producer.Proxies")
+	}
+	if p1 == p3 {
+		t.Error("did not expect same *producer.Proxies")
+	}
+}
+
+func TestEndpoint_Same_Parallel(t *testing.T) {
+	helper := test.New(t)
+	hcl := `server {
+  api {
+    endpoint "/" {
+      request "r1" {
+        url = "/res1"
+        backend = "ba"
+      }
+
+      request "r2" {
+        url = "/res2"
+        backend = "ba"
+      }
+
+      request "r3" {
+        url = "/res3"
+        backend = "ba"
+        json_body = [
+          backend_responses.r1.json_body
+          ,
+          backend_responses.r2.json_body
+        ]
+      }
+
+      request "r4" {
+        url = "/res4"
+        backend = "ba"
+        json_body = [
+          backend_responses.r1.json_body
+          ,
+          backend_responses.r2.json_body
+        ]
+      }
+
+      request {
+        url = "/"
+        backend = "bb"
+        json_body = [
+          backend_responses.r3.json_body
+          ,
+          backend_responses.r4.json_body
+        ]
+      }
+    }
+  }
+}
+definitions {
+  backend "ba" {
+    origin = "https://ba.example.com"
+  }
+  backend "bb" {
+    origin = "https://bb.example.com"
+  }
+}
+`
+	conf, err := configload.LoadBytes([]byte(hcl), "couper.hcl")
+	helper.Must(err)
+
+	log, _ := test.NewLogger()
+	logger := log.WithContext(context.TODO())
+
+	tmpStoreCh := make(chan struct{})
+	defer close(tmpStoreCh)
+
+	evalCtx := conf.Context.(*eval.Context)
+	confCtx := evalCtx.HCLContext()
+	srvConf := conf.Servers[0]
+	serverOptions, err := server.NewServerOptions(srvConf, logger)
+	helper.Must(err)
+
+	apiConf := srvConf.APIs[0]
+	endpointConf := apiConf.Endpoints[0]
+
+	endpointOptions, err := runtime.NewEndpointOptions(confCtx, endpointConf, apiConf, serverOptions, logger, conf, cache.New(logger, tmpStoreCh))
+	helper.Must(err)
+	sequences := endpointOptions.Sequences.(producer.Parallel)
+
+	var p1, p1_, p2 *producer.Parallel
+	if s, ok := sequences[0].(*producer.Sequence); ok {
+		vs := *s
+		if p, ok := vs[0].(*producer.Parallel); ok {
+			p2 = p
+			vp := *p
+			if s, ok := vp[0].(*producer.Sequence); ok {
+				vs := *s
+				if p, ok := vs[0].(*producer.Parallel); ok {
+					p1 = p
+				}
+			}
+			if s, ok := vp[1].(*producer.Sequence); ok {
+				vs := *s
+				if p, ok := vs[0].(*producer.Parallel); ok {
+					p1_ = p
+				}
+			}
+		}
+	}
+	if p1 == nil {
+		t.Error("expected p1 not to be nil")
+	}
+	if p1_ == nil {
+		t.Error("expected p1_ not to be nil")
+	}
+	if p2 == nil {
+		t.Error("expected p2 not to be nil")
+	}
+	if p1 != p1_ {
+		t.Error("expected same *producer.Parallel")
+	}
+	if p1 == p2 {
+		t.Error("did not expect same *producer.Parallel")
+	}
+}
+
+func TestEndpoint_Same_Sequence(t *testing.T) {
+	helper := test.New(t)
+	hcl := `server {
+  api {
+    endpoint "/" {
+      request "r1" {
+        url = "/res1"
+        backend = "ba"
+      }
+
+      request "r2" {
+        url = "/res2"
+        backend = "ba"
+        json_body = backend_responses.r1.json_body
+      }
+
+      request "r3" {
+        url = "/res3"
+        backend = "ba"
+        json_body = backend_responses.r2.json_body
+      }
+
+      request "r4" {
+        url = "/res4"
+        backend = "ba"
+        json_body = backend_responses.r2.json_body
+      }
+
+      request {
+        url = "/"
+        backend = "bb"
+        json_body = [
+          backend_responses.r3.json_body
+          ,
+          backend_responses.r4.json_body
+        ]
+      }
+    }
+  }
+}
+definitions {
+  backend "ba" {
+    origin = "https://ba.example.com"
+  }
+  backend "bb" {
+    origin = "https://bb.example.com"
+  }
+}
+`
+	conf, err := configload.LoadBytes([]byte(hcl), "couper.hcl")
+	helper.Must(err)
+
+	log, _ := test.NewLogger()
+	logger := log.WithContext(context.TODO())
+
+	tmpStoreCh := make(chan struct{})
+	defer close(tmpStoreCh)
+
+	evalCtx := conf.Context.(*eval.Context)
+	confCtx := evalCtx.HCLContext()
+	srvConf := conf.Servers[0]
+	serverOptions, err := server.NewServerOptions(srvConf, logger)
+	helper.Must(err)
+
+	apiConf := srvConf.APIs[0]
+	endpointConf := apiConf.Endpoints[0]
+
+	endpointOptions, err := runtime.NewEndpointOptions(confCtx, endpointConf, apiConf, serverOptions, logger, conf, cache.New(logger, tmpStoreCh))
+	helper.Must(err)
+	sequences := endpointOptions.Sequences.(producer.Parallel)
+
+	var s1, s1_, s2 *producer.Sequence
+	if s, ok := sequences[0].(*producer.Sequence); ok {
+		s2 = s
+		vs := *s
+		if p, ok := vs[0].(*producer.Parallel); ok {
+			vp := *p
+			if s, ok := vp[0].(*producer.Sequence); ok {
+				vs := *s
+				if s, ok := vs[0].(*producer.Sequence); ok {
+					s1 = s
+				}
+			}
+			if s, ok := vp[1].(*producer.Sequence); ok {
+				vs := *s
+				if s, ok := vs[0].(*producer.Sequence); ok {
+					s1_ = s
+				}
+			}
+		}
+	}
+	if s1 == nil {
+		t.Error("expected s1 not to be nil")
+	}
+	if s1_ == nil {
+		t.Error("expected s1_ not to be nil")
+	}
+	if s2 == nil {
+		t.Error("expected s2 not to be nil")
+	}
+	if s1 != s1_ {
+		t.Error("expected same *producer.Sequence")
+	}
+	if s1 == s2 {
+		t.Error("did not expect same *producer.Sequence")
+	}
+}
diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go
index 01cbe7ce..d4a9b782 100644
--- a/handler/endpoint_test.go
+++ b/handler/endpoint_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"os"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
@@ -385,7 +386,7 @@ type mockProducerResult struct {
 	rt http.RoundTripper
 }
 
-func (m *mockProducerResult) Produce(r *http.Request) chan *producer.Result {
+func (m *mockProducerResult) Produce(r *http.Request, _ *sync.Map) chan *producer.Result {
 	result := make(chan *producer.Result, 1)
 	defer close(result)
 
@@ -406,6 +407,10 @@ func (m *mockProducerResult) Len() int {
 	return 1
 }
 
+func (m *mockProducerResult) Names() []string {
+	return []string{"default"}
+}
+
 func TestEndpoint_ServeHTTP_FaultyDefaultResponse(t *testing.T) {
 	log, hook := test.NewLogger()
 
diff --git a/handler/producer/request_test.go b/handler/producer/request_test.go
index 9c4cdc49..2186d3b7 100644
--- a/handler/producer/request_test.go
+++ b/handler/producer/request_test.go
@@ -107,7 +107,7 @@ func Test_ProduceExpectedStatus(t *testing.T) {
 				outreq := clientRequest.WithContext(ctx)
 				outreq.Header.Set("X-Status", strconv.Itoa(tt.reflectStatus))
 
-				result := <-rt.Produce(outreq)
+				result := <-rt.Produce(outreq, nil)
 
 				if !errors.Equals(tt.expectedErr, result.Err) {
 					t.Fatalf("expected error: %v, got %v", tt.expectedErr, result.Err)
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 429a258b..b0868a87 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -592,15 +592,16 @@ func TestEndpoint_Sequence(t *testing.T) {
 		path           string
 		expectedHeader test.Header
 		expectedBody   string
-		expectedLog    log
+		expectedDep    log
+		expNumBEReq    int
 	}
 
 	for _, tc := range []testcase{
-		{"simple request sequence", "/simple", test.Header{"x": "my-value"}, `{"value":"my-value"}`, log{"default": "resolve"}},
-		{"simple request/proxy sequence", "/simple-proxy", test.Header{"x": "my-value", "y": `{"value":"my-proxy-value"}`}, "", log{"default": "resolve"}},
-		{"simple proxy/request sequence", "/simple-proxy-named", test.Header{"x": "my-value"}, "", log{"default": "resolve"}},
-		{"complex request/proxy sequence", "/complex-proxy", test.Header{"x": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}},
-		{"complex request/proxy sequences", "/parallel-complex-proxy", test.Header{"x": "my-value", "y": "my-value", "z": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}},
+		{"simple request sequence", "/simple", test.Header{"x": "my-value"}, `{"value":"my-value"}`, log{"default": "resolve"}, 2},
+		{"simple request/proxy sequence", "/simple-proxy", test.Header{"x": "my-value", "y": `{"value":"my-proxy-value"}`}, "", log{"default": "resolve"}, 2},
+		{"simple proxy/request sequence", "/simple-proxy-named", test.Header{"x": "my-value"}, "", log{"default": "resolve"}, 2},
+		{"complex request/proxy sequence", "/complex-proxy", test.Header{"x": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}, 3},
+		{"complex request/proxy sequences", "/parallel-complex-proxy", test.Header{"x": "my-value", "y": "my-value", "z": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}, 6},
 		{"complex nested request/proxy sequences", "/parallel-complex-nested", test.Header{
 			"a": "my-value",
 			"b": "my-value",
@@ -611,7 +612,11 @@ func TestEndpoint_Sequence(t *testing.T) {
 			"resolve":       "resolve_first",
 			"resolve_gamma": "resolve_gamma_first",
 			"last":          "resolve,resolve_gamma",
-		}},
+		}, 6},
+		{"multiple request uses", "/multiple-request-uses", test.Header{}, "", log{"r2": "r1", "default": "r2,r1"}, 3},
+		{"multiple proxy uses", "/multiple-proxy-uses", test.Header{}, "", log{"r2": "p1", "default": "r2,p1"}, 3},
+		{"multiple sequence uses", "/multiple-sequence-uses", test.Header{}, "", log{"r2": "r1", "r4": "r2", "r3": "r2", "default": "r4,r3"}, 5},
+		{"multiple parallel uses", "/multiple-parallel-uses", test.Header{}, "", log{"r4": "r2,r1", "r3": "r2,r1", "default": "r4,r3"}, 5},
 	} {
 		t.Run(tc.name, func(st *testing.T) {
 			hook.Reset()
@@ -646,15 +651,17 @@ func TestEndpoint_Sequence(t *testing.T) {
 				}
 			}
 
+			nbr := 0
 			for _, e := range hook.AllEntries() {
 				if e.Data["type"] != "couper_backend" {
 					continue
 				}
+				nbr++
 
 				requestName, _ := e.Data["request"].(logging.Fields)["name"].(string)
 
 				// test result for expected named ones
-				if _, exist := tc.expectedLog[requestName]; !exist {
+				if _, exist := tc.expectedDep[requestName]; !exist {
 					continue
 				}
 
@@ -663,10 +670,13 @@ func TestEndpoint_Sequence(t *testing.T) {
 					st.Fatal("Expected 'depends_on' log field")
 				}
 
-				if dependsOn != tc.expectedLog[requestName] {
-					st.Errorf("Expected 'depends_on' log for %q with field value: %q, got: %q", requestName, tc.expectedLog[requestName], dependsOn)
+				if dependsOn != tc.expectedDep[requestName] {
+					st.Errorf("Expected 'depends_on' log for %q with field value: %q, got: %q", requestName, tc.expectedDep[requestName], dependsOn)
 				}
 			}
+			if nbr != tc.expNumBEReq {
+				st.Errorf("Expected number of backend requests: %d, got: %d", tc.expNumBEReq, nbr)
+			}
 
 		})
 	}
diff --git a/server/testdata/endpoints/11_couper.hcl b/server/testdata/endpoints/11_couper.hcl
index 09876da7..2aa282ce 100644
--- a/server/testdata/endpoints/11_couper.hcl
+++ b/server/testdata/endpoints/11_couper.hcl
@@ -158,4 +158,99 @@ server { # sequences
       }
     }
   }
+
+  endpoint "/multiple-request-uses" {
+    request "r1" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+    }
+    request "r2" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = backend_responses.r1.json_body
+    }
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.r1.json_body
+        ,
+        backend_responses.r2.json_body
+      ]
+    }
+  }
+
+  endpoint "/multiple-proxy-uses" {
+    proxy "p1" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+    }
+    request "r2" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = backend_responses.p1.json_body
+    }
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.p1.json_body
+        ,
+        backend_responses.r2.json_body
+      ]
+    }
+  }
+
+  endpoint "/multiple-sequence-uses" {
+    request "r1" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+    }
+    request "r2" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = backend_responses.r1.json_body
+    }
+    request "r3" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = backend_responses.r2.json_body
+    }
+    request "r4" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = backend_responses.r2.json_body
+    }
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.r3.json_body
+        ,
+        backend_responses.r4.json_body
+      ]
+    }
+  }
+
+  endpoint "/multiple-parallel-uses" {
+    request "r1" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+    }
+    request "r2" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+    }
+    request "r3" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.r1.json_body
+        ,
+        backend_responses.r2.json_body
+      ]
+    }
+    request "r4" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.r1.json_body
+        ,
+        backend_responses.r2.json_body
+      ]
+    }
+    request {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      json_body = [
+        backend_responses.r3.json_body
+        ,
+        backend_responses.r4.json_body
+      ]
+    }
+  }
 }