blob: 66a82ccad1f02a4634cc779ee39e864673544959 [file] [log] [blame]
// 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.
package pulsar
import (
"context"
"errors"
"fmt"
"testing"
"time"
pb "github.com/apache/pulsar-client-go/pulsar/internal/pulsar_proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTCClient(t *testing.T) {
//1. Prepare: create PulsarClient and init transaction coordinator client.
topic := newTopicName()
sub := "my-sub"
tc, client := createTcClient(t)
//2. Prepare: create Topic and Subscription.
consumer, err := client.Subscribe(ConsumerOptions{
Topic: topic,
SubscriptionName: sub,
})
assert.NoError(t, err)
//3. Test newTransaction, addSubscriptionToTxn, addPublishPartitionToTxn
//Create a transaction1 and add subscription and publish topic to the transaction.
id1, err := tc.newTransaction(3 * time.Minute)
assert.NoError(t, err)
err = tc.addSubscriptionToTxn(id1, topic, sub)
assert.NoError(t, err)
err = tc.addPublishPartitionToTxn(id1, []string{topic})
assert.NoError(t, err)
//4. Verify the transaction1 stats
stats, err := transactionStats(id1)
assert.NoError(t, err)
assert.Equal(t, "OPEN", stats["status"])
producedPartitions := stats["producedPartitions"].(map[string]interface{})
ackedPartitions := stats["ackedPartitions"].(map[string]interface{})
_, ok := producedPartitions[topic]
assert.True(t, ok)
temp, ok := ackedPartitions[topic]
assert.True(t, ok)
subscriptions := temp.(map[string]interface{})
_, ok = subscriptions[sub]
assert.True(t, ok)
//5. Test End transaction
//Create transaction2 and Commit the transaction.
id2, err := tc.newTransaction(3 * time.Minute)
assert.NoError(t, err)
//6. Verify the transaction2 stats
stats2, err := transactionStats(id2)
assert.NoError(t, err)
assert.Equal(t, "OPEN", stats2["status"])
err = tc.endTxn(id2, pb.TxnAction_COMMIT)
assert.NoError(t, err)
stats2, err = transactionStats(id2)
//The transaction will be removed from txnMeta. Therefore, it is expected that stats2 is zero
if err == nil {
assert.Equal(t, "COMMITTED", stats2["status"])
} else {
assert.Equal(t, err.Error(), "http error status code: 404")
}
defer consumer.Close()
defer tc.close()
defer client.Close()
}
//Test points:
// 1. Abort and commit txn.
// 1. Do nothing, just open a transaction and then commit it or abort it.
// The operations of committing/aborting txn should success at the first time and fail at the second time.
// 2. The internal API, registerSendOrAckOp and endSendOrAckOp
// 1. Register 4 operation but only end 3 operations, the transaction can not be committed or aborted.
// 2. Register 3 operation and end 3 operation the transaction can be committed and aborted.
// 3. Register an operation and end the operation with an error,
// and then the state of the transaction should be replaced to errored.
// 3. The internal API, registerAckTopic and registerProducerTopic
// 1. Register ack topic and send topic, and call http request to get the stats of the transaction
// to do verification.
// TestTxnImplCommitOrAbort Test abort and commit txn
func TestTxnImplCommitOrAbort(t *testing.T) {
tc, _ := createTcClient(t)
//1. Open a transaction and then commit it.
//The operations of committing txn1 should success at the first time and fail at the second time.
txn1 := createTxn(tc, t)
err := txn1.Commit(context.Background())
require.Nil(t, err, fmt.Sprintf("Failed to commit the transaction %d:%d\n", txn1.txnID.MostSigBits,
txn1.txnID.LeastSigBits))
txn1.state = TxnOpen
txn1.opsFlow <- true
err = txn1.Commit(context.Background())
assert.Equal(t, err.(*Error).Result(), TransactionNoFoundError)
assert.Equal(t, txn1.GetState(), TxnError)
//2. Open a transaction and then abort it.
//The operations of aborting txn2 should success at the first time and fail at the second time.
id2, err := tc.newTransaction(time.Hour)
require.Nil(t, err, "Failed to new a transaction")
txn2 := newTransaction(*id2, tc, time.Hour)
err = txn2.Abort(context.Background())
require.Nil(t, err, fmt.Sprintf("Failed to abort the transaction %d:%d\n",
id2.MostSigBits, id2.LeastSigBits))
txn2.state = TxnOpen
txn2.opsFlow <- true
err = txn2.Abort(context.Background())
assert.Equal(t, err.(*Error).Result(), TransactionNoFoundError)
assert.Equal(t, txn1.GetState(), TxnError)
err = txn2.registerSendOrAckOp()
assert.Equal(t, err.(*Error).Result(), InvalidStatus)
err = txn1.registerSendOrAckOp()
assert.Equal(t, err.(*Error).Result(), InvalidStatus)
}
// TestRegisterOpAndEndOp Test the internal API including the registerSendOrAckOp and endSendOrAckOp.
func TestRegisterOpAndEndOp(t *testing.T) {
tc, _ := createTcClient(t)
//1. Register 4 operation but only end 3 operations, the transaction can not be committed or aborted.
res := registerOpAndEndOp(t, tc, 4, 3, nil, true)
assert.Equal(t, res.(*Error).Result(), TimeoutError)
res = registerOpAndEndOp(t, tc, 4, 3, nil, false)
assert.Equal(t, res.(*Error).Result(), TimeoutError)
//2. Register 3 operation and end 3 operation the transaction can be committed and aborted.
res = registerOpAndEndOp(t, tc, 3, 3, nil, true)
assert.Nil(t, res)
res = registerOpAndEndOp(t, tc, 3, 3, nil, false)
assert.Nil(t, res)
//3. Register an operation and end the operation with an error,
// and then the state of the transaction should be replaced to errored.
res = registerOpAndEndOp(t, tc, 4, 4, errors.New(""), true)
assert.Equal(t, res.(*Error).Result(), InvalidStatus)
res = registerOpAndEndOp(t, tc, 4, 4, errors.New(""), false)
assert.Equal(t, res.(*Error).Result(), InvalidStatus)
}
// TestRegisterTopic Test the internal API, registerAckTopic and registerProducerTopic
func TestRegisterTopic(t *testing.T) {
//1. Prepare: create PulsarClient and init transaction coordinator client.
topic := newTopicName()
sub := "my-sub"
tc, client := createTcClient(t)
//2. Prepare: create Topic and Subscription.
_, err := client.Subscribe(ConsumerOptions{
Topic: topic,
SubscriptionName: sub,
})
assert.NoError(t, err)
txn := createTxn(tc, t)
//3. Create a topic and register topic and subscription.
err = txn.registerAckTopic(topic, sub)
require.Nil(t, err, "Failed to register ack topic.")
err = txn.registerProducerTopic(topic)
require.Nil(t, err, "Failed to register ack topic.")
//4. Call http request to get the stats of the transaction to do verification.
stats2, err := transactionStats(&txn.txnID)
assert.NoError(t, err)
topics := stats2["producedPartitions"].(map[string]interface{})
subTopics := stats2["ackedPartitions"].(map[string]interface{})
assert.NotNil(t, topics[topic])
assert.NotNil(t, subTopics[topic])
subs := subTopics[topic].(map[string]interface{})
assert.NotNil(t, subs[sub])
}
func registerOpAndEndOp(t *testing.T, tc *transactionCoordinatorClient, rp int, ep int, err error, commit bool) error {
txn := createTxn(tc, t)
for i := 0; i < rp; i++ {
err := txn.registerSendOrAckOp()
assert.Nil(t, err)
}
for i := 0; i < ep; i++ {
txn.endSendOrAckOp(err)
}
if commit {
err = txn.Commit(context.Background())
} else {
err = txn.Abort(context.Background())
}
return err
}
func createTxn(tc *transactionCoordinatorClient, t *testing.T) *transaction {
id, err := tc.newTransaction(time.Hour)
require.Nil(t, err, "Failed to new a transaction.")
return newTransaction(*id, tc, time.Hour)
}
// createTcClient Create a transaction coordinator client to send request
func createTcClient(t *testing.T) (*transactionCoordinatorClient, *client) {
c, err := NewClient(ClientOptions{
URL: webServiceURLTLS,
TLSTrustCertsFilePath: caCertsPath,
Authentication: NewAuthenticationTLS(tlsClientCertPath, tlsClientKeyPath),
EnableTransaction: true,
})
require.Nil(t, err, "Failed to create client.")
return c.(*client).tcClient, c.(*client)
}
// TestConsumeAndProduceWithTxn is a test function that validates the behavior of producing and consuming
// messages with and without transactions. It consists of the following steps:
//
// 1. Prepare: Create a PulsarClient and initialize the transaction coordinator client.
// 2. Prepare: Create a topic and a subscription.
// 3. Produce 10 messages with a transaction and 10 messages without a transaction.
// - Expectation: The consumer should be able to receive the 10 messages sent without a transaction,
// but not the 10 messages sent with the transaction.
// 4. Commit the transaction and receive the remaining 10 messages.
// - Expectation: The consumer should be able to receive the 10 messages sent with the transaction.
// 5. Clean up: Close the consumer and producer instances.
//
// The test ensures that the consumer can only receive messages sent with a transaction after it is committed,
// and that it can always receive messages sent without a transaction.
func TestConsumeAndProduceWithTxn(t *testing.T) {
// Step 1: Prepare - Create PulsarClient and initialize the transaction coordinator client.
topic := newTopicName()
sub := "my-sub"
_, client := createTcClient(t)
// Step 2: Prepare - Create Topic and Subscription.
consumer, err := client.Subscribe(ConsumerOptions{
Topic: topic,
SubscriptionName: sub,
})
assert.NoError(t, err)
producer, _ := client.CreateProducer(ProducerOptions{
Topic: topic,
SendTimeout: 0,
})
// Step 3: Open a transaction, send 10 messages with the transaction and 10 messages without the transaction.
// Expectation: We can receive the 10 messages sent without a transaction and
// cannot receive the 10 messages sent with the transaction.
txn, err := client.NewTransaction(time.Hour)
require.Nil(t, err)
for i := 0; i < 10; i++ {
_, err = producer.Send(context.Background(), &ProducerMessage{
Payload: make([]byte, 1024),
})
require.Nil(t, err)
}
for i := 0; i < 10; i++ {
_, err := producer.Send(context.Background(), &ProducerMessage{
Transaction: txn,
Payload: make([]byte, 1024),
})
require.Nil(t, err)
}
// Attempt to receive and acknowledge the 10 messages sent without a transaction.
for i := 0; i < 10; i++ {
msg, _ := consumer.Receive(context.Background())
assert.NotNil(t, msg)
err = consumer.Ack(msg)
assert.Nil(t, err)
}
// Create a goroutine to attempt receiving a message and send it to the 'done1' channel.
done1 := make(chan Message)
go func() {
msg, _ := consumer.Receive(context.Background())
err := consumer.AckID(msg.ID())
require.Nil(t, err)
close(done1)
}()
// Expectation: The consumer should not receive uncommitted messages.
select {
case <-done1:
require.Fail(t, "The consumer should not receive uncommitted message")
case <-time.After(time.Second):
}
// Step 4: After committing the transaction, we should be able to receive the remaining 10 messages.
// Acknowledge the rest of the 10 messages with the transaction.
// Expectation: After committing the transaction, all messages of the subscription will be acknowledged.
_ = txn.Commit(context.Background())
txn, err = client.NewTransaction(time.Hour)
require.Nil(t, err)
for i := 0; i < 9; i++ {
msg, _ := consumer.Receive(context.Background())
require.NotNil(t, msg)
err = consumer.AckWithTxn(msg, txn)
require.Nil(t, err)
}
_ = txn.Commit(context.Background())
<-done1
consumer.Close()
consumer, _ = client.Subscribe(ConsumerOptions{
Topic: topic,
SubscriptionName: sub,
})
// Create a goroutine to attempt receiving a message and send it to the 'done1' channel.
done2 := make(chan Message)
go func() {
consumer.Receive(context.Background())
close(done2)
}()
// Expectation: The consumer should not receive uncommitted messages.
select {
case <-done2:
require.Fail(t, "The consumer should not receive messages")
case <-time.After(time.Second):
}
// Step 5: Clean up - Close the consumer and producer instances.
consumer.Close()
producer.Close()
}
func TestAckAndSendWithTxn(t *testing.T) {
// Prepare: Create PulsarClient and initialize the transaction coordinator client.
sourceTopic := newTopicName()
sinkTopic := newTopicName()
sub := "my-sub"
_, client := createTcClient(t)
// Prepare: Create source and sink topics and subscriptions.
sourceConsumer, _ := client.Subscribe(ConsumerOptions{
Topic: sourceTopic,
SubscriptionName: sub,
})
sourceProducer, _ := client.CreateProducer(ProducerOptions{
Topic: sourceTopic,
SendTimeout: 0,
})
sinkConsumer, _ := client.Subscribe(ConsumerOptions{
Topic: sinkTopic,
SubscriptionName: sub,
})
sinkProducer, _ := client.CreateProducer(ProducerOptions{
Topic: sinkTopic,
SendTimeout: 0,
})
// Produce 10 messages to the source topic.
for i := 0; i < 10; i++ {
_, err := sourceProducer.Send(context.Background(), &ProducerMessage{
Payload: make([]byte, 1024),
})
require.Nil(t, err)
}
// Open a transaction and consume messages from the source topic while sending messages to the sink topic.
txn, err := client.NewTransaction(time.Hour)
require.Nil(t, err)
for i := 0; i < 10; i++ {
msg, _ := sourceConsumer.Receive(context.Background())
require.NotNil(t, msg)
payload := msg.Payload()
_, err := sinkProducer.Send(context.Background(), &ProducerMessage{
Transaction: txn,
Payload: payload,
})
require.Nil(t, err)
err = sourceConsumer.AckWithTxn(msg, txn)
require.Nil(t, err)
}
// Commit the transaction.
_ = txn.Commit(context.Background())
// Verify that the messages are available in the sink topic.
for i := 0; i < 10; i++ {
msg, _ := sinkConsumer.Receive(context.Background())
require.NotNil(t, msg)
err = sinkConsumer.Ack(msg)
require.Nil(t, err)
}
// Clean up: Close consumers and producers.
sourceConsumer.Close()
sourceProducer.Close()
sinkConsumer.Close()
sinkProducer.Close()
}
func TestTransactionAbort(t *testing.T) {
// Prepare: Create PulsarClient and initialize the transaction coordinator client.
topic := newTopicName()
sub := "my-sub"
_, client := createTcClient(t)
// Prepare: Create Topic and Subscription.
consumer, _ := client.Subscribe(ConsumerOptions{
Topic: topic,
SubscriptionName: sub,
})
producer, _ := client.CreateProducer(ProducerOptions{
Topic: topic,
SendTimeout: 0,
})
// Open a transaction and send 10 messages with the transaction.
txn, err := client.NewTransaction(time.Hour)
require.Nil(t, err)
for i := 0; i < 10; i++ {
_, err = producer.Send(context.Background(), &ProducerMessage{
Transaction: txn,
Payload: make([]byte, 1024),
})
require.Nil(t, err)
}
// Abort the transaction.
_ = txn.Abort(context.Background())
// Expectation: The consumer should not receive any messages.
done := make(chan struct{})
go func() {
_, err := consumer.Receive(context.Background())
if err != nil {
close(done)
}
}()
select {
case <-done:
// The consumer should not receive any messages.
require.Fail(t, "The consumer should not receive any messages")
case <-time.After(time.Second):
}
// Clean up: Close the consumer and producer instances.
consumer.Close()
producer.Close()
}