-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Add publisher confirms to ingress #543
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
Copyright 2021 The Knative Authors | ||
|
||
Licensed 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. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"log" | ||
"sync" | ||
|
||
amqp "github.com/rabbitmq/amqp091-go" | ||
) | ||
|
||
type Channel struct { | ||
channel *amqp.Channel | ||
counter uint64 | ||
mutex sync.Mutex | ||
confirmsMap sync.Map | ||
pconfirms_chan chan amqp.Confirmation | ||
} | ||
|
||
func openChannel(conn *amqp.Connection) *Channel { | ||
a_channel, err := conn.Channel() | ||
if err != nil { | ||
log.Fatalf("failed to open a channel: %s", err) | ||
} | ||
|
||
channel := &Channel{channel: a_channel, counter: 1, pconfirms_chan: make(chan amqp.Confirmation)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we use unbuffered channel for confirmation. docs for confirmation channel in NotifyPublish state it should be buffered to accomodate for multiple in-flights, otherwise risking deadlocks. Could we run into this scenario with an unbuffered channel? |
||
|
||
go channel.confirmsHandler() | ||
|
||
a_channel.NotifyPublish(channel.pconfirms_chan) | ||
// noWait is false | ||
if err := a_channel.Confirm(false); err != nil { | ||
log.Fatalf("faild to switch connection channel to confirm mode: %s", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return err instead of calling fatal so you get central err handling in main/run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: typo faild |
||
} | ||
|
||
return channel | ||
} | ||
|
||
func (channel *Channel) publish(exchange string, headers amqp.Table, bytes []byte) (chan amqp.Confirmation, error) { | ||
channel.mutex.Lock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This basically means that every message published will lock the particular channel, i.e. serialize the logic and constrain parallelism to the number of defined channels, correct? |
||
defer channel.mutex.Unlock() | ||
|
||
cchan := make(chan amqp.Confirmation, 1) | ||
|
||
channel.confirmsMap.Store(channel.counter, cchan) | ||
|
||
if err := channel.channel.Publish( | ||
exchange, | ||
"", // routing key | ||
false, // mandatory | ||
false, // immediate | ||
amqp.Publishing{ | ||
Headers: headers, | ||
ContentType: "application/json", | ||
Body: bytes, | ||
}); err != nil { | ||
channel.confirmsMap.LoadAndDelete(channel.counter) | ||
return nil, err | ||
} | ||
|
||
channel.counter++ | ||
|
||
return cchan, nil | ||
} | ||
|
||
func (channel *Channel) confirmsHandler() func() error { | ||
return func() error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need to make this an anonymous function, just have this be the signature of |
||
for confirm := range channel.pconfirms_chan { | ||
channel.handleConfirm(confirm) | ||
} | ||
return nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a log debug line could not harm here to understand that the chan was closed, e.g. due to underlying RMQ channel closed |
||
} | ||
} | ||
|
||
func (channel *Channel) handleConfirm(confirm amqp.Confirmation) { | ||
// NOTE: current golang driver resequences confirms and sends them one-by-one | ||
value, present := channel.confirmsMap.LoadAndDelete(confirm.DeliveryTag) | ||
|
||
if !present { | ||
// really die? | ||
log.Fatalf("Received confirm for unknown send. DeliveryTag: %d", confirm.DeliveryTag) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would |
||
} | ||
|
||
ch := value.(chan amqp.Confirmation) | ||
|
||
ch <- confirm | ||
} | ||
|
||
func (channel *Channel) Close() { | ||
// should I also stop confirmHandler here? | ||
// mb sending kill switch to channel.pconfirms_chan? | ||
channel.channel.Close() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,29 @@ | ||
/* | ||
Copyright 2020 The Knative Authors | ||
Copyright 2020 The Knative Authors | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove these whitespace changes |
||
|
||
Licensed 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 | ||
Licensed 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 | ||
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. | ||
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. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"sync/atomic" | ||
|
||
cloudevents "github.com/cloudevents/sdk-go/v2" | ||
"github.com/cloudevents/sdk-go/v2/binding" | ||
|
@@ -36,34 +38,44 @@ import ( | |
const ( | ||
defaultMaxIdleConnections = 1000 | ||
defaultMaxIdleConnectionsPerHost = 1000 | ||
defaultMaxAmqpChannels = 10 | ||
) | ||
|
||
type envConfig struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can we create a separate ingress server object and use |
||
Port int `envconfig:"PORT" default:"8080"` | ||
BrokerURL string `envconfig:"BROKER_URL" required:"true"` | ||
ExchangeName string `envconfig:"EXCHANGE_NAME" required:"true"` | ||
|
||
channel *amqp.Channel | ||
logger *zap.SugaredLogger | ||
conn *amqp.Connection | ||
channels []*Channel | ||
logger *zap.SugaredLogger | ||
|
||
rc uint64 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These variable names could be a little more clear |
||
cc int | ||
} | ||
|
||
func main() { | ||
var env envConfig | ||
|
||
var err error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
|
||
if err := envconfig.Process("", &env); err != nil { | ||
log.Fatal("Failed to process env var", zap.Error(err)) | ||
} | ||
|
||
conn, err := amqp.Dial(env.BrokerURL) | ||
env.conn, err = amqp.Dial(env.BrokerURL) | ||
|
||
if err != nil { | ||
log.Fatalf("failed to connect to RabbitMQ: %s", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd use the pkg logger (move its construction up so you can use it) and thus allow for structured logging (see also other places where we still use log.) |
||
} | ||
defer conn.Close() | ||
defer env.conn.Close() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd at least log the |
||
|
||
env.channel, err = conn.Channel() | ||
if err != nil { | ||
log.Fatalf("failed to open a channel: %s", err) | ||
} | ||
defer env.channel.Close() | ||
// TODO: get it from annotation | ||
env.cc = defaultMaxAmqpChannels | ||
|
||
env.openAmqpChannels() | ||
|
||
defer env.closeAmqpChannels() | ||
|
||
env.logger = logging.FromContext(context.Background()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could use |
||
|
||
|
@@ -114,6 +126,7 @@ func (env *envConfig) ServeHTTP(writer http.ResponseWriter, request *http.Reques | |
return | ||
} | ||
|
||
// send to RabbitMQ | ||
statusCode, err := env.send(event) | ||
if err != nil { | ||
env.logger.Error("failed to send event,", err) | ||
|
@@ -134,17 +147,40 @@ func (env *envConfig) send(event *cloudevents.Event) (int, error) { | |
for key, val := range event.Extensions() { | ||
headers[key] = val | ||
} | ||
if err := env.channel.Publish( | ||
env.ExchangeName, | ||
"", // routing key | ||
false, // mandatory | ||
false, // immediate | ||
amqp.Publishing{ | ||
Headers: headers, | ||
ContentType: "application/json", | ||
Body: bytes, | ||
}); err != nil { | ||
|
||
ch, err := env.sendMessage(headers, bytes) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: confirmCh |
||
|
||
if err != nil { | ||
return http.StatusInternalServerError, fmt.Errorf("failed to publish message") | ||
} | ||
return http.StatusAccepted, nil | ||
|
||
confirmation := <-ch | ||
|
||
if confirmation.Ack { | ||
return http.StatusAccepted, nil | ||
} else { | ||
return http.StatusServiceUnavailable, errors.New("message was not confirmed") | ||
} | ||
} | ||
|
||
func (env *envConfig) sendMessage(headers amqp.Table, bytes []byte) (chan amqp.Confirmation, error) { | ||
crc := atomic.AddUint64(&env.rc, 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like this means the first message will bump counter to 1 and hence env.channels[0] would never be used? if so, you might change your slice indexing in line 175 accordingly |
||
|
||
channel := env.channels[crc%uint64(env.cc)] | ||
Comment on lines
+167
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use a Go channel and have all these Channels select on it rather than trying to do this load balancing logic |
||
|
||
return channel.publish(env.ExchangeName, headers, bytes) | ||
} | ||
|
||
func (env *envConfig) openAmqpChannels() { | ||
env.channels = make([]*Channel, env.cc) | ||
|
||
for i := 0; i < env.cc; i++ { | ||
env.channels[i] = openChannel(env.conn) | ||
} | ||
} | ||
|
||
func (env *envConfig) closeAmqpChannels() { | ||
for i := 0; i < len(env.channels); i++ { | ||
env.channels[i].Close() | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: not go variable naming style. use aChan or amqpChan for example