joao-r-reis commented on code in PR #1943:
URL:
https://github.com/apache/cassandra-gocql-driver/pull/1943#discussion_r3623804803
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
+ Statement() string
+ Values() []interface{}
+}
+
+type immutableQuery struct {
+ query *Query
+}
+
+func (q *immutableQuery) Statement() string {
+ return q.query.stmt
+}
+
+func (q *immutableQuery) Values() []interface{} {
+ return q.query.values
+}
+
+type ImmutableBatch interface {
+ Type() BatchType
+ Entries() []BatchEntry
+ Cons() Consistency
+}
+
+type immutableBatch struct {
+ batch *Batch
+}
+
+func (b *immutableBatch) Type() BatchType {
+ return b.batch.Type
+}
+
+func (b *immutableBatch) Entries() []BatchEntry {
+ return b.batch.Entries
+}
+
+func (b *immutableBatch) Cons() Consistency {
+ return b.batch.Cons
+}
+
+type QueryAttempt struct {
+ // The op type, whether query or batch.
+ Type OpType
+ // Only one of Query or Batch will be set, depending on the op type.
+ Query ImmutableQuery
+ Batch ImmutableBatch
+ // The host that will receive the query.
+ Host *HostInfo
+ // The local address of the connection used to execute the query.
+ LocalAddr net.Addr
+ // The remote address of the connection used to execute the query.
+ RemoteAddr net.Addr
+ // The number of this query attempt. 0 for the initial attempt, 1 for
the first retry, etc. Note that there may be multiple serial Attempts to
different hosts within a single execution, whether main execution or
speculative.
+ Attempts int
+ // The index of the speculative execution attempt this attempt is
associated with, starting with index 1. -1 indicates this is the "main"
execution.
Review Comment:
Why -1 and not 0?
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
+ Statement() string
+ Values() []interface{}
+}
+
+type immutableQuery struct {
+ query *Query
+}
+
+func (q *immutableQuery) Statement() string {
+ return q.query.stmt
+}
+
+func (q *immutableQuery) Values() []interface{} {
+ return q.query.values
+}
+
+type ImmutableBatch interface {
+ Type() BatchType
+ Entries() []BatchEntry
+ Cons() Consistency
+}
+
+type immutableBatch struct {
+ batch *Batch
+}
+
+func (b *immutableBatch) Type() BatchType {
+ return b.batch.Type
+}
+
+func (b *immutableBatch) Entries() []BatchEntry {
+ return b.batch.Entries
+}
+
+func (b *immutableBatch) Cons() Consistency {
+ return b.batch.Cons
+}
+
+type QueryAttempt struct {
+ // The op type, whether query or batch.
+ Type OpType
+ // Only one of Query or Batch will be set, depending on the op type.
+ Query ImmutableQuery
+ Batch ImmutableBatch
+ // The host that will receive the query.
+ Host *HostInfo
+ // The local address of the connection used to execute the query.
+ LocalAddr net.Addr
+ // The remote address of the connection used to execute the query.
+ RemoteAddr net.Addr
+ // The number of this query attempt. 0 for the initial attempt, 1 for
the first retry, etc. Note that there may be multiple serial Attempts to
different hosts within a single execution, whether main execution or
speculative.
+ Attempts int
+ // The index of the speculative execution attempt this attempt is
associated with, starting with index 1. -1 indicates this is the "main"
execution.
+ SpeculativeExecutionCount int
+}
+
+// QueryAttemptHandler is a function that attempts query execution. The
interceptor must call this once if it does not return an error.
+type QueryAttemptHandler = func(context.Context) (*Iter, error)
+
+// ExecAttemptInterceptor is the interface implemented by interceptors /
middleware.
+//
+// Interceptors are well-suited to logic that is not specific to a single
query or batch, such as flow control.
+type ExecAttemptInterceptor interface {
+ // Intercept is invoked once immediately before a query or batch
execution attempt, including retry attempts and
+ // speculative execution attempts.
+ //
+ // The interceptor is responsible for calling the `handler` function
and returning the handler result. If the
+ // interceptor wants to bypass the handler and skip query execution, it
should return an error. Failure to
+ // return either the handler result or an error will panic.
+ //
+ // Note that there is no affordance to mutate the query or batch at
this stage -- the handler already encapsulates the original query/batch and
cannot be modified.
Review Comment:
This comment is not 100% accurate but it will have to be modified regardless
once my feedback on this topic is addressed
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
+ Statement() string
+ Values() []interface{}
+}
+
+type immutableQuery struct {
+ query *Query
+}
+
+func (q *immutableQuery) Statement() string {
+ return q.query.stmt
+}
+
+func (q *immutableQuery) Values() []interface{} {
+ return q.query.values
+}
+
+type ImmutableBatch interface {
+ Type() BatchType
+ Entries() []BatchEntry
+ Cons() Consistency
+}
+
+type immutableBatch struct {
+ batch *Batch
+}
+
+func (b *immutableBatch) Type() BatchType {
+ return b.batch.Type
+}
+
+func (b *immutableBatch) Entries() []BatchEntry {
+ return b.batch.Entries
+}
+
+func (b *immutableBatch) Cons() Consistency {
+ return b.batch.Cons
+}
+
+type QueryAttempt struct {
+ // The op type, whether query or batch.
+ Type OpType
+ // Only one of Query or Batch will be set, depending on the op type.
+ Query ImmutableQuery
+ Batch ImmutableBatch
+ // The host that will receive the query.
+ Host *HostInfo
+ // The local address of the connection used to execute the query.
+ LocalAddr net.Addr
+ // The remote address of the connection used to execute the query.
+ RemoteAddr net.Addr
+ // The number of this query attempt. 0 for the initial attempt, 1 for
the first retry, etc. Note that there may be multiple serial Attempts to
different hosts within a single execution, whether main execution or
speculative.
+ Attempts int
+ // The index of the speculative execution attempt this attempt is
associated with, starting with index 1. -1 indicates this is the "main"
execution.
+ SpeculativeExecutionCount int
+}
+
+// QueryAttemptHandler is a function that attempts query execution. The
interceptor must call this once if it does not return an error.
+type QueryAttemptHandler = func(context.Context) (*Iter, error)
+
+// ExecAttemptInterceptor is the interface implemented by interceptors /
middleware.
+//
+// Interceptors are well-suited to logic that is not specific to a single
query or batch, such as flow control.
+type ExecAttemptInterceptor interface {
Review Comment:
We could probably get away with naming this `RequestInterceptor` no? And
renaming the related types as well
##########
example_interceptor_test.go:
##########
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Content before git sha 34fdeebefcbf183ed7f916f931aa0586fdaa1b40
+ * Copyright (c) 2016, The Gocql authors,
+ * provided under the BSD-3-Clause License.
+ * See the NOTICE file distributed with this work for additional information.
+ */
+
+package gocql_test
+
+import (
+ "context"
+ "log"
+ "time"
+
+ gocql "github.com/apache/cassandra-gocql-driver/v2"
+)
+
+type MyQueryAttemptInterceptor struct {
+ injectFault bool
+}
+
+func (q MyQueryAttemptInterceptor) Intercept(
+ ctx context.Context,
+ attempt gocql.QueryAttempt,
+ handler gocql.QueryAttemptHandler,
+) (*gocql.Iter, error) {
+ switch q := attempt.Statement.Statement().(type) {
+ case *gocql.Query:
+ // Inspect query
+ log.Println(q.Statement())
+ case *gocql.Batch:
+ // Inspect batch
+
+ log.Println(q.Entries[0].Stmt)
+ }
Review Comment:
I think it's fine if the original Batch and Query objects are exposed here,
the observers already do this and they have a comment on the field to let users
know they should only use these as read only (`// Batch object associated with
this request. Should be used as read only.`). It's not ideal but it's the best
we can do until we have query and batch types that are **actually** immutable
without creating an overcomplicated API.
In regards to allowing interceptors to do things like setting the trace I
think that as long as we have the `QueryAttempt` struct we would have a way to
add this type of functionality in the future without having to add more ways to
mutate Query/Batch (e.g. add a field to the struct that interceptor
implementations are expected to set).
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
+ Statement() string
+ Values() []interface{}
+}
+
+type immutableQuery struct {
+ query *Query
+}
+
+func (q *immutableQuery) Statement() string {
+ return q.query.stmt
+}
+
+func (q *immutableQuery) Values() []interface{} {
+ return q.query.values
+}
+
+type ImmutableBatch interface {
+ Type() BatchType
+ Entries() []BatchEntry
+ Cons() Consistency
+}
+
+type immutableBatch struct {
+ batch *Batch
+}
+
+func (b *immutableBatch) Type() BatchType {
+ return b.batch.Type
+}
+
+func (b *immutableBatch) Entries() []BatchEntry {
+ return b.batch.Entries
+}
+
+func (b *immutableBatch) Cons() Consistency {
+ return b.batch.Cons
+}
+
+type QueryAttempt struct {
+ // The op type, whether query or batch.
+ Type OpType
+ // Only one of Query or Batch will be set, depending on the op type.
+ Query ImmutableQuery
+ Batch ImmutableBatch
+ // The host that will receive the query.
+ Host *HostInfo
+ // The local address of the connection used to execute the query.
+ LocalAddr net.Addr
+ // The remote address of the connection used to execute the query.
+ RemoteAddr net.Addr
+ // The number of this query attempt. 0 for the initial attempt, 1 for
the first retry, etc. Note that there may be multiple serial Attempts to
different hosts within a single execution, whether main execution or
speculative.
+ Attempts int
+ // The index of the speculative execution attempt this attempt is
associated with, starting with index 1. -1 indicates this is the "main"
execution.
+ SpeculativeExecutionCount int
+}
+
+// QueryAttemptHandler is a function that attempts query execution. The
interceptor must call this once if it does not return an error.
+type QueryAttemptHandler = func(context.Context) (*Iter, error)
+
+// ExecAttemptInterceptor is the interface implemented by interceptors /
middleware.
+//
+// Interceptors are well-suited to logic that is not specific to a single
query or batch, such as flow control.
+type ExecAttemptInterceptor interface {
+ // Intercept is invoked once immediately before a query or batch
execution attempt, including retry attempts and
+ // speculative execution attempts.
+ //
+ // The interceptor is responsible for calling the `handler` function
and returning the handler result. If the
+ // interceptor wants to bypass the handler and skip query execution, it
should return an error. Failure to
+ // return either the handler result or an error will panic.
+ //
+ // Note that there is no affordance to mutate the query or batch at
this stage -- the handler already encapsulates the original query/batch and
cannot be modified.
+ Intercept(ctx context.Context, attempt QueryAttempt, handler
QueryAttemptHandler) (*Iter, error)
+}
+
+// Use an ExecAttemptInterceptorChain to apply multiple Interceptors at
Intercept time.
+type ExecAttemptInterceptorChain struct {
Review Comment:
If we want to follow the
https://github.com/apache/cassandra-gocql-driver/pull/1929 precedent of adding
multiplexer types for this type of feature then we should probably use similar
names. In this case it would probably be `ExecAttemptInterceptorMux`
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
Review Comment:
We already have a `Statement` interface that is implemented by both `Query`
and `Batch`, maybe we should rename `OpType` to `StatementType`
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
+ Statement() string
+ Values() []interface{}
+}
+
+type immutableQuery struct {
+ query *Query
+}
+
+func (q *immutableQuery) Statement() string {
+ return q.query.stmt
+}
+
+func (q *immutableQuery) Values() []interface{} {
+ return q.query.values
+}
+
+type ImmutableBatch interface {
+ Type() BatchType
+ Entries() []BatchEntry
+ Cons() Consistency
+}
+
+type immutableBatch struct {
+ batch *Batch
+}
+
+func (b *immutableBatch) Type() BatchType {
+ return b.batch.Type
+}
+
+func (b *immutableBatch) Entries() []BatchEntry {
+ return b.batch.Entries
+}
+
+func (b *immutableBatch) Cons() Consistency {
+ return b.batch.Cons
+}
+
+type QueryAttempt struct {
+ // The op type, whether query or batch.
+ Type OpType
+ // Only one of Query or Batch will be set, depending on the op type.
+ Query ImmutableQuery
+ Batch ImmutableBatch
+ // The host that will receive the query.
+ Host *HostInfo
+ // The local address of the connection used to execute the query.
+ LocalAddr net.Addr
+ // The remote address of the connection used to execute the query.
+ RemoteAddr net.Addr
+ // The number of this query attempt. 0 for the initial attempt, 1 for
the first retry, etc. Note that there may be multiple serial Attempts to
different hosts within a single execution, whether main execution or
speculative.
+ Attempts int
+ // The index of the speculative execution attempt this attempt is
associated with, starting with index 1. -1 indicates this is the "main"
execution.
+ SpeculativeExecutionCount int
+}
+
+// QueryAttemptHandler is a function that attempts query execution. The
interceptor must call this once if it does not return an error.
+type QueryAttemptHandler = func(context.Context) (*Iter, error)
+
+// ExecAttemptInterceptor is the interface implemented by interceptors /
middleware.
+//
+// Interceptors are well-suited to logic that is not specific to a single
query or batch, such as flow control.
+type ExecAttemptInterceptor interface {
+ // Intercept is invoked once immediately before a query or batch
execution attempt, including retry attempts and
+ // speculative execution attempts.
+ //
+ // The interceptor is responsible for calling the `handler` function
and returning the handler result. If the
+ // interceptor wants to bypass the handler and skip query execution, it
should return an error. Failure to
+ // return either the handler result or an error will panic.
+ //
+ // Note that there is no affordance to mutate the query or batch at
this stage -- the handler already encapsulates the original query/batch and
cannot be modified.
+ Intercept(ctx context.Context, attempt QueryAttempt, handler
QueryAttemptHandler) (*Iter, error)
Review Comment:
I'm not sure I like having the interceptor being responsible for calling the
driver code that executes the request. I know this is following the
"middleware" way of doing things but I'd prefer having two methods on this
interface like `BeforeAttempt` and `AfterAttempt` so the driver is in full
control of the request execution and just "checks in" with the interceptor at
the relevant points in time.
##########
session.go:
##########
@@ -968,26 +969,49 @@ type hostMetrics struct {
TotalLatency int64
}
+// Query attempts could be started and finished out of order when using
speculative execution. Therefore,
+// we issue attempt indexes at query start time (so that Interceptors can use
them) then record those attempts
+// at completion time (so that metrics can correctly express average latency).
type queryMetrics struct {
- totalAttempts int64
- totalLatency int64
+ l sync.RWMutex
Review Comment:
Do we need this mutex? Can't we keep using the atomic operations?
##########
session.go:
##########
@@ -968,26 +969,49 @@ type hostMetrics struct {
TotalLatency int64
}
+// Query attempts could be started and finished out of order when using
speculative execution. Therefore,
+// we issue attempt indexes at query start time (so that Interceptors can use
them) then record those attempts
+// at completion time (so that metrics can correctly express average latency).
type queryMetrics struct {
- totalAttempts int64
- totalLatency int64
+ l sync.RWMutex
+ attemptsStarted int
+ attemptsCompleted int
+ totalLatency int64
}
-func (qm *queryMetrics) attempt(addLatency time.Duration) int {
- atomic.AddInt64(&qm.totalLatency, addLatency.Nanoseconds())
- return int(atomic.AddInt64(&qm.totalAttempts, 1) - 1)
+func (qm *queryMetrics) getNextAttempt() int {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ attempt := qm.attemptsStarted
+ qm.attemptsStarted++
+ return attempt
}
+func (qm *queryMetrics) recordAttempt(attempt int, addLatency time.Duration, s
*Session) {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ qm.totalLatency += addLatency.Nanoseconds()
+ qm.attemptsCompleted++
+ if attempt > qm.attemptsCompleted && s != nil {
+ s.logger.Debug("attempt number is greater than total attempts
completed, this should not happen", NewLogFieldInt("attempt", attempt),
NewLogFieldInt("attemptsCompleted", qm.attemptsCompleted))
Review Comment:
If multiple speculative executions are triggered before responses come back
won't this happen? I think that's expected no?
##########
session.go:
##########
@@ -968,26 +969,49 @@ type hostMetrics struct {
TotalLatency int64
}
+// Query attempts could be started and finished out of order when using
speculative execution. Therefore,
+// we issue attempt indexes at query start time (so that Interceptors can use
them) then record those attempts
+// at completion time (so that metrics can correctly express average latency).
type queryMetrics struct {
- totalAttempts int64
- totalLatency int64
+ l sync.RWMutex
+ attemptsStarted int
+ attemptsCompleted int
+ totalLatency int64
}
-func (qm *queryMetrics) attempt(addLatency time.Duration) int {
- atomic.AddInt64(&qm.totalLatency, addLatency.Nanoseconds())
- return int(atomic.AddInt64(&qm.totalAttempts, 1) - 1)
+func (qm *queryMetrics) getNextAttempt() int {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ attempt := qm.attemptsStarted
+ qm.attemptsStarted++
+ return attempt
}
+func (qm *queryMetrics) recordAttempt(attempt int, addLatency time.Duration, s
*Session) {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ qm.totalLatency += addLatency.Nanoseconds()
+ qm.attemptsCompleted++
+ if attempt > qm.attemptsCompleted && s != nil {
+ s.logger.Debug("attempt number is greater than total attempts
completed, this should not happen", NewLogFieldInt("attempt", attempt),
NewLogFieldInt("attemptsCompleted", qm.attemptsCompleted))
+ }
+}
+
+// Returns total number of attempts started.
func (qm *queryMetrics) attempts() int {
- return int(atomic.LoadInt64(&qm.totalAttempts))
+ qm.l.RLock()
+ defer qm.l.RUnlock()
+ return qm.attemptsStarted
Review Comment:
I know you mention that changing this from "attemptsCompleted" to
"attemptsStarted" was a deliberate choice but I would prefer not changing the
behavior of this for existing users with custom retry policies. We can discuss
this on a new issue/PR dedicated to this topic if needed.
##########
query_executor.go:
##########
@@ -69,16 +71,151 @@ type internalRequest interface {
}
type queryExecutor struct {
- pool *policyConnPool
- policy HostSelectionPolicy
+ pool *policyConnPool
+ policy HostSelectionPolicy
+ interceptor ExecAttemptInterceptor
}
-func (q *queryExecutor) attemptQuery(ctx context.Context, qry internalRequest,
conn *Conn) *Iter {
+type OpType int
+
+const (
+ OpQuery OpType = iota
+ OpBatch
+)
+
+type ImmutableQuery interface {
Review Comment:
Even with this API you can still modify the slices that are being returned
so I don't think there's a way to prevent the interceptors from modifying the
query/batch completely unless we deep clone these but this would have to be
opt-in so that applications that don't need to modify them don't have to suffer
the performance penalty that comes with copying these slices on every request.
And to make this opt-in the API would have to be much more complicated and I
don't think it's worth going there.
The observers already expose `Query` and `Batch` directly so I think we
should do the same here with a comment mentioning that they should be treated
as read only.
In the future the goal is to have proper immutable Query and Batch types and
this issue will go away then.
##########
session.go:
##########
@@ -968,26 +969,49 @@ type hostMetrics struct {
TotalLatency int64
}
+// Query attempts could be started and finished out of order when using
speculative execution. Therefore,
+// we issue attempt indexes at query start time (so that Interceptors can use
them) then record those attempts
+// at completion time (so that metrics can correctly express average latency).
type queryMetrics struct {
- totalAttempts int64
- totalLatency int64
+ l sync.RWMutex
+ attemptsStarted int
+ attemptsCompleted int
+ totalLatency int64
}
-func (qm *queryMetrics) attempt(addLatency time.Duration) int {
- atomic.AddInt64(&qm.totalLatency, addLatency.Nanoseconds())
- return int(atomic.AddInt64(&qm.totalAttempts, 1) - 1)
+func (qm *queryMetrics) getNextAttempt() int {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ attempt := qm.attemptsStarted
+ qm.attemptsStarted++
+ return attempt
}
+func (qm *queryMetrics) recordAttempt(attempt int, addLatency time.Duration, s
*Session) {
+ qm.l.Lock()
+ defer qm.l.Unlock()
+ qm.totalLatency += addLatency.Nanoseconds()
+ qm.attemptsCompleted++
+ if attempt > qm.attemptsCompleted && s != nil {
+ s.logger.Debug("attempt number is greater than total attempts
completed, this should not happen", NewLogFieldInt("attempt", attempt),
NewLogFieldInt("attemptsCompleted", qm.attemptsCompleted))
Review Comment:
If you remove this log statement then the session argument can also be
removed
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]