Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions examples/AdvancedProducer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
Expand Down Expand Up @@ -36,14 +37,8 @@ public static void Main(string[] args)

var config = new Dictionary<string, object> { { "bootstrap.servers", brokerList } };

using (var producer = new Producer<string, string>(config))
using (var producer = new Producer<string, string>(config, new StringSerializer(Encoding.UTF8), new StringSerializer(Encoding.UTF8)))
{
// TODO: work out why explicit cast is needed here.
// TODO: remove need to explicitly specify string serializers - assume Utf8StringSerializer in Producer as default.
// TODO: allow be be set only in constructor. make readonly.
producer.KeySerializer = (ISerializer<string>)new Confluent.Kafka.Serialization.Utf8StringSerializer();
producer.ValueSerializer = producer.KeySerializer;

Console.WriteLine("\n-----------------------------------------------------------------------");
Console.WriteLine($"Producer {producer.Name} producing on topic {topicName}.");
Console.WriteLine("-----------------------------------------------------------------------");
Expand All @@ -68,7 +63,7 @@ public static void Main(string[] args)
{
text = Console.ReadLine();
}
catch
catch (IOException)
{
// IO exception is thrown when ConsoleCancelEventArgs.Cancel == true.
break;
Expand All @@ -82,10 +77,10 @@ public static void Main(string[] args)
if (index != -1)
{
key = text.Substring(0, index);
val = text.Substring(index);
val = text.Substring(index + 1);
}

Task<DeliveryReport> deliveryReport = producer.Produce(topicName, key, val);
Task<DeliveryReport> deliveryReport = producer.ProduceAsync(topicName, key, val);
var result = deliveryReport.Result; // synchronously waits for message to be produced.
Console.WriteLine($"Partition: {result.Partition}, Offset: {result.Offset}");
}
Expand Down
83 changes: 36 additions & 47 deletions examples/Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Confluent.Kafka.Serialization;


namespace Confluent.Kafka.Benchmark
{
public class Program
{
public class DeliveryHandler : IDeliveryHandler
public class BenchmarkProducer
{
public void SetException(Exception exception)
public static void Run(string broker, string topicName, int numberOfMessagesToProduce, int numberOfTests)
{
throw exception;
}

public void SetResult(DeliveryReport deliveryReport)
{
}
}

public static void Produce(string broker, string topicName, long numMessages)
{
var deliveryHandler = new DeliveryHandler();

var config = new Dictionary<string, object> { { "bootstrap.servers", broker } };

using (var producer = new Producer<Null, byte[]>(config))
{
// TODO: remove need to explicitly specify this serializer.
producer.ValueSerializer = (ISerializer<byte[]>)new ByteArraySerializer();
// mirrors the librdkafka performance test example.
var config = new Dictionary<string, object>
{
{ "bootstrap.servers", broker },
{ "queue.buffering.max.messages", 500000 },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for setting these properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"mirrors the librdkafka performance test example.". Was trying to get something directly comparable to your numbers.

{ "message.send.max.retries", 3 },
{ "retry.backoff.ms", 500 },
{ "session.timeout.ms", 6000 }
};

Console.WriteLine($"{producer.Name} producing on {topicName}");
// TODO: think more about exactly what we want to benchmark.
var payload = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
for (int i = 0; i < numMessages; i++)
using (var producer = new Producer(config))
{
producer.ProduceWithDeliveryReport(topicName, payload, deliveryHandler);
for (var j=0; j<numberOfTests; ++j)
{
Console.WriteLine($"{producer.Name} producing on {topicName}");

byte cnt = 0;
var val = new byte[100].Select(a => ++cnt).ToArray();

var startTime = DateTime.Now.Ticks;
var tasks = new Task[numberOfMessagesToProduce];
for (int i = 0; i < numberOfMessagesToProduce; i++)
{
tasks[i] = producer.ProduceAsync(topicName, null, val);
}
Task.WaitAll(tasks);
var duration = DateTime.Now.Ticks - startTime;

Console.WriteLine($"Produced {numberOfMessagesToProduce} in {duration/10000.0:F0}ms");
Console.WriteLine($"{numberOfMessagesToProduce / (duration/10000.0):F0} messages/ms");
}

Console.WriteLine("Disposing producer");
}

Console.WriteLine("Shutting down");
}
}

// TODO: Update Consumer benchmark for new Consumer when it's written.
public static async Task<long> Consume(string broker, string topic)
{
long n = 0;
Expand Down Expand Up @@ -86,24 +92,7 @@ public static void Main(string[] args)
string brokerList = args[0];
string topic = args[1];

long numMessages = 1000000;

var stopwatch = new Stopwatch();

// TODO: we really want time from first ack. as it is, this includes producer startup time.
stopwatch.Start();
Produce(brokerList, topic, numMessages);
stopwatch.Stop();

Console.WriteLine($"Sent {numMessages} messages in {stopwatch.Elapsed}");
Console.WriteLine($"{numMessages / stopwatch.Elapsed.TotalSeconds:F0} messages/second");

stopwatch.Restart();
long n = Consume(brokerList, topic).Result;
stopwatch.Stop();

Console.WriteLine($"Received {n} messages in {stopwatch.Elapsed}");
Console.WriteLine($"{n / stopwatch.Elapsed.TotalSeconds:F0} messages/second");
BenchmarkProducer.Run(brokerList, topic, 5000000, 4);
}
}
}
6 changes: 2 additions & 4 deletions examples/Misc/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ static async Task ListGroups(string brokerList)
{
var config = new Dictionary<string, object> { { "bootstrap.servers", brokerList } };

using (var producer = new Producer<Null, Null>(config))
using (var producer = new Producer(config))
{
var groups = await producer.ListGroups(TimeSpan.FromSeconds(10));
Console.WriteLine($"Consumer Groups:");
Expand All @@ -27,9 +27,7 @@ static async Task ListGroups(string brokerList)
{
Console.WriteLine($" {m.MemberId} {m.ClientId} {m.ClientHost}");
Console.WriteLine($" Metadata: {m.MemberMetadata.Length} bytes");
//Console.WriteLine(System.Text.Encoding.UTF8.GetString(m.MemberMetadata));
Console.WriteLine($" Assignment: {m.MemberAssignment.Length} bytes");
//Console.WriteLine(System.Text.Encoding.UTF8.GetString(m.MemberAssignment));
}
}
}
Expand All @@ -38,7 +36,7 @@ static async Task ListGroups(string brokerList)
static async Task PrintMetadata(string brokerList)
{
var config = new Dictionary<string, object> { { "bootstrap.servers", brokerList } };
using (var producer = new Producer<Null, Null>(config))
using (var producer = new Producer(config))
{
var meta = await producer.Metadata();
Console.WriteLine($"{meta.OriginatingBrokerId} {meta.OriginatingBrokerName}");
Expand Down
12 changes: 6 additions & 6 deletions examples/SimpleProducer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Confluent.Kafka.Serialization;
Expand All @@ -14,23 +15,22 @@ public static void Main(string[] args)

var config = new Dictionary<string, object> { { "bootstrap.servers", brokerList } };

using (var producer = new Producer<Null, string>(config))
using (var producer = new Producer<Null, string>(config, new NullSerializer(), new StringSerializer(Encoding.UTF8)))
{
// TODO: figure out why the cast below is necessary and how to avoid it.
// TODO: There should be no need to specify a serializer for common types like string - I think it should default to the UTF8 serializer.
producer.ValueSerializer = (ISerializer<string>)new Confluent.Kafka.Serialization.Utf8StringSerializer();

Console.WriteLine($"{producer.Name} producing on {topicName}. q to exit.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ctrl-c baby, not "q".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided I disagree. The purpose of these examples is to demonstrate usage of the client in a straightforward, easy to understand way. Turns out that detecting Ctrl-C is a bit convoluted, in fact there are as many lines dedicated to doing this properly as demonstrating the producer. None of the code is rocket science of course, so i'm sort of indifferent, but on balance, I think using q to exit is better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might seem like a tiny nitpick thing, but there are two proper reasons:

  • examples should be correct, even if for unrelated stuff, people will base their own code on this.
  • out-of-band cancellation shows an interesting problem: how do I break out of the consume loop. If we can't show people how to do that in an effective and correct manner they will get it wrong and that will bite us back.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is AppDomain.ProcessExit not a workable solution that will be relatively small?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding #1 - i wouldn't call using 'q' to exit 'incorrect' as such.
regarding #2 - you've convinced me it's useful. People won't be using Console.ReadLine, but many will be making console apps, and the CancelKeyPress handler is the way to detect Ctrl-C.

how about i leave it in the advanced producer example and keep it out of the simple producer example - keep that as dead simple as possible - i want the first example people look at to be inviting and not scary in any way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ewencp AppDomain is changed a lot or doesn't exist in .net core. http://www.michael-whelan.net/replacing-appdomain-in-dotnet-core/

The new assembly unload event mentioned in the above article is not effective at catching Ctrl-C. Examples I see around the web use Console.CancelKeyPress. I'm not certain there is not a better way, but it seems likely CancelKeyPress is good.

Another reason not to include this: there are higher priorities than figuring this out.


string text;
while ((text = Console.ReadLine()) != "q")
{
Task<DeliveryReport> deliveryReport = producer.Produce(topicName, text);
Task<DeliveryReport> deliveryReport = producer.ProduceAsync(topicName, null, text);
var unused = deliveryReport.ContinueWith(task =>
{
Console.WriteLine($"Partition: {task.Result.Partition}, Offset: {task.Result.Offset}");
});
}

// Tasks are not waited on, it's possible they may still in progress here.
producer.Flush();
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions examples/Wrapped/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Text;
using System.Collections.Generic;
using Confluent.Kafka.Serialization;

namespace Confluent.Kafka.Wrapped
{
/// <summary>
/// An example showing how to wrap a single Producer to produce messages using
/// different serializers.
/// </summary>
/// <remarks>
/// If you only want to use a single pair of serializers in your application,
/// you should use the Producer&lt;TKey, TValue&gt; constructor instead.
/// </remarks>
public class Program
{
public static void Main(string[] args)
{
var config = new Dictionary<string, object> { { "bootstrap.servers", args[0] } };

using (var producer = new Producer(config))
{
// sProducer1 is a lightweight wrapper around a Producer instance that adds
// (string, string) serialization. Note that sProducer1 does not need to be
// (and cannot be) disposed.
var sProducer1 = producer.Wrap<string, string>(new StringSerializer(Encoding.UTF8), new StringSerializer(Encoding.UTF8));

// sProducer2 is another lightweight wrapper around kafkaProducer that adds
// (null, int) serialization. When you do not wish to write any data to a key
// or value, the Null type should be used.
var sProducer2 = producer.Wrap<Null, int>(new NullSerializer(), new IntSerializer());

// write (string, string) data to topic "first-topic", statically type checked.
sProducer1.ProduceAsync("first-topic", "my-key-value", "my-value");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i forgot to wait on these tasks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere, ideally this could just be a Flush() on the unwrapped producer.

For that matter, this does raise the question of how "aggregate" operations behave for the wrapper producers. i.e. would Flush() in any way isolate itself to messages for the single format (presumably no, given the way this is implemented)? How about things like Dispose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want Flush (though I'm not 100% sure i'm seeing the world correctly here). I think we just want to wait on the tasks, or a collection of tasks.

If we have a flush method, it doesn't make sense to put it on ISerializingProducer. It'd be on the concrete Producer and Producer<TKey, TValue> only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispose method of Producer effectively flushes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to have en explicit Flush() method, and not to do an implicit Flush() when disposing, to allow applications to exit quickly without waiting for message transmission (which may block for a long time)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about not wanting to flush in Dispose... shutdown is not the only consideration - a using statement will typically be used to wrap a producer and this is equivalent to try / finally. We probably don't want messages being flushed before an exception gets handled.

I guess Flush is a good thing to have in addition to the Tasks, as some people will probably just want to fire and forget and ignore the Tasks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand not flushing in .Dispose makes the API more error prone as users will often be in a situation where there may be messages in flight when they want to exit because all the calls are async. So, calling Flush will usually be an appropriate thing to do right at the end of the Producer using block.

Also, if an exception makes it outside the Producer scope, there is no reference to the producer, so no option to Flush.

Thoughts on having a property .FlushOnDispose, which by default is true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the other clients does an implicit flush on dispose, so I dont think we should alter that behaviour in this client.

Also note that flush, and thus dispose, might block for up to message.timeout.ms which defaults to 5 minutes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree.

I think I'm only still thinking about flush on dispose because that is the existing behavior of rdkafka-dotnet (and there are some positives to the idea).

But now I'm seeing it from a different point of view - it's not normal to wait a long time on dispose, so it's a counter intuitive thing to do.

I'll work out something similar to the other clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as indication that it's counter intuitive to flush in dispose, see my first comment in this thread "oh, i forgot to wait on these tasks." - this was before I started thinking about what was happening in the dispose method and before people started suggesting a flush method. my intuition then was dispose was not going to wait on anything.


// write (null, int) data to topic "second-data". statically type checked, using
// the same underlying producer as the producer1.
sProducer2.ProduceAsync("second-topic", null, 42);

// producers are NOT tied to topics. Although it's unusual that you might want to
// do so, you can use different serializing producers to write to the same topic.
sProducer2.ProduceAsync("first-topic", null, 107);

// ProducerAsync tasks are not waited on - there is a good chance they are still
// in flight.
producer.Flush();
}
}
}
}
25 changes: 25 additions & 0 deletions examples/Wrapped/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"version": "1.0.0",
"authors": ["Confluent Inc"],

"buildOptions": {
"emitEntryPoint": true
},

"dependencies": {
"Confluent.Kafka": {
"target": "project"
}
},

"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0"
}
}
}
}
}
17 changes: 17 additions & 0 deletions src/Confluent.Kafka/DeliveryReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Confluent.Kafka
{
/*
TODO: (via ewencp): The equivalent in the Python client fills in more information --
the callbacks accept (err, msg) parameters, where the latter is
http://docs.confluent.io/3.1.0/clients/confluent-kafka-python/index.html#confluent_kafka.Message
Same deal with Go where the DR channel gets one of these:
http://docs.confluent.io/3.1.0/clients/confluent-kafka-go/index.html#Message Is this being
kept more minimal intentionally?
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on Slack, the message metadata contains an ever growing number of fields, so passing a rich Message object to the delivery report , like the other clients, is most likely the best way forward.
And that Message object should be the same as returned by consumer.poll()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. however if a very common use case is to just produce a key and value (I think it is), then it's worth having a Producer.ProducerAsync overload for this as well to make the interface easier to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is hard to make assumptions on what information the dr callback needs based on the produce() arguments. We should provide whatever librdkafka provides in the dr.


public struct DeliveryReport
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equivalent in the Python client fills in more information -- the callbacks accept (err, msg) parameters, where the latter is http://docs.confluent.io/3.1.0/clients/confluent-kafka-python/index.html#confluent_kafka.Message Same deal with Go where the DR channel gets one of these: http://docs.confluent.io/3.1.0/clients/confluent-kafka-go/index.html#Message Is this being kept more minimal intentionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unchanged from rdkafka-dotnet.
If we include the message in the delivery report (given the precedent, we should), we're going to need to think about generics here too. I propose doing this in the next PR which is going to be a refactor of the Consumer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A delivery report will need at least:

  • topic
  • partition
  • offset
  • error
  • msg opaque (or bound variable through other means)

Other stuff that might be useful:

  • value-object
  • key-object
  • value
  • key
  • timestamp
  • future fields that the community makes up, e.g. headers

Wrapping this in a Message type is consistent with other clients.

{
public int Partition;
public long Offset;
}
}
16 changes: 10 additions & 6 deletions src/Confluent.Kafka/Handle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ internal void Init(RdKafkaType type, IntPtr config, Config.LogCallback logger)
callbackTask = StartCallbackTask(callbackCts.Token);
}

// TODO: Add timout parameter (with default option == block indefinitely) when use rd_kafka_flush.
public void Flush()
{
// TODO: use rd_kafka_flush here instead..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would've been less code calling rd_kafka_flush() than adding these comments ;)

while (OutQueueLength > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is actually a rd_kafka_flush() call that should beused.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I checked and this wasn't exposed in the LibRdKafka layer yet whereas the OutQueueLength stuff was. Agreed that we should use the correct internal version though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. I was just doing whatever ah- was here. will change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll put a todo. I want to prioritize getting the high level API right, and i'm going to be reviewing / addressing a lot more lower level stuff in future PRs

{
handle.Poll((IntPtr) 100);
}
}

public void Dispose()
{
Dispose(true);
Expand All @@ -83,12 +93,6 @@ protected virtual void Dispose(bool disposing)

if (disposing)
{
// Wait until all outstanding sends have completed.
while (OutQueueLength > 0)
{
handle.Poll((IntPtr) 100);
}

handle.Dispose();
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/Confluent.Kafka/ISerializingProducer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Confluent.Kafka.Serialization;


namespace Confluent.Kafka
{
public interface ISerializingProducer<TKey, TValue>
{
string Name { get; }

ISerializer<TKey> KeySerializer { get; }

ISerializer<TValue> ValueSerializer { get; }

Task<DeliveryReport> ProduceAsync(string topic, TKey key, TValue val, int? partition = null, bool blockIfQueueFull = true);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this plethora of variants?
What about an easy one for most use-cases:
Produce(topic, key, val, deliveryHandler)

And then the full one:
Produce(topic, key, val, deliveryHandler, partition, block, timestamp, ..)

Or even better:

Produce(Message)

where Message is a class that we expand as necessary when new functionality (e.g. timestamp, soon headers) are added

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking we should get rid of the val only variant because it's not common enough to justify. @ewencp please agree/disagree. The reason this variant existed is i'm bringing forward the capability from rdkafka-dotnet.

So now i only have one ProduceAsync variant on the typed producer (takes TKey, TValue)

I wanted to avoid forcing people to do new Message(...), but you bring up a good point about extensibility.

However, I propose keeping things the way they are and not introducing a Message class/struct now. I think in the future it will still be the most common use case to send (key, value) an not set timestamp / header explicitly - so it would be nice to have this short hand way of doing that.

In the future, we introduce a new overload that takes a Message<K,V> rather than T, V and add timestamp, headers etc into that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sort of makes sense, but timestamp is available today already so it should probably be supported.
And what about partition?

}
11 changes: 6 additions & 5 deletions src/Confluent.Kafka/Impl/LibRdKafka.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,14 @@ internal static ErrorCode committed(IntPtr rk, IntPtr partitions, IntPtr timeout
internal static ErrorCode position(IntPtr rk, IntPtr partitions)
=> _position(rk, partitions);

private static Func<IntPtr, int, IntPtr, byte[], UIntPtr, byte[], UIntPtr,
private static Func<IntPtr, int, IntPtr, IntPtr, UIntPtr, IntPtr, UIntPtr,
IntPtr, IntPtr> _produce;
internal static IntPtr produce(
IntPtr rkt,
int partition,
IntPtr msgflags,
byte[] val, UIntPtr len,
byte[] key, UIntPtr keylen,
IntPtr val, UIntPtr len,
IntPtr key, UIntPtr keylen,
IntPtr msg_opaque)
=> _produce(rkt, partition, msgflags, val, len, key, keylen, msg_opaque);

Expand Down Expand Up @@ -607,8 +607,8 @@ internal static extern IntPtr rd_kafka_produce(
IntPtr rkt,
int partition,
IntPtr msgflags,
byte[] val, UIntPtr len,
byte[] key, UIntPtr keylen,
IntPtr val, UIntPtr len,
IntPtr key, UIntPtr keylen,
IntPtr msg_opaque);

[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
Expand Down Expand Up @@ -644,5 +644,6 @@ internal static extern IntPtr rd_kafka_brokers_add(IntPtr rk,
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr rd_kafka_wait_destroyed(IntPtr timeout_ms);
}

}
}
Loading