Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
20 changes: 20 additions & 0 deletions sdk/eventhub/event-hubs/review/event-hubs.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ export class EventPosition {
sequenceNumber?: number;
}

// @public
export class EventProcessor {
// Warning: (ae-forgotten-export) The symbol "PartitionProcessorFactory" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "PartitionManager" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "EventProcessorOptions" needs to be exported by the entry point index.d.ts
constructor(consumerGroupName: string, eventHubClient: EventHubClient, partitionProcessorFactory: PartitionProcessorFactory, partitionManager: PartitionManager, options?: EventProcessorOptions);
start(): Promise<void>;
stop(): Promise<void>;
}

export { MessagingError }

// @public
Expand All @@ -154,6 +164,16 @@ export type OnError = (error: MessagingError | Error) => void;
// @public
export type OnMessage = (eventData: ReceivedEventData) => void;

// @public
export interface PartitionContext {
// (undocumented)
readonly consumerGroupName: string;
// (undocumented)
readonly eventHubName: string;
// (undocumented)
readonly partitionId: string;
}

// @public
export interface PartitionProperties {
beginningSequenceNumber: number;
Expand Down
62 changes: 62 additions & 0 deletions sdk/eventhub/event-hubs/samples/eventProcessorHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
EventHubClient,
EventData,
EventPosition,
delay,
EventProcessor,
PartitionContext
} from "@azure/event-hubs";

class EventProcessorHost {
async processEvents(events: EventData[]) {
for (const event of events) {
console.log("Received event", event.body);
}
}

async processError(error: Error) {
console.log(`Encountered an error: ${error.message}`);
}

async initialize() {
console.log(`Started processing`);
}

async close() {
console.log(`Stopped processing`);
}
}

// Define connection string and related Event Hubs entity name here
const connectionString = "";
const eventHubName = "";

async function main() {
const client = new EventHubClient(connectionString, eventHubName);

const eventProcessorFactory = (context: PartitionContext) => {
return new EventProcessorHost();
};

const eph = new EventProcessor(
"$Default",
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
client,
eventProcessorFactory,
"partitionManager" as any,
{
initialEventPosition: EventPosition.earliest(),
maxBatchSize: 10,
maxWaitTime: 20
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
}
);
await eph.start();
// after 2 seconds, stop processing
await delay(2000);

await eph.stop();
await client.close();
}

main().catch((err) => {
console.log("Error occurred: ", err);
});
42 changes: 39 additions & 3 deletions sdk/eventhub/event-hubs/src/eventProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EventPosition } from "./eventPosition";
import { PartitionContext } from "./partitionContext";
import { CheckpointManager, Checkpoint } from "./checkpointManager";
import { EventData } from "./eventData";
import { PartitionPump } from "./partitionPump";

export interface PartitionProcessor {
/**
Expand Down Expand Up @@ -74,13 +75,26 @@ export interface EventProcessorOptions {
* @class EventProcessorHost
*/
export class EventProcessor {
private _consumerGroupName: string;
private _eventHubClient: EventHubClient;
private _partitionProcessorFactory: PartitionProcessorFactory;
private _processorOptions: EventProcessorOptions;
private _partitionPump: PartitionPump | undefined;
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated

constructor(
consumerGroupName: string,
eventHubClient: EventHubClient,
partitionProcessorFactory: PartitionProcessorFactory,
partitionManager: PartitionManager,
options?: EventProcessorOptions
) {}
) {
if (!options) options = {};

this._consumerGroupName = consumerGroupName;
this._eventHubClient = eventHubClient;
this._partitionProcessorFactory = partitionProcessorFactory;
this._processorOptions = options;
}

/**
* Starts the event processor, fetching the list of partitions, and attempting to grab leases
Expand All @@ -89,11 +103,33 @@ export class EventProcessor {
*
* @return {Promise<void>}
*/
async start(): Promise<void> {}
async start(): Promise<void> {
const partitionIds = await this._eventHubClient.getPartitionIds();
const partitionContext: PartitionContext = {
partitionId: partitionIds[0],
consumerGroupName: this._consumerGroupName,
eventHubName: this._eventHubClient.eventHubName
};
const partitionProcessor = this._partitionProcessorFactory(
partitionContext,
"checkpointManager" as any
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
);
this._partitionPump = new PartitionPump(
this._eventHubClient,
partitionContext,
partitionProcessor,
this._processorOptions
);
this._partitionPump.start(partitionIds[0]);
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
}

/**
* Stops the EventProcessor from processing messages.
* @return {Promise<void>}
*/
async stop(): Promise<void> {}
async stop(): Promise<void> {
if (this._partitionPump) {
this._partitionPump.stop();
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
}
}
}
2 changes: 2 additions & 0 deletions sdk/eventhub/event-hubs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export { PartitionProperties, EventHubProperties } from "./managementClient";
export { EventHubProducer } from "./sender";
export { EventHubConsumer, EventIteratorOptions } from "./receiver";
export { EventDataBatch } from "./eventDataBatch";
export { EventProcessor } from "./eventProcessor";
export { PartitionContext } from "./partitionContext";
export {
MessagingError,
DataTransformer,
Expand Down
5 changes: 5 additions & 0 deletions sdk/eventhub/event-hubs/src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ export const client = debugModule("azure:event-hubs:client");
* log statements for iothub client
*/
export const iotClient = debugModule("azure:event-hubs:iothubClient");
/**
* @ignore
* log statements for partitionManager
*/
export const partitionPump = debugModule("azure:event-hubs:partitionPump");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

73 changes: 73 additions & 0 deletions sdk/eventhub/event-hubs/src/partitionPump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as log from "./log";
import { EventProcessorOptions, PartitionProcessor } from "./eventProcessor";
import { PartitionContext } from "./partitionContext";
import { EventHubClient } from "./eventHubClient";
import { EventPosition } from "./eventPosition";
import { EventHubConsumer } from "./receiver";

export class PartitionPump {
private _partitionContext: PartitionContext;
private _eventHubClient: EventHubClient;
private _partitionProcessor: PartitionProcessor;
private _processorOptions: EventProcessorOptions;
private _receiver: EventHubConsumer | undefined;
private _isReceiving: boolean = false;

constructor(
eventHubClient: EventHubClient,
partitionContext: PartitionContext,
partitionProcessor: PartitionProcessor,
options?: EventProcessorOptions
) {
if (!options) options = {};
this._eventHubClient = eventHubClient;
this._partitionContext = partitionContext;
this._partitionProcessor = partitionProcessor;
this._processorOptions = options;
}

async start(partitionId: string): Promise<void> {
await this._partitionProcessor.initialize!();
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
await this._receiveEvents(partitionId);
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
log.partitionPump("Successfully started the receiver.");
}

private async _receiveEvents(partitionId: string): Promise<void> {
this._isReceiving = true;
this._receiver = await this._eventHubClient.createConsumer(
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
this._partitionContext.consumerGroupName,
partitionId,
this._processorOptions.initialEventPosition || EventPosition.earliest()
);
try {
while (this._isReceiving) {
const receivedEvents = await this._receiver.receiveBatch(
this._processorOptions.maxBatchSize || 1,
this._processorOptions.maxWaitTime
);
await this._partitionProcessor.processEvents(receivedEvents);
}
} catch (err) {
this._isReceiving = false;
this._receiver.close();
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
await this._partitionProcessor.processError(err);
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
log.partitionPump("An error occurred while receiving events.", err);
}
}

async stop(): Promise<void> {
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
this._isReceiving = false;
try {
if (this._receiver) {
this._receiver.close();

@chradek chradek Jul 29, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is your intent to kick off the close and then immediately go to the customer's close method?

If so, we should also add a .catch to close() so we swallow any errors encountered with that. Otherwise we might get an UnhandledPromiseRejectionWarning.

One thing I think we'll have to spend more time on is guaranteeing we close the underlying AMQP link. I think right now if we were to quickly call start and then stop, we might try closing the AMQP link while it's being initialized and end up with a link we can't close.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah..I'll test that scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If each partitionPump had an internal abortController, it could pass the abortSignal to the receiveBatch call, and call abort when stop is called. I think that would ensure the link is closed. This would mean we also need to avoid calling the processError handler if we stopped receiving messages so the AbortError doesn't get forwarded.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

One thing I think we'll have to spend more time on is guaranteeing we close the underlying AMQP link. I think right now if we were to quickly call start and then stop, we might try closing the AMQP link while it's being initialized and end up with a link we can't close.

This is the generic issue, I think we should handle this scenario inside close().

For now I'll pass the abortSignal to the receiveBatch call, and call abort when stop is called.

}
await this._partitionProcessor.close!("Stopped processing");
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
} catch (err) {
log.partitionPump("An error occurred while closing the receiver.", err);
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
throw err;
}
}
}
100 changes: 100 additions & 0 deletions sdk/eventhub/event-hubs/test/eventProcessorHost.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Comment thread
ShivangiReja marked this conversation as resolved.
// Licensed under the MIT License.

import chai from "chai";
const should = chai.should();
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import debugModule from "debug";
const debug = debugModule("azure:event-hubs:partitionPump");
import {
EventPosition,
EventHubClient,
EventData,
EventProcessor,
PartitionContext,
delay
} from "../src";
import { EnvVarKeys, getEnvVars } from "./utils/testUtils";
const env = getEnvVars();

describe("Event processor Host", function(): void {
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
const service = {
connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING],
path: env[EnvVarKeys.EVENTHUB_NAME]
};
const client: EventHubClient = new EventHubClient(service.connectionString, service.path);
before("validate environment", async function(): Promise<void> {
should.exist(
env[EnvVarKeys.EVENTHUB_CONNECTION_STRING],
"define EVENTHUB_CONNECTION_STRING in your environment before running integration tests."
);
should.exist(
env[EnvVarKeys.EVENTHUB_NAME],
"define EVENTHUB_NAME in your environment before running integration tests."
);
});

after("close the connection", async function(): Promise<void> {
await client.close();
});

describe("Partition processor", function(): void {
const receivedEvents: EventData[] = [];
let isinitializeCalled = false;
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
let isCloseCalled = false;
class TestEventProcessor {
async initialize() {
isinitializeCalled = true;
debug(`Started processing`);
}
async processEvents(events: EventData[]) {
for (const event of events) {
receivedEvents.push(event);
debug("Received event", event.body);
}
}

async processError(error: Error) {
debug(`Encountered an error: ${error.message}`);
}

async close() {
isCloseCalled = true;
debug(`Stopped processing`);
}
}
const eventProcessorFactory = (context: PartitionContext) => {
return new TestEventProcessor();
};

it("should call methods on a PartitionProcessor ", async function(): Promise<void> {
const partitionInfo = await client.getPartitionProperties("0");
const eph = new EventProcessor(
"$Default",
client,
eventProcessorFactory,
"partitionManager" as any,
{
initialEventPosition: EventPosition.fromSequenceNumber(
partitionInfo.lastEnqueuedSequenceNumber
),
maxBatchSize: 1,
maxWaitTime: 5
}
);
const producer = client.createProducer({ partitionId: "0" });
await producer.send({ body: "Hello world!!!" });

await eph.start();
// after 10 seconds, stop processing
await delay(1000);
Comment thread
ShivangiReja marked this conversation as resolved.
Outdated
await eph.stop();
await producer.close();
isinitializeCalled.should.equal(true);
receivedEvents.length.should.equal(1);
receivedEvents[0].body.should.equal("Hello world!!!");
isCloseCalled.should.equal(true);
});
});
}).timeout(90000);