├── .babelrc
├── .editorconfig
├── .eslintignore
├── .github
├── ISSUE_TEMPLATE
│ └── feature_request.md
├── penguin.png
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SUMMARY.md
├── clientConfig.ts
├── demo
├── .babelrc
├── .dockerignore
├── .eslintrc.json
├── .gitignore
├── Dockerfile
├── __tests__
│ └── backend
│ │ ├── strategy.ts
│ │ └── topic.ts
├── client
│ ├── App.tsx
│ ├── assets
│ │ ├── ausar.jpeg
│ │ ├── kushal.jpeg
│ │ ├── timeo.jpeg
│ │ └── ziyad.png
│ ├── clientConfig.ts
│ ├── components
│ │ ├── Error.tsx
│ │ ├── GettingStarted.tsx
│ │ ├── GlobalNavBar.tsx
│ │ ├── LandingBody.tsx
│ │ ├── Message.tsx
│ │ ├── ParticlesBackdrop.tsx
│ │ ├── TeamMember.tsx
│ │ └── Topic.tsx
│ ├── containers
│ │ ├── MainContainer.tsx
│ │ ├── MessageErrorContainer.tsx
│ │ ├── StrategyContainer.tsx
│ │ ├── TeamContainer.tsx
│ │ └── TopicsContainer.tsx
│ ├── context
│ │ ├── BackDropContext.tsx
│ │ ├── ErrorContext.tsx
│ │ ├── MessageContext.tsx
│ │ └── TopicContext.tsx
│ ├── index.html
│ ├── index.tsx
│ └── theme.ts
├── jest.config.js
├── package.json
├── server
│ ├── app.ts
│ ├── controllers
│ │ ├── DLQController.ts
│ │ ├── failfastController.ts
│ │ ├── ignoreController.ts
│ │ ├── kafkaController.ts
│ │ ├── strategyController.ts
│ │ └── topicsController.ts
│ ├── routes
│ │ ├── strategy.ts
│ │ └── topic.ts
│ └── server.ts
├── tsconfig.json
├── tsconfig.spec.json
└── webpack.config.js
├── exampleBasic.ts
├── exampleDLQConsumer.ts
├── exampleDLQProducer.ts
├── exampleFailFast.ts
├── exampleIgnoreConsumer.ts
├── exampleIgnoreProducer.ts
├── jest.config.js
├── jest.config.ts
├── kafka-penguin
├── LICENSE
├── README.md
├── babel.config.js
├── dist
│ ├── clientConfig.d.ts
│ ├── clientConfig.js
│ ├── index.d.ts
│ └── index.js
├── package.json
├── src
│ ├── clientConfig.ts
│ ├── deadletterqueue.test.ts
│ ├── failfast.test.ts
│ ├── ignore.test.ts
│ └── index.ts
└── tsconfig.json
├── settings.json
├── strategies
├── README.md
├── dead-letter-queue.md
├── failfast.md
└── strategies-ignore.md
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react",
5 | "@babel/preset-typescript"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/penguin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/kafka-penguin/d8983545c5169da31318e44792271adee9fcffe0/.github/penguin.png
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 |
2 | # Create name
3 | name: CI/CD
4 |
5 | # Trigger the workflow on push or pull request,
6 | # but only for the main branch
7 | on: [push, pull_request]
8 | # One main job: to run testing suite after every potential PR.
9 | # Enter series of terminal CMD's to test application
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: ./demo
16 | strategy:
17 | matrix:
18 | node-version: [12.x]
19 |
20 | steps:
21 | #Checkout the source code of our github repo
22 | - uses: actions/checkout@v2
23 | - name: Set up Node
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - name: npm install, build and test
28 | run: |
29 | npm install
30 | npm test
31 | env:
32 | KAFKA_BOOTSTRAP_SERVER: ${{secrets.KAFKA_BOOTSTRAP_SERVER}}
33 | KAFKA_USERNAME: ${{secrets.KAFKA_USERNAME}}
34 | KAFKA_PASSWORD: ${{secrets.KAFKA_PASSWORD}}
35 | #npm run build --if-present
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | package-lock.json
4 | .env
5 |
6 | demo/node_modules
7 | demo/.env
8 | demo/package-lock.json
9 | .DS_Store
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5"
4 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes new environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
17 | do not have permission to do that, you may request the second reviewer to merge it for you.
18 |
19 | ## Code of Conduct
20 |
21 | ### Our Pledge
22 |
23 | In the interest of fostering an open and welcoming environment, we as
24 | contributors and maintainers pledge to making participation in our project and
25 | our community a harassment-free experience for everyone, regardless of age, body
26 | size, disability, ethnicity, gender identity and expression, level of experience,
27 | nationality, personal appearance, race, religion, or sexual identity and
28 | orientation.
29 |
30 | ### Our Standards
31 |
32 | Examples of behavior that contributes to creating a positive environment
33 | include:
34 |
35 | * Using welcoming and inclusive language
36 | * Being respectful of differing viewpoints and experiences
37 | * Gracefully accepting constructive criticism
38 | * Focusing on what is best for the community
39 | * Showing empathy towards other community members
40 |
41 | Examples of unacceptable behavior by participants include:
42 |
43 | * The use of sexualized language or imagery and unwelcome sexual attention or
44 | advances
45 | * Trolling, insulting/derogatory comments, and personal or political attacks
46 | * Public or private harassment
47 | * Publishing others' private information, such as a physical or electronic
48 | address, without explicit permission
49 | * Other conduct which could reasonably be considered inappropriate in a
50 | professional setting
51 |
52 | ### Our Responsibilities
53 |
54 | Project maintainers are responsible for clarifying the standards of acceptable
55 | behavior and are expected to take appropriate and fair corrective action in
56 | response to any instances of unacceptable behavior.
57 |
58 | Project maintainers have the right and responsibility to remove, edit, or
59 | reject comments, commits, code, wiki edits, issues, and other contributions
60 | that are not aligned to this Code of Conduct, or to ban temporarily or
61 | permanently any contributor for other behaviors that they deem inappropriate,
62 | threatening, offensive, or harmful.
63 |
64 | ### Scope
65 |
66 | This Code of Conduct applies both within project spaces and in public spaces
67 | when an individual is representing the project or its community. Examples of
68 | representing a project or community include using an official project e-mail
69 | address, posting via an official social media account, or acting as an appointed
70 | representative at an online or offline event. Representation of a project may be
71 | further defined and clarified by project maintainers.
72 |
73 | ### Enforcement
74 |
75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
77 | complaints will be reviewed and investigated and will result in a response that
78 | is deemed necessary and appropriate to the circumstances. The project team is
79 | obligated to maintain confidentiality with regard to the reporter of an incident.
80 | Further details of specific enforcement policies may be posted separately.
81 |
82 | Project maintainers who do not follow or enforce the Code of Conduct in good
83 | faith may face temporary or permanent repercussions as determined by other
84 | members of the project's leadership.
85 |
86 | ### Attribution
87 |
88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
89 | available at [http://contributor-covenant.org/version/1/4][version]
90 |
91 | [homepage]: http://contributor-covenant.org
92 | [version]: http://contributor-covenant.org/version/1/4/
93 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kafka-Penguin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Home
2 |
3 |
4 |
5 | Kafka-Penguin
6 |
7 |
8 | An easy-to-use, lightweight KafkaJS library for message re-processing.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Table of Contents
20 |
21 | * [About](#About)
22 | * [Getting Started](#Getting-Started)
23 | * [Example](#Example)
24 | * [Strategies](#Strategies)
25 | * [Contributors](#Contributors)
26 | * [License](#License)
27 |
28 | ### About
29 |
30 | Kafka-Penguin is an easy-to-use, lightweight KafkaJS plugin for message re-processing. It provides developers with three strategies for setting up message re-processing: FailFast, Ignore, and Dead Letter Queue.
31 |
32 | The package allows developers to build event-driven applications with dedicated "fail strategies" modeled after best practices in the field. This in turn allows developers to effectively address bugs in development and deploy more fault-tolerant systems in production.
33 |
34 | This package is meant to work in conjunction with with KafkaJS. For more information on KafkaJS, check out [Getting Started with KafkaJS](https://kafka.js.org/docs/getting-started).
35 |
36 | ### Getting Started
37 |
38 | Install Kafka-Penguin as an npm module and save it to your package.json file as a dependency:
39 |
40 | ```text
41 | npm install kafka-penguin
42 | ```
43 |
44 | Once installed it can now be referenced by simply calling `require('kafka-penguin');`
45 |
46 | ## Example
47 |
48 | All Kafka-Penguin needs is a KafkaJS client to run. Start by passing the client for your preferred strategy and Kafka-Penguin will create bespoke consumers, producers, and admins with built-in functionality to execute the chosen strategy. On the surface, you implement your application exactly as you would with KafkaJS.
49 |
50 | ```text
51 | /* eslint-disable no-console */
52 | import { FailFast } from 'kafka-penguin';
53 |
54 | const exampleClient = require('./clientConfig.ts');
55 |
56 | // Set up the preferred strategy with a configured KafkaJS client
57 | const exampleStrategy = new FailFast(2, exampleClient);
58 |
59 | // Initialize a producer or consumer from the instance of the strategy
60 | const producer = exampleStrategy.producer();
61 |
62 | const message = {
63 | topic: 'wrong-topic',
64 | messages: [
65 | {
66 | key: 'hello',
67 | value: 'world',
68 | },
69 | ],
70 | };
71 |
72 | // Connect, Subscribe, Send, or Run virtually the same as with KafkaJS
73 | producer.connect()
74 | .then(() => console.log('Connected!'))
75 | // The chosen strategy executes under the hood, like in this send method
76 | .then(() => producer.send(message))
77 | .catch((e: any) => console.log('error: ', e.message));
78 | ```
79 |
80 | ## Strategies
81 |
82 | Dive in deeper to any of the strategies for set up, execution, and implementation.
83 |
84 | [FailFast](https://kafkapenguin.gitbook.io/kafka-penguin/strategies/failfast)
85 |
86 | [Ignore](https://kafkapenguin.gitbook.io/kafka-penguin/strategies/strategies-ignore)
87 |
88 | [Dead Letter Queue](https://kafkapenguin.gitbook.io/kafka-penguin/strategies/dead-letter-queue)
89 |
90 | ## **Contributors**
91 |
92 | [Ausar English](https://www.linkedin.com/in/ausarenglish) [@ausarenglish](https://github.com/ausarenglish)
93 |
94 | [Kushal Talele](https://www.linkedin.com/in/kushal-talele-29040820b/) [@ktrane1](https://github.com/ktrane1)
95 |
96 | [Timeo Williams](https://www.linkedin.com/in/timeowilliams/) [@timeowilliams](https://github.com/timeowilliams)
97 |
98 | [Ziyad El Baz](https://www.linkedin.com/in/ziyadelbaz) [@zelbaz946](https://github.com/zelbaz946)
99 |
100 | ### License
101 |
102 | This product is licensed under the MIT License - see the [LICENSE.md](https://github.com/oslabs-beta/kafka-penguin/blob/main/LICENSE) file for details.
103 |
104 | This is an open source product. We are not affiliated nor endorsed by either the Apache Software Foundation or KafkaJS.
105 |
106 | This product is accelerated by [OS Labs](https://opensourcelabs.io/).
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [Home](README.md)
4 | * [Strategies](strategies/README.md)
5 | * [FailFast](strategies/failfast.md)
6 | * [Ignore](strategies/strategies-ignore.md)
7 | * [Dead Letter Queue](strategies/dead-letter-queue.md)
8 |
9 |
--------------------------------------------------------------------------------
/clientConfig.ts:
--------------------------------------------------------------------------------
1 | const { Kafka } = require('kafkajs');
2 | require('dotenv').config();
3 |
4 | // Create the client with the broker list
5 | const kafka = new Kafka({
6 | clientId: 'fail-fast-producer',
7 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
8 | ssl: true,
9 | sasl: {
10 | mechanism: 'plain',
11 | username: process.env.KAFKA_USERNAME,
12 | password: process.env.KAFKA_PASSWORD,
13 | },
14 | });
15 |
16 | module.exports = kafka;
17 |
--------------------------------------------------------------------------------
/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react",
5 | "@babel/preset-typescript"
6 | ]
7 | }
--------------------------------------------------------------------------------
/demo/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/demo/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "plugin:react/recommended",
9 | "airbnb"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaFeatures": {
14 | "jsx": true,
15 | "tsx": true
16 | },
17 | "ecmaVersion": 12,
18 | "sourceType": "module"
19 | },
20 | "plugins": [
21 | "react",
22 | "@typescript-eslint"
23 | ],
24 | "rules": {
25 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
26 | "import/extensions": "off",
27 | "no-use-before-define": "off",
28 | "@typescript-eslint/no-use-before-define": ["error"]
29 | },
30 | "settings": {
31 | "import/resolver": {
32 | "node": {
33 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/demo/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.18.3
2 |
3 | RUN npm i -g webpack
4 | WORKDIR /usr/src/app
5 | COPY . /usr/src/app
6 | RUN npm install
7 | RUN npm run build2
8 | # RUN npm run tsc
9 | EXPOSE 3000
10 | # ENTRYPOINT [ "nodemon", "./server/server.ts" ]
11 | CMD ["npm", "start"]
12 |
--------------------------------------------------------------------------------
/demo/__tests__/backend/strategy.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import server from '../../server/app';
3 |
4 | describe('Strategy tests', () => {
5 | const messageValid = {
6 | topic: 'test2',
7 | message: 'Hello world',
8 | retries: 4,
9 | faults: 2,
10 | };
11 | const messageInvalid = {
12 | topic: 'wrongtopic',
13 | message: 'Hello world',
14 | retries: 4,
15 | };
16 | beforeAll((done: () => void) => {
17 | done();
18 | });
19 | afterAll((done: () => void) => {
20 | done();
21 | });
22 |
23 | describe('Failfast tests', () => {
24 | it('Expect valid response from valid message', (done: { (): void; (err: Error, res: request.Response): void; }) => {
25 | request(server)
26 | .post('/strategy/failfast')
27 | .send(messageValid)
28 | .expect('Content-Type', 'application/json; charset=utf-8')
29 | .expect(200)
30 | .expect((res) => {
31 | expect(res.body.length).toEqual(1);
32 | expect(typeof res.body[0] === 'string').toBe(true);
33 | expect(res.body[0]).toBe('kafka-penguin: Message produced successfully');
34 | done();
35 | })
36 | .end(done);
37 | });
38 |
39 | it('Expect error log in response from invalid message', (done: { (): void; (err: any, res: request.Response): void; }) => {
40 | request(server)
41 | .post('/strategy/failfast')
42 | .send(messageInvalid)
43 | .expect('Content-Type', 'application/json; charset=utf-8')
44 | .expect(200)
45 | .expect((res) => {
46 | expect(res.body.length).toEqual(messageInvalid.retries + 1);
47 | expect(res.body[messageInvalid.retries]).toBe(`kafka-penguin: FailFast stopped producer after ${messageInvalid.retries} times!`);
48 | res.body.forEach((error: any) => {
49 | expect(typeof error === 'string').toBe(true);
50 | });
51 | done();
52 | })
53 | .end(done);
54 | });
55 | });
56 |
57 | describe('DLQ tests', () => {
58 | describe('DLQ Consumer: consuming messages from existent topic', () => {
59 | it('Expect valid response without interruption of data flow', (done: { (): void; (err: Error, res: request.Response): void; }) => {
60 | request(server)
61 | .post('/strategy/dlq')
62 | .send(messageValid)
63 | .expect(200)
64 | .expect((res) => {
65 | const messages = res.body.slice(0, res.body.length - 1);
66 | expect(messages.length).toEqual(messageValid.retries - messageValid.faults);
67 | messages.forEach((message: any) => {
68 | //expect(message).toEqual('Hello world');
69 | expect(typeof message).toBe('string');
70 | });
71 | expect(res.body[messageValid.retries - messageValid.faults]).toContain('kafka-penguin');
72 | expect(res.body[messageValid.retries - messageValid.faults]).toContain('2');
73 | expect(res.body[messageValid.retries - messageValid.faults]).toContain('test2.deadLetterQueue');
74 | done();
75 | })
76 | .end(done);
77 | });
78 | });
79 | describe('DLQ Producer: non-existent and existent topics', () => {
80 | it('Expect kafka-penguin error while posting to non-existent topic', (done: { (): void; (err: any, res: request.Response): void; }) => {
81 | request(server)
82 | .post('/strategy/dlq')
83 | .send(messageInvalid)
84 | .expect(300)
85 | .expect((res) => {
86 | const error = res.body[0];
87 | expect(res.body.length).toBe(1);
88 | expect(typeof error).toBe('string');
89 | done();
90 | })
91 | .end(done);
92 | });
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/demo/__tests__/backend/topic.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import dotenv from 'dotenv';
3 | import server from '../../server/app';
4 |
5 | dotenv.config();
6 |
7 | describe('Static router tests', () => {
8 | const userDetails = {
9 | brokers: process.env.KAFKA_BOOTSTRAP_SERVER,
10 | username: process.env.KAFKA_USERNAME,
11 | password: process.env.KAFKA_PASSWORD,
12 | };
13 |
14 | it('Returns array of objects containing topic names and partitions', (done) => {
15 | request(server)
16 | .post('/topic/getTopics')
17 | .send(JSON.stringify(userDetails))
18 | .expect('Content-Type', 'application/json; charset=utf-8')
19 | .expect(200)
20 | .expect((res) => {
21 | res.body.topics.forEach((topic) => {
22 | expect(topic.hasOwnProperty('name')).toBe(true);
23 | expect(topic.hasOwnProperty('partitions')).toBe(true);
24 | expect(typeof topic.name === 'string').toBe(true);
25 | expect(Array.isArray(topic.partitions)).toBe(true);
26 | });
27 | done();
28 | })
29 | .end(done);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/demo/client/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | createStyles, makeStyles, Typography, Container,
5 | } from '@material-ui/core';
6 | import { Element } from 'react-scroll';
7 | import MainContainer from './containers/MainContainer';
8 | import { BackdropProvider } from './context/BackDropContext';
9 | import GlobalNavBar from './components/GlobalNavBar';
10 | import LandingBody from './components/LandingBody';
11 | import TeamContainer from './containers/TeamContainer';
12 | import GettingStarted from './components/GettingStarted';
13 | import ParticlesBackdrop from './components/ParticlesBackdrop';
14 |
15 | const useStyles = makeStyles(() => createStyles({
16 | container: {
17 | display: 'flex',
18 | alignItems: 'center',
19 | justifyContent: 'flex-end',
20 | flexDirection: 'column',
21 | },
22 | titleBox: {
23 | display: 'flex',
24 | flexDirection: 'column',
25 | justifyContent: 'center',
26 | alignItems: 'center',
27 | paddingTop: '30vh',
28 | paddingBottom: '15vh',
29 | },
30 | segment: {
31 | display: 'flex',
32 | flexDirection: 'column',
33 | justifyContent: 'center',
34 | alignItems: 'center',
35 | paddingTop: '5vh',
36 | paddingBottom: '5vh',
37 | },
38 | }));
39 |
40 | const App: FC = () => {
41 | const classes = useStyles();
42 | return (
43 | <>
44 |
45 |
46 |
47 |
48 |
54 | kafka-penguin
55 |
56 |
57 |
63 | ERROR HANDLING LIBRARY FOR KAFKAJS
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | >
82 | );
83 | };
84 |
85 | export default App;
86 |
--------------------------------------------------------------------------------
/demo/client/assets/ausar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/kafka-penguin/d8983545c5169da31318e44792271adee9fcffe0/demo/client/assets/ausar.jpeg
--------------------------------------------------------------------------------
/demo/client/assets/kushal.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/kafka-penguin/d8983545c5169da31318e44792271adee9fcffe0/demo/client/assets/kushal.jpeg
--------------------------------------------------------------------------------
/demo/client/assets/timeo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/kafka-penguin/d8983545c5169da31318e44792271adee9fcffe0/demo/client/assets/timeo.jpeg
--------------------------------------------------------------------------------
/demo/client/assets/ziyad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/kafka-penguin/d8983545c5169da31318e44792271adee9fcffe0/demo/client/assets/ziyad.png
--------------------------------------------------------------------------------
/demo/client/clientConfig.ts:
--------------------------------------------------------------------------------
1 | import { Kafka } from 'kafkajs';
2 |
3 | require('dotenv').config();
4 |
5 | // Create the client with the broker list
6 | const kafka = new Kafka({
7 | clientId: 'fail-fast-producer',
8 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
9 | ssl: true,
10 | sasl: {
11 | mechanism: 'plain',
12 | username: process.env.KAFKA_USERNAME,
13 | password: process.env.KAFKA_PASSWORD,
14 | },
15 | });
16 |
17 | module.exports = kafka;
18 |
--------------------------------------------------------------------------------
/demo/client/components/Error.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import { createStyles, makeStyles, Typography } from '@material-ui/core';
4 |
5 | const useStyles = makeStyles(() => createStyles({
6 | root: {
7 | padding: '0em 1em 1em 1em',
8 | height: 'auto',
9 | },
10 | }));
11 |
12 | type Props = {
13 | errorMessage: string,
14 | }
15 |
16 | const Error: FC = ({ errorMessage }: Props) => {
17 | const classes = useStyles();
18 | return (
19 |
20 | {errorMessage}
21 |
22 | );
23 | };
24 |
25 | export default Error;
26 |
--------------------------------------------------------------------------------
/demo/client/components/GettingStarted.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | Container, Typography, Divider, makeStyles, createStyles, Button, Theme,
4 | } from '@material-ui/core';
5 | import SyntaxHighlighter from 'react-syntax-highlighter';
6 | import { materialLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
7 | import GetAppRoundedIcon from '@material-ui/icons/GetAppRounded';
8 |
9 | const GettingStarted: FC = () => {
10 | const useStyles = makeStyles((theme: Theme) => createStyles({
11 | main: {
12 | display: 'flex',
13 | flexDirection: 'column',
14 | justifyContent: 'space-between',
15 | alignItems: 'center',
16 | alignContent: 'center',
17 | marginTop: '5vh',
18 | backgroundColor: theme.palette.background.default,
19 | paddingTop: '3vh',
20 | paddingBottom: '3vh',
21 | width: 'auto',
22 | },
23 | button: {
24 | margin: '1rem 1rem 1rem 1rem',
25 | },
26 | wrapper: {
27 | display: 'flex',
28 | flexDirection: 'column',
29 | justifyItems: 'center',
30 | alignItems: 'center',
31 | },
32 | buttonsContainer: {
33 | display: 'flex',
34 | justifyContent: 'center',
35 | },
36 | divider: {
37 | height: '1px',
38 | },
39 | icon: {
40 | color: theme.palette.secondary.light,
41 | fontSize: '5vh',
42 | padding: '2vh',
43 | },
44 | }));
45 | const installCode = 'npm install kafka-penguin';
46 | const importCode = 'import { DeadLetterQueue } from \'kafka-penguin\';';
47 | const classes = useStyles();
48 | return (
49 |
50 |
51 | GETTING STARTED
52 |
53 |
54 |
55 |
56 | Simply install from npm and import into your project
57 |
58 |
59 |
63 | { installCode }
64 |
65 |
69 | { importCode }
70 |
71 |
72 |
78 | Documentation
79 |
80 |
86 | Github
87 |
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default GettingStarted;
95 |
--------------------------------------------------------------------------------
/demo/client/components/GlobalNavBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | createStyles, makeStyles, withStyles, AppBar, Toolbar, IconButton, Button, Icon,
5 | } from '@material-ui/core';
6 | import { Link } from 'react-scroll';
7 |
8 | const useStyles = makeStyles(() => createStyles({
9 | root: {
10 | width: '100vw',
11 | padding: 0,
12 | },
13 | landingButtons: {
14 | display: 'flex',
15 | justifyContent: 'flex-end',
16 | },
17 | logo: {
18 | alignSelf: 'flex-start',
19 | },
20 | button: {
21 | margin: '1rem 1rem 1rem 1rem',
22 | },
23 | }));
24 |
25 | const GlobalCss = withStyles({
26 | '@global': {
27 | 'html, body': {
28 | margin: 0,
29 | padding: 0,
30 | },
31 | },
32 | })(() => null);
33 |
34 | const GlobalNavBar: FC = () => {
35 | const classes = useStyles();
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
49 |
50 |
51 |
52 |
62 | Features
63 |
64 |
74 | Demo
75 |
76 |
86 | Getting Started
87 |
88 |
98 | Team
99 |
100 | {/*
105 | Docs
106 |
107 |
112 | Github
113 | */}
114 |
115 |
116 |
117 | );
118 | };
119 |
120 | export default GlobalNavBar;
121 |
--------------------------------------------------------------------------------
/demo/client/components/LandingBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | makeStyles, createStyles, Container, Divider, Theme, Typography,
4 | } from '@material-ui/core';
5 | import ErrorOutlineRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
6 | import FitnessCenterRoundedIcon from '@material-ui/icons/FitnessCenterRounded';
7 | import AccessibilityNewRoundedIcon from '@material-ui/icons/AccessibilityNewRounded';
8 | import SyntaxHighlighter from 'react-syntax-highlighter';
9 | import { materialLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
10 |
11 | const useStyles = makeStyles((theme: Theme) => createStyles({
12 | button: {
13 | margin: '1rem 1rem 1rem 1rem',
14 | },
15 | container: {
16 | display: 'flex',
17 | flexDirection: 'column',
18 |
19 | },
20 | inner: {
21 | display: 'flex',
22 | alignContent: 'center',
23 | justifyContent: 'space-around',
24 | alignItems: 'center',
25 | marginTop: '3vh',
26 | marginBottom: '5vh',
27 | paddingTop: '5vh',
28 | paddingBottom: '5vh',
29 | background: theme.palette.background.default,
30 | },
31 | icon: {
32 | fontSize: '7vh',
33 | color: theme.palette.primary.light,
34 | padding: '2vh',
35 | },
36 | }));
37 |
38 | const lightWeightCode = `import { FailFast } from 'kafka-penguin';
39 | const failfast = new FailFast(2, kafkaClient);`;
40 |
41 | const easyToUseCode = `const producer = failfast.producer();
42 | producer.connect()
43 | .then(producer.send(...))`;
44 |
45 | const errorHandlingCode = `import {
46 | DeadLetterQueue,
47 | FailFast,
48 | Ignore
49 | } from 'kafka-penguin';`;
50 |
51 | const LandingBody: FC = () => {
52 | const classes = useStyles();
53 | return (
54 | <>
55 |
56 |
57 | FEATURES
58 |
59 |
60 |
61 |
62 |
63 |
64 | LIGHT-WEIGHT
65 |
66 | Minimal Configuration
67 |
68 |
69 | A working KafkaJS client and a callback that returns a boolean.
70 |
71 |
72 | That’s all it takes to implement a strategy.
73 |
74 |
75 |
79 | {lightWeightCode}
80 |
81 |
82 |
83 |
87 | { easyToUseCode }
88 |
89 |
90 |
91 |
92 | EASY-TO-USE
93 |
94 | Plug-and-Play
95 |
96 |
97 | Add one line of code on top of your existing
98 |
99 | implementation and keep using KafkaJS as normal.
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | ERROR-HANDLING
108 |
109 | Common, Programmable Strategies
110 |
111 |
112 | Choose from some of the most widely used strategies.
113 |
114 |
115 | Program them to fit your application’s logic.
116 |
117 |
118 |
122 | { errorHandlingCode }
123 |
124 |
125 |
126 | >
127 | );
128 | };
129 |
130 | export default LandingBody;
131 |
--------------------------------------------------------------------------------
/demo/client/components/Message.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | Typography, TextField, makeStyles, createStyles, Slider,
5 | } from '@material-ui/core';
6 | import { useMesageUpdateContext } from '../context/MessageContext';
7 |
8 | const useStyles = makeStyles(() => createStyles({
9 | root: {
10 | display: 'flex',
11 | flexDirection: 'column',
12 | alignItems: 'center',
13 | justifyContent: 'center',
14 | '& .MuiTextField-root': {
15 | // margin: theme.spacing(1),
16 | width: '25vw',
17 | minWidth: '20vw',
18 | marginTop: '0',
19 | },
20 | },
21 | textfield: {
22 | paddingBottom: '1rem',
23 | },
24 | }));
25 |
26 | const Message: FC = () => {
27 | const classes = useStyles();
28 | const messageUpdate = useMesageUpdateContext();
29 | return (
30 |
98 | );
99 | };
100 |
101 | export default Message;
102 |
--------------------------------------------------------------------------------
/demo/client/components/ParticlesBackdrop.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import Particles from 'react-tsparticles';
4 |
5 | const ParticlesBackdrop: FC = () => (
6 |
96 | );
97 |
98 | export default ParticlesBackdrop;
99 |
--------------------------------------------------------------------------------
/demo/client/components/TeamMember.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | makeStyles, createStyles, Card, CardContent, Typography,
4 | } from '@material-ui/core';
5 | import GitHubIcon from '@material-ui/icons/GitHub';
6 | import LinkedInIcon from '@material-ui/icons/LinkedIn';
7 |
8 | interface Props {
9 | details: {
10 | name: string,
11 | linkedIn: string,
12 | github: string,
13 | photo: string
14 | }
15 | }
16 |
17 | const TeamMember: FC = ({
18 | details,
19 | }: Props) => {
20 | const useStyles = makeStyles(() => createStyles({
21 | root: {
22 | display: 'flex',
23 | alignItems: 'center',
24 | alignContent: 'center',
25 | justifyContent: 'center',
26 | padding: '1rem 1rem 1rem 1rem',
27 | margin: '0rem 2rem 2rem 2rem',
28 | minWidth: '15vw',
29 | maxWidth: '15vw',
30 | },
31 | }));
32 |
33 | const classes = useStyles();
34 |
35 | return (
36 |
37 |
38 |
39 |
48 |
49 |
50 | {details.name}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default TeamMember;
66 |
--------------------------------------------------------------------------------
/demo/client/components/Topic.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | Card, CardContent, Typography, createStyles, makeStyles,
5 | } from '@material-ui/core';
6 |
7 | type Props = {
8 | topicInfo: {
9 | name: string,
10 | partitions: number
11 | },
12 | id: number,
13 | }
14 |
15 | const Topic: FC = ({ topicInfo, id }: Props) => {
16 | const useStyles = makeStyles(() => createStyles({
17 | root: {
18 | display: 'flex',
19 | alignItems: 'center',
20 | justifyContent: 'center',
21 | margin: '1rem 1rem 1rem 1rem',
22 | },
23 | }));
24 |
25 | const classes = useStyles();
26 |
27 | return (
28 |
29 |
30 |
31 | {topicInfo.name}
32 |
33 |
34 | {`partitions: ${topicInfo.partitions}`}
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default Topic;
42 |
--------------------------------------------------------------------------------
/demo/client/containers/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | Container, createStyles, makeStyles, Backdrop, CircularProgress, Theme, Typography, Divider,
5 | } from '@material-ui/core';
6 | import TopicsContainer from './TopicsContainer';
7 | import StrategyContainer from './StrategyContainer';
8 | import MessageErrorContainer from './MessageErrorContainer';
9 | import { TopicsProvider } from '../context/TopicContext';
10 | import { MessageProvider } from '../context/MessageContext';
11 | import { ErrorProvider } from '../context/ErrorContext';
12 | import { useBackdropContext, useBackdropUpdateContext } from '../context/BackDropContext';
13 |
14 | const MainContainer: FC = () => {
15 | const useStyles = makeStyles((theme: Theme) => createStyles({
16 | wrapper: {
17 | display: 'flex',
18 | flexDirection: 'column',
19 | justifyContent: 'center',
20 | alignItems: 'center',
21 | alignContent: 'center',
22 | },
23 | container: {
24 | display: 'flex',
25 | alignItems: 'center',
26 | justifyContent: 'center',
27 | alignContent: 'center',
28 | flexDirection: 'column',
29 | },
30 | button: {
31 | margin: '1rem 1rem 1rem 1rem',
32 | },
33 | backdrop: {
34 | zIndex: theme.zIndex.drawer + 1,
35 | color: '#fff',
36 | },
37 | divider: {
38 | height: '1px',
39 | },
40 | }));
41 |
42 | const classes = useStyles();
43 | const backdropContext = useBackdropContext();
44 | const backdropUpdate = useBackdropUpdateContext();
45 |
46 | return (
47 |
48 |
49 | DEMO
50 |
51 |
52 |
53 | Trigger errors to see kafka-penguin in action
54 |
55 |
56 | Errors can be triggered by publishing to a non-existent
57 | topic or prescribing a number of faults.
58 |
59 |
60 | Enter in a topic, message and choose your strategy to test with our sample cluster.
61 |
62 |
63 | Load demo topics
64 |
65 | Publish either to existent topic or non-existent topic
66 |
67 | Select retries
68 |
69 | Execute strategy
70 |
71 |
72 | * DLQ and Ignore require repeats to be greater than faults
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default MainContainer;
101 |
--------------------------------------------------------------------------------
/demo/client/containers/MessageErrorContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | Container, Typography, Theme, Paper, createStyles, makeStyles,
5 | } from '@material-ui/core';
6 | import Error from '../components/Error';
7 | import Message from '../components/Message';
8 | import { useErrorContext } from '../context/ErrorContext';
9 |
10 | const useStyles = makeStyles((theme: Theme) => createStyles({
11 | paper: {
12 | display: 'flex',
13 | minHeight: '80%',
14 | '& > *': {
15 | margin: theme.spacing(1),
16 | },
17 | },
18 | containerVertical: {
19 | display: 'flex',
20 | flexDirection: 'column',
21 | justifyContent: 'flex-start',
22 | paddingTop: '3vh',
23 | // minWidth: '10vw'
24 | },
25 | containerHorizontal: {
26 | display: 'flex',
27 | justifyContent: 'center',
28 | flexGrow: 1,
29 | // flexWrap: 'wrap'
30 | },
31 | }));
32 |
33 | const MessageErrorContainer: FC = () => {
34 | const classes = useStyles();
35 | const updateError = useErrorContext();
36 |
37 | const errors = updateError.map((error: string) => (
38 |
39 | ));
40 | return (
41 |
42 |
43 |
49 | Publish
50 |
51 |
52 |
53 |
54 |
60 | Log
61 |
62 |
66 |
70 | {errors}
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default MessageErrorContainer;
79 |
--------------------------------------------------------------------------------
/demo/client/containers/StrategyContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | createStyles, makeStyles, Button, Container,
5 | } from '@material-ui/core';
6 | import { useErrorUpdateContext } from '../context/ErrorContext';
7 | import { useMessageContext } from '../context/MessageContext';
8 | import { useBackdropUpdateContext } from '../context/BackDropContext';
9 |
10 | const useStyles = makeStyles(() => createStyles({
11 | container: {
12 | display: 'flex',
13 | alignItems: 'center',
14 | justifyContent: 'center',
15 | alignContent: 'space-between',
16 | },
17 | button: {
18 | margin: '1rem 1rem 1rem 1rem',
19 | },
20 | }));
21 |
22 | const StrategyContainer: FC = () => {
23 | // After the user clicks on the button FF or DLQ,
24 | // we will render the associated strategy data
25 | // to them in a div below
26 | const message = useMessageContext();
27 | const handleClicks = useErrorUpdateContext();
28 | const backdropUpdate = useBackdropUpdateContext();
29 | const classes = useStyles();
30 |
31 | return (
32 |
33 | {
38 | if (message.message && message.topic) backdropUpdate.handleToggle();
39 | handleClicks.handleFailFast(message);
40 | }}
41 | >
42 | FailFast
43 |
44 | {
49 | if (message.message
50 | && message.topic
51 | && message.retries > message.faults) backdropUpdate.handleToggle();
52 | handleClicks.handleDLQ(message);
53 | }}
54 | >
55 | DLQ
56 |
57 | {
62 | if (message.message
63 | && message.topic
64 | && message.retries > message.faults) backdropUpdate.handleToggle();
65 | handleClicks.handleIgnore(message);
66 | }}
67 | >
68 | Ignore
69 |
70 |
71 | );
72 | };
73 | export default StrategyContainer;
74 |
--------------------------------------------------------------------------------
/demo/client/containers/TeamContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import {
3 | makeStyles, createStyles, Container, Divider, Typography,
4 | } from '@material-ui/core';
5 | import TeamMember from '../components/TeamMember';
6 |
7 | const TeamContainer: FC = () => {
8 | const useStyles = makeStyles(() => createStyles({
9 | containerHorizontal: {
10 | // height: '80vh',
11 | paddingTop: '10vh',
12 | display: 'flex',
13 | flexDirection: 'column',
14 | alignItems: 'center',
15 | alignContent: 'center',
16 | justifyContent: 'center',
17 | },
18 | containerVertical: {
19 | display: 'flex',
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 |
23 | },
24 | }));
25 | const timeo = {
26 | name: 'Timeo Williams',
27 | linkedIn: 'https://www.linkedin.com/in/timeowilliams/',
28 | github: 'https://github.com/timeowilliams',
29 | photo: '/assets/timeo.jpeg',
30 | };
31 | const ausar = {
32 | name: 'Ausar English',
33 | linkedIn: 'https://www.linkedin.com/in/ausarenglish/',
34 | github: 'https://github.com/ausarenglish',
35 | photo: '/assets/ausar.jpeg',
36 | };
37 | const ziyad = {
38 | name: 'Ziyad Elbaz',
39 | linkedIn: 'https://www.linkedin.com/in/ziyadelbaz/',
40 | github: 'https://github.com/zelbaz946',
41 | photo: '/assets/ziyad.png',
42 | };
43 | const kushal = {
44 | name: 'Kushal Talele',
45 | linkedIn: 'https://www.linkedin.com/in/kushaltalele/',
46 | github: 'https://github.com/ktrane1',
47 | photo: '/assets/kushal.jpeg',
48 | };
49 |
50 | const classes = useStyles();
51 |
52 | return (
53 |
54 |
55 | TEAM
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default TeamContainer;
73 |
--------------------------------------------------------------------------------
/demo/client/containers/TopicsContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC } from 'react';
3 | import {
4 | createStyles, makeStyles, Container, Button,
5 | } from '@material-ui/core';
6 | import Topic from '../components/Topic';
7 | import { useTopicsContext, useTopicsContextUpdate } from '../context/TopicContext';
8 | import { useBackdropUpdateContext } from '../context/BackDropContext';
9 |
10 | const useStyles = makeStyles(() => createStyles({
11 | topicsContainer: {
12 | display: 'flex',
13 | flexWrap: 'wrap',
14 | alignItems: 'center',
15 | justifyContent: 'center',
16 | },
17 | button: {
18 | margin: '1rem 1rem 1rem 1rem',
19 | },
20 | }));
21 |
22 | const TopicsContainer: FC = () => {
23 | const backdropUpdate = useBackdropUpdateContext();
24 | const topicsUpdate = useTopicsContextUpdate();
25 | const topics = useTopicsContext();
26 | const topicsMapped = topics.map((topicInfo, i) => );
27 |
28 | const classes = useStyles();
29 |
30 | return (
31 |
32 |
33 | {
38 | backdropUpdate.handleToggle();
39 | topicsUpdate.getTopics();
40 | }}
41 | >
42 | Load Demo Topics
43 |
44 | {
49 | topicsUpdate.clearTopics();
50 | }}
51 | >
52 | Clear Topics
53 |
54 |
55 |
56 | {topicsMapped}
57 |
58 |
59 | );
60 | };
61 |
62 | export default TopicsContainer;
63 |
--------------------------------------------------------------------------------
/demo/client/context/BackDropContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | useState, useContext, FC, createContext,
4 | } from 'react';
5 |
6 | const BackdropContext = createContext(null);
7 | const BackdropUpdateContext = createContext(null);
8 |
9 | const useBackdropContext = () => useContext(BackdropContext);
10 |
11 | const useBackdropUpdateContext = () => useContext(BackdropUpdateContext);
12 |
13 | interface Props {
14 | children: any
15 | }
16 |
17 | const BackdropProvider: FC = ({ children } : Props) => {
18 | const [open, setOpen] = useState(false);
19 |
20 | const handleClose = () => {
21 | setOpen(false);
22 | };
23 |
24 | const handleToggle = () => {
25 | setOpen(!open);
26 | };
27 |
28 | return (
29 |
30 |
38 | {children}
39 |
40 |
41 | );
42 | };
43 |
44 | export { BackdropProvider, useBackdropContext, useBackdropUpdateContext };
45 |
--------------------------------------------------------------------------------
/demo/client/context/ErrorContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | useState, useContext, FC, createContext, useEffect,
4 | } from 'react';
5 | import { useBackdropUpdateContext } from './BackDropContext';
6 |
7 | const ErrorContext = createContext(null);
8 | const ErrorUpdateContext = createContext(null);
9 |
10 | const useErrorContext = () => useContext(ErrorContext);
11 |
12 | const useErrorUpdateContext = () => useContext(ErrorUpdateContext);
13 |
14 | interface Props {
15 | children: any
16 | }
17 |
18 | const ErrorProvider: FC = ({ children } : Props) => {
19 | const [error, changeError] = useState([]);
20 |
21 | const backdropUpdate = useBackdropUpdateContext();
22 | // UPDATE BACKDROP STATE
23 | useEffect(() => {
24 | backdropUpdate.handleClose();
25 | }, [error]);
26 | // FAILFAST POST REQUEST //
27 | const handleFailFast = (input: {
28 | message: string,
29 | topic: string,
30 | retries: number
31 | }) => {
32 | const { message, topic, retries } = input;
33 | if (!topic || !message) return;
34 |
35 | fetch('/strategy/failfast', {
36 | method: 'POST',
37 | headers: { 'Content-Type': 'Application/JSON' },
38 | body: JSON.stringify({ topic, message, retries }),
39 | })
40 | .then((data) => data.json())
41 | .then((errors) => {
42 | changeError(errors);
43 | });
44 | };
45 | // DEAD LETTER QUEUE POST REQUEST //
46 | const handleDLQ = (input: {
47 | message: string;
48 | topic: string;
49 | retries: number;
50 | faults: number;
51 | }) => {
52 | const {
53 | message, topic, retries, faults,
54 | } = input;
55 | if (!topic || !message || faults >= retries) return;
56 |
57 | fetch('/strategy/dlq', {
58 | method: 'POST',
59 | headers: { 'Content-Type': 'Application/JSON' },
60 | body: JSON.stringify({
61 | topic, message, retries, faults,
62 | }),
63 | })
64 | .then((res) => res.json())
65 | .then((messages) => {
66 | changeError(messages);
67 | })
68 | .catch((e) => console.log(e));
69 | };
70 |
71 | const handleIgnore = (input: {
72 | message: string;
73 | topic: string;
74 | retries: number;
75 | faults: number;
76 | }) => {
77 | const {
78 | message, topic, retries, faults,
79 | } = input;
80 | if (!topic || !message || faults >= retries) return;
81 |
82 | fetch('/strategy/ignore', {
83 | method: 'POST',
84 | headers: { 'Content-Type': 'Application/JSON' },
85 | body: JSON.stringify({
86 | topic, message, retries, faults,
87 | }),
88 | })
89 | .then((res) => res.json())
90 | .then((messages) => {
91 | changeError(messages);
92 | })
93 | .catch((e) => console.log(e));
94 | };
95 |
96 | return (
97 |
98 |
107 | {children}
108 |
109 |
110 | );
111 | };
112 |
113 | export { ErrorProvider, useErrorContext, useErrorUpdateContext };
114 |
--------------------------------------------------------------------------------
/demo/client/context/MessageContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | useState, useContext, FC, createContext,
4 | } from 'react';
5 |
6 | const MessageContext = createContext(null);
7 | const MessageUpdateContext = createContext(null);
8 |
9 | const useMessageContext = () => useContext(MessageContext);
10 |
11 | const useMesageUpdateContext = () => useContext(MessageUpdateContext);
12 |
13 | interface Props {
14 | children: any
15 | }
16 |
17 | const MessageProvider: FC = ({ children } : Props) => {
18 | const [message, changeMessage] = useState('');
19 | const [topic, changeTopic] = useState('');
20 | const [retries, changeRetries] = useState(2);
21 | const [faults, changeFaults] = useState(2);
22 |
23 | return (
24 |
34 |
44 | {children}
45 |
46 |
47 | );
48 | };
49 |
50 | export { MessageProvider, useMessageContext, useMesageUpdateContext };
51 |
--------------------------------------------------------------------------------
/demo/client/context/TopicContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | useState, useContext, FC, createContext, useEffect,
4 | } from 'react';
5 | import { useBackdropUpdateContext } from './BackDropContext';
6 |
7 | const TopicsContext = createContext(null);
8 | const TopicsUpdateContext = createContext(null);
9 |
10 | const useTopicsContext = () => useContext(TopicsContext);
11 |
12 | const useTopicsContextUpdate = () => useContext(TopicsUpdateContext);
13 |
14 | interface Props {
15 | children: any
16 | }
17 |
18 | const TopicsProvider: FC = ({ children } : Props) => {
19 | const [topicsArray, changeTopicsArray] = useState([]);
20 |
21 | const backdropUpdate = useBackdropUpdateContext();
22 |
23 | useEffect(() => {
24 | backdropUpdate.handleClose();
25 | }, [topicsArray]);
26 |
27 | const getTopics = () => {
28 | const userDetails = localStorage.getItem('userDetails');
29 | fetch('/topic/getTopics', {
30 | method: 'POST',
31 | headers: { 'Content-Type': 'Application/JSON' },
32 | body: userDetails,
33 | })
34 | .then((data) => data.json())
35 | .then((data) => {
36 | const topicData = data.topics.reduce((acc, cur) => {
37 | acc.push({
38 | name: cur.name,
39 | partitions: cur.partitions.length,
40 | });
41 | return acc;
42 | }, []);
43 | changeTopicsArray(topicData);
44 | });
45 | };
46 |
47 | const clearTopics = () => {
48 | changeTopicsArray([]);
49 | };
50 |
51 | return (
52 |
53 |
59 | {children}
60 |
61 |
62 | );
63 | };
64 |
65 | export { TopicsProvider, useTopicsContext, useTopicsContextUpdate };
66 |
--------------------------------------------------------------------------------
/demo/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 | kafka-penguin
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/client/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { ThemeProvider } from '@material-ui/core/styles';
4 | import App from './App';
5 | import theme from './theme';
6 |
7 | ReactDOM.render(
8 |
9 | {/* */}
10 |
11 | ,
12 | document.getElementById('root'),
13 | );
14 |
--------------------------------------------------------------------------------
/demo/client/theme.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles';
2 |
3 | const theme = createMuiTheme({
4 | // spacing: factor => `${0.25 * factor}rem`,
5 | palette: {
6 | primary: {
7 | light: '#72cff8',
8 | main: '#4fc3f7',
9 | dark: '#3788ac',
10 | contrastText: '#fff',
11 | },
12 | secondary: {
13 | light: '#ff7961',
14 | main: '#f44336',
15 | dark: '#ba000d',
16 | contrastText: '#000',
17 | },
18 | background: {
19 | default: '#f4f4f4',
20 | },
21 | text: {
22 | primary: '#404040',
23 | secondary: '#696969',
24 | disabled: '#a3a3a3',
25 | hint: '#a3a3a3',
26 | },
27 | },
28 | typography: {
29 | fontSize: 14,
30 | fontFamily: [
31 | 'Montserrat',
32 | ].join(','),
33 | },
34 | });
35 |
36 | export default theme;
37 |
--------------------------------------------------------------------------------
/demo/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/__tests__'],
3 | transform: {
4 | '^.+\\.tsx?$': 'ts-jest',
5 | },
6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
8 | }
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kafka-penguin/kafka-penguin",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "client/index.tsx",
6 | "engines": {
7 | "node": "12.18.3"
8 | },
9 | "scripts": {
10 | "build2": "webpack",
11 | "start": "ts-node ./server/server.ts",
12 | "format": "prettier --write src/**/*.ts{,x}",
13 | "lint": "tsc --noEmit && eslint src/**/*.ts{,x}",
14 | "tsc": "tsc -p tsconfig.json",
15 | "build": "tsc",
16 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js --hot --mode development\" \"nodemon server/server.ts\"",
17 | "test": "jest --detectOpenHandles --testTimeout=20000"
18 | },
19 | "jest": {
20 | "moduleFileExtensions": [
21 | "ts",
22 | "tsx",
23 | "js"
24 | ],
25 | "transform": {
26 | "^.+\\.(ts|tsx)$": "ts-jest"
27 | },
28 | "testMatch": [
29 | "**/__tests__/*.+(ts|tsx|js)"
30 | ]
31 | },
32 | "keywords": [],
33 | "author": "",
34 | "license": "ISC",
35 | "dependencies": {
36 | "@babel/plugin-syntax-jsx": "^7.12.13",
37 | "@babel/plugin-transform-runtime": "^7.13.10",
38 | "@material-ui/core": "^4.11.3",
39 | "@material-ui/icons": "^4.11.2",
40 | "awesome-typescript-loader": "^5.2.1",
41 | "babel-jest": "^26.6.3",
42 | "concurrently": "^6.0.0",
43 | "cookie-parser": "^1.4.5",
44 | "cross-env": "^7.0.3",
45 | "dotenv": "^8.2.0",
46 | "express": "^4.17.1",
47 | "highlight.js": "^10.7.2",
48 | "kafka-penguin": "^0.3.2",
49 | "kafkajs": "^1.15.0",
50 | "nodemon": "^2.0.7",
51 | "prop-types": "^15.7.2",
52 | "puppeteer": "^2.1.1",
53 | "react": "^16.14.0",
54 | "react-dom": "^16.14.0",
55 | "react-router-dom": "^5.2.0",
56 | "react-scroll": "^1.8.2",
57 | "react-showdown": "^2.3.0",
58 | "react-syntax-highlighter": "^15.4.3",
59 | "react-tsparticles": "^1.26.3",
60 | "regenerator-runtime": "^0.13.7",
61 | "set-interval": "^2.1.2",
62 | "ts-loader": "^8.0.18",
63 | "ts-node": "^9.1.1",
64 | "typescript": "^3.0.0",
65 | "util": "^0.12.3"
66 | },
67 | "devDependencies": {
68 | "@babel/core": "^7.13.13",
69 | "@babel/preset-env": "^7.13.12",
70 | "@babel/preset-react": "^7.13.13",
71 | "@babel/preset-typescript": "^7.13.0",
72 | "@types/express": "^4.17.11",
73 | "@types/jest": "^26.0.22",
74 | "@types/mocha": "^8.2.2",
75 | "@types/react": "^17.0.3",
76 | "@types/react-dom": "^17.0.3",
77 | "@types/react-router-dom": "^5.1.7",
78 | "@types/supertest": "^2.0.10",
79 | "@typescript-eslint/eslint-plugin": "^4.22.0",
80 | "@typescript-eslint/parser": "^4.22.0",
81 | "@webpack-cli/serve": "^1.3.0",
82 | "awesome-typescript-loader": "^5.2.1",
83 | "babel-cli": "^6.26.0",
84 | "babel-loader": "^8.2.2",
85 | "babel-preset-env": "^1.7.0",
86 | "enzyme": "^3.11.0",
87 | "enzyme-adapter-react-16": "^1.15.6",
88 | "eslint": "^7.24.0",
89 | "eslint-config-airbnb": "^18.2.1",
90 | "eslint-config-airbnb-typescript": "^12.3.1",
91 | "eslint-plugin-import": "^2.22.1",
92 | "eslint-plugin-jest": "^24.3.2",
93 | "eslint-plugin-jsx-a11y": "^6.4.1",
94 | "eslint-plugin-prettier": "^3.3.1",
95 | "eslint-plugin-react": "^7.23.2",
96 | "eslint-plugin-react-hooks": "^4.2.0",
97 | "express": "^4.17.1",
98 | "html-webpack-plugin": "^5.3.1",
99 | "jest": "^26.6.3",
100 | "mini-css-extract-plugin": "^1.3.9",
101 | "nodemon": "^2.0.7",
102 | "source-map-loader": "^2.0.1",
103 | "style-loader": "^2.0.0",
104 | "superagent": "^6.1.0",
105 | "supertest": "^6.1.3",
106 | "ts-jest": "^26.5.4",
107 | "ts-loader": "^8.0.18",
108 | "typescript": "^4.2.3",
109 | "webpack": "^5.28.0",
110 | "webpack-cli": "^4.5.0",
111 | "webpack-dev-server": "^3.11.2"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/demo/server/app.ts:
--------------------------------------------------------------------------------
1 | // routers
2 | import express from 'express';
3 | import path from 'path';
4 | import dotenv from 'dotenv';
5 |
6 | import strategyRouter from './routes/strategy';
7 | import topicRouter from './routes/topic';
8 |
9 | const app = express();
10 |
11 | dotenv.config(); app.use(express.json());
12 | app.use(express.urlencoded({ extended: true }));
13 |
14 | app.use('/assets', express.static(path.resolve(__dirname, '../client/assets/')));
15 | app.use('/js', express.static(path.resolve(__dirname, '../build/js/')));
16 |
17 | app.get('/', (req, res) => res.status(200).sendFile(path.join(__dirname, '../build/index.html')));
18 |
19 | app.use('/topic', topicRouter);
20 | app.use('/strategy', strategyRouter);
21 | app.get('/*', (req, res) => res.status(200).sendFile(path.resolve(__dirname, '../build/index.html')));
22 | app.get('*', (req, res) => res.status(404).json());
23 |
24 | app.use((err, req, res, next) => {
25 | const defaultErr = {
26 | log: 'Express error handler caught unknown middleware error',
27 | status: 500,
28 | message: 'An error occurred',
29 | };
30 | const error = {
31 | ...defaultErr,
32 | ...err,
33 | };
34 | return res.status(error.status).json(error.message);
35 | });
36 |
37 | export default app;
38 |
--------------------------------------------------------------------------------
/demo/server/controllers/DLQController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from 'express';
2 | import { Kafka, logLevel } from 'kafkajs';
3 | import { DeadLetterQueue } from 'kafka-penguin';
4 | import dotenv = require('dotenv');
5 |
6 | dotenv.config();
7 |
8 | const ERROR_LOG = [];
9 |
10 | const MyLogCreator = (logLevel) => ({
11 | namespace, level, label, log,
12 | }) => {
13 | // also availabe on log object => timestamp, logger, message and more
14 | const { error, correlationId } = log;
15 | if (correlationId) {
16 | ERROR_LOG.push(
17 | `[${namespace}] Logger: kafka-penguin ${label}: ${error} correlationId: ${correlationId}`,
18 | );
19 | }
20 | };
21 |
22 | const DLQKafka = new Kafka({
23 | clientId: 'makeClient',
24 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
25 | ssl: true,
26 | sasl: {
27 | mechanism: 'plain',
28 | username: process.env.KAFKA_USERNAME,
29 | password: process.env.KAFKA_PASSWORD,
30 | },
31 | logLevel: logLevel.ERROR,
32 | logCreator: MyLogCreator,
33 | });
34 |
35 | const dlqProduce: RequestHandler = (req, res, next) => {
36 | const {
37 | topic, message, retries, faults,
38 | } = req.body;
39 |
40 | const messagesArray = [];
41 | // create messages array with specified number of faults
42 | for (let i = 0; i < retries; i++) {
43 | if (i < faults) messagesArray.push({ key: 'test', value: 'fault' });
44 | else {
45 | messagesArray.push({
46 | key: 'test',
47 | value: message,
48 | });
49 | }
50 | }
51 |
52 | const cb = (message) => {
53 | if (message.value.toString() === 'fault') {
54 | return false;
55 | } return true;
56 | };
57 |
58 | const admin = DLQKafka.admin();
59 | const DLQClient = new DeadLetterQueue(DLQKafka, topic, cb);
60 | const DLQProducer = DLQClient.producer();
61 | const DLQConsumer = DLQClient.consumer({ groupId: 'demo' });
62 |
63 | res.locals.DLQClients = {
64 | consumer: DLQConsumer,
65 | retries,
66 | faults,
67 | };
68 | // DLQProducer.logger().info('TEST', {KAFKA_PENGUIN: 'TESTING CUSTOM'})
69 | DLQProducer.connect()
70 | .then(() => {
71 | DLQProducer.send({
72 | topic,
73 | messages: messagesArray,
74 | }).catch((e) => console.log('this is error in try', e.reference));
75 | })
76 | .then(DLQProducer.disconnect())
77 | .then(admin.connect())
78 | .then(async () => {
79 | const offsetData = await admin.fetchTopicOffsets(topic);
80 | res.locals.latestOffset = offsetData[0].offset;
81 | })
82 | .then(admin.disconnect())
83 | .then(() => next())
84 | .catch((e: Error) => {
85 | if (e.message === 'This server does not host this topic-partition') {
86 | return res.status(300).json([`This error was executed as part of the kafka-penguin
87 | Dead Letter Queue message reprocessing strategy. Your producer attempted to deliver
88 | a message 6 times but was unsuccessful. As a result, the message was sent to a
89 | Dead Letter Queue. Refer to the original error for further information`]);
90 | }
91 | return next({
92 | message: `Error implementing Dead Letter Queue strategy, producer side:${e.message}`,
93 | error: e,
94 | });
95 | });
96 | };
97 |
98 | const dlqConsume: RequestHandler = (req, res, next) => {
99 | const { faults, consumer, retries } = res.locals.DLQClients;
100 | const messageLog = [];
101 | consumer.connect()
102 | .then(consumer.subscribe())
103 | .then(() => {
104 | const latestOffset = Number(res.locals.latestOffset);
105 | consumer.run({
106 | eachMessage: ({ topic, partitions, message }) => {
107 | const messageOffset = Number(message.offset);
108 |
109 | if (messageOffset >= latestOffset - retries) {
110 | messageLog.push(message.value.toString());
111 | }
112 | if (messageLog.length === retries - faults) {
113 | messageLog.push(`kafka-penguin: Error with message processing, ${faults} ${faults > 1 ? 'messages' : 'message'}
114 | sent to DLQ topic ${topic}.deadLetterQueue`);
115 | res.locals.messages = messageLog;
116 | consumer.disconnect()
117 | .then(() => res.status(200).json(res.locals.messages))
118 | .catch((e) => next({
119 | message: `Error implementing Dead Letter Queue strategy while consuming messages, consumer side: ${e.message}`,
120 | error: e,
121 | }));
122 | }
123 | },
124 | });
125 | })
126 | .catch((e) => next({
127 | message: `Error implementing Dead Letter Queue strategy, consumer side: ${e.message}`,
128 | error: e,
129 | }));
130 | };
131 |
132 | export default {
133 | dlqConsume,
134 | dlqProduce,
135 | };
136 |
--------------------------------------------------------------------------------
/demo/server/controllers/failfastController.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { RequestHandler } from 'express';
3 | import { Kafka, logLevel } from 'kafkajs';
4 | import { FailFast } from 'kafka-penguin';
5 | import dotenv = require('dotenv');
6 |
7 | dotenv.config();
8 | // cache to store error logs
9 | let ERROR_LOG = [];
10 |
11 | const MyLogCreator = (logLevel) => ({
12 | namespace, label, log,
13 | }) => {
14 | // also availabe on log object => timestamp, logger, message and more
15 | // console.log(log)
16 | const { error, correlationId } = log;
17 | if (correlationId) {
18 | ERROR_LOG.push(
19 | `[${namespace}] Logger: kafka-penguin ${label}: ${error} correlationId: ${correlationId}`,
20 | );
21 | }
22 | };
23 |
24 | // new kafka instance with logCreator added
25 | const failfastKafka = new Kafka({
26 | clientId: 'makeClient',
27 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
28 | ssl: true,
29 | sasl: {
30 | mechanism: 'plain',
31 | username: process.env.KAFKA_USERNAME,
32 | password: process.env.KAFKA_PASSWORD,
33 | },
34 | logLevel: logLevel.ERROR,
35 | logCreator: MyLogCreator,
36 | });
37 |
38 | const failfast: RequestHandler = (req, res, next) => {
39 | const newStrategy = new FailFast(req.body.retries - 1, failfastKafka);
40 | const producer = newStrategy.producer();
41 | const message = {
42 | topic: req.body.topic,
43 | messages: [
44 | {
45 | key: 'hello',
46 | value: req.body.message,
47 | },
48 | ],
49 | };
50 | producer
51 | .connect()
52 | .then(() => console.log('Connected'))
53 | .then(() => producer.send(message))
54 | .then(() => {
55 | if (ERROR_LOG.length > 0) {
56 | const plural = ERROR_LOG.length > 1 ? 'times' : 'time';
57 | ERROR_LOG.push(
58 | `kafka-penguin: FailFast stopped producer after ${ERROR_LOG.length} ${plural}!`,
59 | );
60 | res.locals.error = [...ERROR_LOG];
61 | } else {
62 | res.locals.error = ['kafka-penguin: Message produced successfully'];
63 | }
64 |
65 | ERROR_LOG = [];
66 | return next();
67 | })
68 | .catch((e : Error) => next({
69 | message: `Error implementing FailFast strategy: ${e.message}`,
70 | error: e,
71 | }));
72 | };
73 |
74 | export default {
75 | failfast,
76 | };
77 |
--------------------------------------------------------------------------------
/demo/server/controllers/ignoreController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from 'express';
2 | import { Kafka, logLevel } from 'kafkajs';
3 | import { DeadLetterQueue } from 'kafka-penguin';
4 | import dotenv = require('dotenv');
5 |
6 | dotenv.config();
7 |
8 | const ERROR_LOG = [];
9 |
10 | const MyLogCreator = (logLevel) => ({
11 | namespace, label, log,
12 | }) => {
13 | // also availabe on log object => timestamp, logger, message and more
14 | const { error, correlationId } = log;
15 | if (correlationId) {
16 | ERROR_LOG.push(
17 | `[${namespace}] Logger: kafka-penguin ${label}: ${error} correlationId: ${correlationId}`,
18 | );
19 | }
20 | };
21 |
22 | const ignoreKafka = new Kafka({
23 | clientId: 'makeClient',
24 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
25 | ssl: true,
26 | sasl: {
27 | mechanism: 'plain',
28 | username: process.env.KAFKA_USERNAME,
29 | password: process.env.KAFKA_PASSWORD,
30 | },
31 | logLevel: logLevel.ERROR,
32 | logCreator: MyLogCreator,
33 | });
34 |
35 | const ignoreProduce: RequestHandler = (req, res, next) => {
36 | const {
37 | topic, message, retries, faults,
38 | } = req.body;
39 |
40 | const messagesArray = [];
41 | // create messages array with specified number of faults
42 | for (let i = 0; i < retries; i++) {
43 | if (i < faults) messagesArray.push({ key: 'test', value: 'fault' });
44 | else {
45 | messagesArray.push({
46 | key: 'test',
47 | value: message,
48 | });
49 | }
50 | }
51 |
52 | const cb = (message) => {
53 | if (message.value.toString() === 'fault') {
54 | return false;
55 | } return true;
56 | };
57 |
58 | const admin = ignoreKafka.admin();
59 | const ignoreClient = new DeadLetterQueue(ignoreKafka, topic, cb);
60 | const ignoreProducer = ignoreClient.producer();
61 | const ignoreConsumer = ignoreClient.consumer({ groupId: 'demo' });
62 |
63 | res.locals.ignoreClients = {
64 | consumer: ignoreConsumer,
65 | retries,
66 | faults,
67 | };
68 | // ignoreProducer.logger().info('TEST', {KAFKA_PENGUIN: 'TESTING CUSTOM'})
69 | ignoreProducer.connect()
70 | .then(() => {
71 | ignoreProducer.send({
72 | topic,
73 | messages: messagesArray,
74 | }).catch((e) => console.log('this is error in try', e.reference));
75 | })
76 | .then(ignoreProducer.disconnect())
77 | .then(admin.connect())
78 | .then(async () => {
79 | const offsetData = await admin.fetchTopicOffsets(topic);
80 | res.locals.latestOffset = offsetData[0].offset;
81 | })
82 | .then(admin.disconnect())
83 | .then(() => next())
84 | .catch((e: Error) => {
85 | if (e.message === 'This server does not host this topic-partition') {
86 | return res.status(300).json([`This error was executed as part of the kafka-penguin
87 | Ignore message reprocessing strategy.`]);
88 | }
89 | return next({
90 | message: `Error implementing Ignore strategy, producer side:${e.message}`,
91 | error: e,
92 | });
93 | });
94 | };
95 |
96 | const ignoreConsume: RequestHandler = (req, res, next) => {
97 | const { faults, consumer, retries } = res.locals.ignoreClients;
98 | const messageLog = [];
99 | consumer.connect()
100 | .then(consumer.subscribe())
101 | .then(() => {
102 | const latestOffset = Number(res.locals.latestOffset);
103 | consumer.run({
104 | eachMessage: ({ topic, partitions, message }) => {
105 | const messageOffset = Number(message.offset);
106 |
107 | if (messageOffset >= latestOffset - retries) {
108 | messageLog.push(message.value.toString());
109 | }
110 | if (messageLog.length === retries - faults) {
111 | messageLog.push(`kafka-penguin: Error with message processing, ${faults} ${faults > 1 ? 'messages' : 'message'}
112 | ignored.`);
113 | res.locals.messages = messageLog;
114 | consumer.disconnect()
115 | .then(() => res.status(200).json(res.locals.messages))
116 | .catch((e) => next({
117 | message: `Error implementing Ignore strategy while consuming messages, consumer side: ${e.message}`,
118 | error: e,
119 | }));
120 | }
121 | },
122 | });
123 | })
124 | .catch((e) => next({
125 | message: `Error implementing Ignore strategy, consumer side: ${e.message}`,
126 | error: e,
127 | }));
128 | };
129 |
130 | export default {
131 | ignoreConsume,
132 | ignoreProduce,
133 | };
134 |
--------------------------------------------------------------------------------
/demo/server/controllers/kafkaController.ts:
--------------------------------------------------------------------------------
1 | import { Kafka } from 'kafkajs';
2 | import { RequestHandler } from 'express';
3 |
4 | const makeClient: RequestHandler = (req, res, next) => {
5 | const brokers = !req.body.brokers ? process.env.KAFKA_BOOTSTRAP_SERVER : req.body.brokers;
6 | const kafka = new Kafka({
7 | clientId: 'makeClient',
8 | brokers: [brokers],
9 | ssl: true,
10 | sasl: {
11 | mechanism: 'plain',
12 | username: !req.body.username ? process.env.KAFKA_USERNAME : req.body.username,
13 | password: !req.body.password ? process.env.KAFKA_PASSWORD : req.body.password,
14 | },
15 | });
16 | res.locals.kafka = kafka;
17 | return next();
18 | };
19 |
20 | export default {
21 | makeClient,
22 | };
23 |
--------------------------------------------------------------------------------
/demo/server/controllers/strategyController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from 'express';
2 | import { Kafka, logLevel } from 'kafkajs';
3 | // const kafkapenguin = require('kafka-penguin');
4 | import { FailFast, DeadLetterQueue } from 'kafka-penguin';
5 | // import { DeadLetterQueue } from '../../../kafka-penguin/src/index'
6 | import dotenv = require('dotenv');
7 | dotenv.config();
8 | //cache to store error logs
9 | let ERROR_LOG = [];
10 | let MESSAGE_LOG = [];
11 |
12 |
13 | const MyLogCreator = logLevel => ({ namespace, level, label, log }) => {
14 | //also availabe on log object => timestamp, logger, message and more
15 | const { error, correlationId } = log;
16 | if (correlationId) {
17 | ERROR_LOG.push(
18 | `[${namespace}] Logger: kafka-penguin ${label}: ${error} correlationId: ${correlationId}`
19 | );
20 | }
21 | };
22 |
23 | //new kafka instance with logCreator added
24 | const strategyKafka = new Kafka({
25 | clientId: 'makeClient',
26 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
27 | ssl: true,
28 | sasl: {
29 | mechanism: 'plain',
30 | username: process.env.KAFKA_USERNAME,
31 | password: process.env.KAFKA_PASSWORD,
32 | },
33 | logLevel: logLevel.ERROR,
34 | logCreator: MyLogCreator,
35 | });
36 |
37 | const failfast: RequestHandler = (req, res, next) => {
38 | // const strategies = kafkapenguin.failfast;
39 | // const newStrategy = new strategies.FailFast(req.body.retries - 1, strategyKafka);
40 | const newStrategy = new FailFast(req.body.retries - 1, strategyKafka);
41 | const producer = newStrategy.producer();
42 | const message = {
43 | topic: req.body.topic,
44 | messages: [
45 | {
46 | key: 'hello',
47 | value: req.body.message,
48 | },
49 | ],
50 | };
51 | producer
52 | .connect()
53 | .then(() => console.log('Connected'))
54 | .then(() => producer.send(message))
55 | .then(() => {
56 | if (ERROR_LOG.length > 0) {
57 | const plural = ERROR_LOG.length > 1 ? 'times' : 'time';
58 | ERROR_LOG.push(
59 | `kafka-penguin: FailFast stopped producer after ${ERROR_LOG.length} ${plural}!`
60 | );
61 | res.locals.error = [...ERROR_LOG];
62 | } else {
63 | res.locals.error = ['kafka-penguin: Message produced successfully'];
64 | }
65 |
66 | ERROR_LOG = [];
67 | return next();
68 | })
69 | .catch(e => {
70 | return next({
71 | message: 'Error implementing FailFast strategy: ' + e.message,
72 | error: e,
73 | });
74 | });
75 | };
76 |
77 | const dlqProduce: RequestHandler = (req, res, next) => {
78 | //create messages array with specified number of faults
79 | const { topic, message, retries, faults } = req.body;
80 | const random = (count, messages, result = new Set()) => {
81 | if (result.size === count) return result;
82 | const num = Math.floor(Math.random() * messages);
83 | result.add(num);
84 | return random(count, messages, result);
85 | };
86 |
87 | const faultsIndex = random(faults, retries);
88 |
89 | const messagesArray = [];
90 |
91 | for (let i = 0; i < retries; i++) {
92 | if (faultsIndex.has(i)) messagesArray.push({ key: 'test', value: 'fault' });
93 | else
94 | messagesArray.push({
95 | key: 'test',
96 | value: message,
97 | });
98 | }
99 |
100 | const cb = message => {
101 | if (message === 'fault')
102 | return false;
103 | return true;
104 | };
105 | console.log('topic-----------', topic);
106 | console.log(messagesArray);
107 | const DLQClient = new DeadLetterQueue(strategyKafka, topic, cb);
108 | // produce message to topic
109 | const DLQProducer = DLQClient.producer();
110 | // const DLQProducer = res.locals.kafka.producer();
111 | const DLQConsumer = DLQClient.consumer({ groupId: 'demo' });
112 |
113 | DLQProducer.connect()
114 | .then(
115 | DLQProducer.send({
116 | topic: topic,
117 | messages: messagesArray,
118 | })
119 | )
120 | .then(DLQProducer.disconnect())
121 | .then(() => {
122 | // res.locals.clientInfo = {
123 | // brokers: process.env.KAFKA_BOOTSTRAP_SERVER,
124 | // username: process.env.KAFKA_USERNAME,
125 | // password: process.env.KAFKA_PASSWORD,
126 | // };
127 | res.locals.DLQClients = {
128 | producer: DLQProducer,
129 | consumer: DLQConsumer,
130 | retries: retries
131 | }
132 |
133 | return next();
134 | })
135 | .catch(e => {
136 | console.log(e);
137 | });
138 | };
139 |
140 | const dlqConsume: RequestHandler = (req, res, next) => {
141 | const { producer, consumer, retries } = res.locals.DLQClients
142 | let messageLog = [];
143 | consumer.connect()
144 | .then(consumer.subscribe())
145 | .then(() => {
146 | // const run = () => {
147 | consumer.run({
148 | eachMessage: ({topic, partitions, message}) => {
149 | console.log(message.value.toString())
150 | messageLog.push(message.value.toString())
151 | console.log(messageLog);
152 | if (messageLog.length === retries) {
153 | res.locals.messages = [...messageLog];
154 | messageLog = [];
155 | consumer.disconnect()
156 | .then(() => {
157 | return res.status(200).json(res.locals.messages)
158 | })
159 |
160 |
161 | }
162 | }
163 | })
164 | // }
165 | })
166 | .then(async () => {
167 | console.log('message log', await messageLog)
168 | })
169 | .then(console.log(messageLog))
170 | .catch((e: Error) => console.log)
171 |
172 | };
173 |
174 | export default {
175 | failfast,
176 | dlqProduce,
177 | dlqConsume,
178 | };
179 |
--------------------------------------------------------------------------------
/demo/server/controllers/topicsController.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { RequestHandler } from 'express';
3 |
4 | const getTopics: RequestHandler = (req, res, next) => {
5 | const { kafka } = res.locals;
6 | const admin = kafka.admin();
7 | admin.connect()
8 | .then(() => console.log('Connected'))
9 | .then(() => admin.fetchTopicMetadata())
10 | .then((data: {
11 | name: string,
12 | partitions: Array
13 | }) => { res.locals.topicsData = data; })
14 | .then(() => admin.disconnect())
15 | .then(() => next())
16 | .catch((e) => next({
17 | message: `Error getting topics in topicsController.getTopics ${e.message}`,
18 | error: e,
19 | }));
20 | };
21 |
22 | export default {
23 | getTopics,
24 | };
25 |
--------------------------------------------------------------------------------
/demo/server/routes/strategy.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import kafkaController from '../controllers/kafkaController';
3 | import failfastController from '../controllers/failfastController';
4 | import DLQController from '../controllers/DLQController';
5 | import ignoreController from '../controllers/ignoreController';
6 |
7 | const router = express.Router();
8 |
9 | router.post('/failfast',
10 | failfastController.failfast,
11 | (req, res) => res.status(200).json(res.locals.error));
12 |
13 | router.post(
14 | '/dlq',
15 | kafkaController.makeClient,
16 | DLQController.dlqProduce,
17 | DLQController.dlqConsume,
18 | (req, res) => res.status(200).json(res.locals.error),
19 | );
20 |
21 | router.post(
22 | '/ignore',
23 | kafkaController.makeClient,
24 | ignoreController.ignoreProduce,
25 | ignoreController.ignoreConsume,
26 | (req, res) => res.status(200).json(res.locals.error),
27 | );
28 |
29 | export default router;
30 |
--------------------------------------------------------------------------------
/demo/server/routes/topic.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import topicsController from '../controllers/topicsController';
3 | import kafkaController from '../controllers/kafkaController';
4 |
5 | const router = express.Router();
6 |
7 | router.post('/getTopics',
8 | kafkaController.makeClient,
9 | topicsController.getTopics,
10 | (req, res) => res.status(200).json(res.locals.topicsData));
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/demo/server/server.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import app from './app';
3 | // const app = require('./app');
4 | const PORT = process.env.PORT || 3000;
5 |
6 | app.listen(PORT, () => {
7 | console.log(`Server listening on port ${PORT}`);
8 | });
9 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // "types": ["node"],
4 | "typeRoots": ["./types", "./node_modules/@types"],
5 | "target": "ES5",
6 | "module": "commonJS",
7 | "jsx": "react",
8 | "sourceMap": true,
9 | "outDir": "./build",
10 | "removeComments": true,
11 | "allowSyntheticDefaultImports": true,
12 | "esModuleInterop": true,
13 | "preserveConstEnums": true
14 | },
15 | "include": [
16 | "./client/**/*",
17 | ],
18 | "exclude": [
19 | "node_modules"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/demo/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["jest", "node"],
4 | "typeRoots": ["./types", "./node_modules/@types"],
5 | "target": "ES5",
6 | "module": "commonJS",
7 | "jsx": "react",
8 | "sourceMap": true,
9 | "outDir": "./build",
10 | "removeComments": true,
11 | "allowSyntheticDefaultImports": true,
12 | "esModuleInterop": true,
13 | "preserveConstEnums": true
14 | },
15 | "include": [
16 | "./client/**/*",
17 | ],
18 | "exclude": [
19 | "node_modules"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 |
3 | const path = require('path')
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 |
6 | module.exports = {
7 | mode: "production",
8 | entry: {
9 | app: ['./client/index.tsx'],
10 | vendor: ['react', 'react-dom']
11 | },
12 | output: {
13 | path: path.resolve(__dirname, 'build'),
14 | filename: 'js/[name].bundle.js'
15 | },
16 | devtool: 'source-map',
17 | resolve: {
18 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
19 | },
20 | devServer: {
21 | contentBase: './build',
22 | port: 8000,
23 | proxy: {
24 | '/': 'http://localhost:3000'
25 | }
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.(ts|js)x?$/,
31 | exclude: '/node_modules',
32 | loader: 'babel-loader'
33 | }
34 | ]
35 | },
36 | plugins: [
37 | new HtmlWebpackPlugin({
38 | template: path.resolve(__dirname, './client/index.html')
39 | })
40 | ]
41 | }
--------------------------------------------------------------------------------
/exampleBasic.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { FailFast } from 'kafka-penguin';
3 |
4 | const exampleClient = require('./clientConfig.ts');
5 |
6 | // Set up the preferred strategy with a configured KafkaJS client
7 | const exampleStrategy = new FailFast(2, exampleClient);
8 |
9 | // Initialize a producer or consumer from the instance of the strategy
10 | const producer = exampleStrategy.producer();
11 |
12 | const message = {
13 | topic: 'wrong-topic',
14 | messages: [
15 | {
16 | key: 'hello',
17 | value: 'world',
18 | },
19 | ],
20 | };
21 |
22 | // Connect, Subscribe, Send, or Run virtually the same as with KafkaJS
23 | producer.connect()
24 | .then(() => console.log('Connected!'))
25 | // The chosen strategy executes under the hood, like in this send method
26 | .then(() => producer.send(message))
27 | .catch((e: any) => console.log('error: ', e.message));
28 |
--------------------------------------------------------------------------------
/exampleDLQConsumer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { DeadLetterQueue } from 'kafka-penguin';
3 |
4 | const client = require('./clientConfig.ts');
5 |
6 | const topic = 'test-topic-DLQ';
7 |
8 | // This allows the consumer to evaluate each message according to a condition
9 | // The callback must return a boolean value
10 | const callback = (message) => {
11 | try {
12 | if (typeof message.value === 'string') {
13 | return true;
14 | }
15 | } catch (e) {
16 | return false;
17 | }
18 | return true;
19 | };
20 |
21 | // Set up the Dead Letter Queue (DLQ) strategy
22 | // with a configured KafkaJS client, a topic, and the evaluating callback
23 | const exampleDLQConsumer = new DeadLetterQueue(client, topic, callback);
24 |
25 | // Initialize a consumer from the new instance of the Dead Letter Queue strategy
26 | const consumerDLQ = exampleDLQConsumer.consumer({ groupId: 'testID' });
27 |
28 | // Connecting the consumer creates a DLQ topic in case of bad messages
29 | // If the callback returns false, the strategy moves the message to the topic specific DLQ
30 | // The consumer is able to keep consuming good messages from the topic
31 | consumerDLQ.connect()
32 | .then(consumerDLQ.subscribe())
33 | .then(() => consumerDLQ.run({
34 | eachMessage: ({ message }) => {
35 | if (message.value.length < 5) return true;
36 | return false;
37 | },
38 | }))
39 | .catch((e) => console.log(`Error message from consumer: ${e}`));
40 |
--------------------------------------------------------------------------------
/exampleDLQProducer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { DeadLetterQueue } from 'kafka-penguin';
3 |
4 | const producerClientDLQ = require('./clientConfig.ts');
5 |
6 | // This example simulates an error where the producer sends to a bad topic
7 | const topicGood = 'test-topic-DLQ';
8 | const topicBad = 'topic-non-existent';
9 |
10 | // Set up the Dead Letter Queue (DLQ) strategy
11 | // Configure it with a configured KafkaJS client, a topic, and a callback that returns boolean
12 | const exampleDLQProducer = new DeadLetterQueue(producerClientDLQ, topicGood, true);
13 |
14 | // Initialize a producer from the new instance of the Dead Letter Queue strategy
15 | const producerDLQ = exampleDLQProducer.producer();
16 |
17 | // Connecting the producer creates a DLQ topic in case of bad messages
18 | // If an error occurs, the strategy moves the message to the topic specific DLQ
19 | // The producer is able to keep publishing good messages to the topic
20 | producerDLQ.connect()
21 | .then(() => producerDLQ.send({
22 | topic: topicGood,
23 | messages: [
24 | {
25 | key: 'message 1',
26 | value: 'Good Message',
27 | },
28 | ],
29 | }))
30 | .then(() => producerDLQ.send({
31 | topic: topicBad,
32 | messages: [
33 | {
34 | key: 'message 2',
35 | value: 'Bad Message',
36 | },
37 | ],
38 | }))
39 | .then(() => producerDLQ.send({
40 | topic: topicGood,
41 | messages: [
42 | {
43 | key: 'message 3',
44 | value: 'Good Message',
45 | },
46 | ],
47 | }))
48 | .then(() => producerDLQ.disconnect())
49 | .catch((e: any) => {
50 | console.log(e);
51 | });
52 |
--------------------------------------------------------------------------------
/exampleFailFast.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { FailFast } from './kafka-penguin/src/index';
3 |
4 | const FailFastClient = require('../src/clientConfig.ts');
5 |
6 | // Set up Fail Fast with the number of retried and a configured KafkaJS client
7 | const exampleFailFast = new FailFast(2, FailFastClient);
8 |
9 | // Initialize a producer from the new instance of Fail Fast
10 | const producer = exampleFailFast.producer();
11 |
12 | // Example error of a producer sending to a non-existent topic
13 | const message = {
14 | topic: 'topic-non-existent',
15 | messages: [
16 | {
17 | key: 'hello',
18 | value: 'world',
19 | },
20 | ],
21 | };
22 |
23 | // Fail Fast will attempt to send the message to the Kafka cluster.
24 | // After the retry count is reached, the producer will automatically disconnect.
25 | // An Error is also thrown.
26 | producer.connect()
27 | .then(() => console.log('Connected!'))
28 | .then(() => producer.send(message))
29 | .catch((e: any) => console.log('error: ', e.message));
30 |
--------------------------------------------------------------------------------
/exampleIgnoreConsumer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Ignore } from './kafka-penguin/src/index';
3 |
4 | const client = require('./clientConfig.ts');
5 |
6 | const topic = 'test-topic';
7 |
8 | // This allows the consumer to evaluate each message according to a condition
9 | // The callback must return a boolean value
10 | const callback = (message) => {
11 | try {
12 | JSON.parse(message.value);
13 | } catch (e) {
14 | return false;
15 | }
16 | return true;
17 | };
18 |
19 | // Set up the Ignore strategy
20 | // with a configured KafkaJS client, a topic, and the evaluating callback
21 | const exampleIgnoreConsumer = new Ignore(client, topic, callback);
22 |
23 | // Initialize a consumer from the new instance of the Dead Letter Queue strategy
24 | const consumerIgnore = exampleIgnoreConsumer.consumer({ groupId: 'testID' });
25 |
26 | // Connecting the consumer to consume messages. bad messages
27 | // If the callback evaluates a message as erroneous by returning false, the strategy
28 | // enables the consumer to keep consuming good messages from the topic
29 | consumerIgnore.connect()
30 | .then(consumerIgnore.subscribe())
31 | .then(() => consumerIgnore.run({
32 | eachMessage: ({ message }) => {
33 | // if (message.value.length < 5) return true;
34 | // return false;
35 | console.log("message value:", message.value.toString())
36 | },
37 | }))
38 | .catch((e) => console.log(`Error message from consumer: ${e}`));
39 |
--------------------------------------------------------------------------------
/exampleIgnoreProducer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Ignore } from './kafka-penguin/src/index'
3 |
4 | const producerClientIgnore = require('./clientConfig.ts');
5 |
6 | // This example simulates an error where the producer sends to a bad topic
7 | const topicGood = 'test-topic';
8 | const topicBad = 'topic-non-existent';
9 |
10 | // Set up the Ignore strategy
11 | // Configure it with a configured KafkaJS client, a topic, and a callback that returns boolean
12 | const exampleIgnoreProducer = new Ignore(producerClientIgnore, topicGood, true);
13 |
14 | // Initialize a producer from the new instance of the Ignore strategy
15 | const producerIgnore = exampleIgnoreProducer.producer();
16 |
17 | // Connecting the producer and send messages.
18 | // If an error occurs with a message, the strategy ignores erroneous message and continues
19 | // publishing good messages to the topic
20 | producerIgnore.connect()
21 | .then(() => producerIgnore.send({
22 | topic: topicGood,
23 | messages: [
24 | {
25 | key: 'message 1',
26 | value: JSON.stringify('Good Message'),
27 | },
28 | ],
29 | }))
30 | .then(() => producerIgnore.send({
31 | topic: topicBad,
32 | messages: [
33 | {
34 | key: 'message 2',
35 | value: 'Bad Message',
36 | },
37 | ],
38 | }))
39 | .then(() => producerIgnore.send({
40 | topic: topicGood,
41 | messages: [
42 | {
43 | key: 'message 3',
44 | value: JSON.stringify('Good Message'),
45 | },
46 | ],
47 | }))
48 | .then(() => producerIgnore.disconnect())
49 | .catch((e: any) => {
50 | console.log(e);
51 | });
52 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | };
5 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | /*
3 | * For a detailed explanation regarding each configuration property and type check, visit:
4 | * https://jestjs.io/docs/en/configuration.html
5 | */
6 |
7 | export default {
8 | // All imported modules in your tests should be mocked automatically
9 | // automock: false,
10 |
11 | // Stop running tests after `n` failures
12 | // bail: 0,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/r7/gl7yvbmn0_lf04yblmhmxp180000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: undefined,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: 'coverage',
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // Indicates which provider should be used to instrument code for coverage
35 | // coverageProvider: "babel",
36 |
37 | // A list of reporter names that Jest uses when writing coverage reports
38 | // coverageReporters: [
39 | // "json",
40 | // "text",
41 | // "lcov",
42 | // "clover"
43 | // ],
44 |
45 | // An object that configures minimum threshold enforcement for coverage results
46 | // coverageThreshold: undefined,
47 |
48 | // A path to a custom dependency extractor
49 | // dependencyExtractor: undefined,
50 |
51 | // Make calling deprecated APIs throw helpful error messages
52 | // errorOnDeprecated: false,
53 |
54 | // Force coverage collection from ignored files using an array of glob patterns
55 | // forceCoverageMatch: [],
56 |
57 | // A path to a module which exports an async function that is triggered once before all test suites
58 | // globalSetup: undefined,
59 |
60 | // A path to a module which exports an async function that is triggered once after all test suites
61 | // globalTeardown: undefined,
62 |
63 | // A set of global variables that need to be available in all test environments
64 | // globals: {},
65 |
66 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
67 | // maxWorkers: "50%",
68 |
69 | // An array of directory names to be searched recursively up from the requiring module's location
70 | // moduleDirectories: [
71 | // "node_modules"
72 | // ],
73 |
74 | // An array of file extensions your modules use
75 | // moduleFileExtensions: [
76 | // "js",
77 | // "json",
78 | // "jsx",
79 | // "ts",
80 | // "tsx",
81 | // "node"
82 | // ],
83 |
84 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
85 | // moduleNameMapper: {},
86 |
87 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
88 | // modulePathIgnorePatterns: [],
89 |
90 | // Activates notifications for test results
91 | // notify: false,
92 |
93 | // An enum that specifies notification mode. Requires { notify: true }
94 | // notifyMode: "failure-change",
95 |
96 | // A preset that is used as a base for Jest's configuration
97 | // preset: undefined,
98 |
99 | // Run tests from one or more projects
100 | // projects: undefined,
101 |
102 | // Use this configuration option to add custom reporters to Jest
103 | // reporters: undefined,
104 |
105 | // Automatically reset mock state between every test
106 | // resetMocks: false,
107 |
108 | // Reset the module registry before running each individual test
109 | // resetModules: false,
110 |
111 | // A path to a custom resolver
112 | // resolver: undefined,
113 |
114 | // Automatically restore mock state between every test
115 | // restoreMocks: false,
116 |
117 | // The root directory that Jest should scan for tests and modules within
118 | // rootDir: undefined,
119 |
120 | // A list of paths to directories that Jest should use to search for files in
121 | // roots: [
122 | // ""
123 | // ],
124 |
125 | // Allows you to use a custom runner instead of Jest's default test runner
126 | // runner: "jest-runner",
127 |
128 | // The paths to modules that run some code to configure or set up the testing environment before each test
129 | // setupFiles: [],
130 |
131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
132 | // setupFilesAfterEnv: [],
133 |
134 | // The number of seconds after which a test is considered as slow and reported as such in the results.
135 | // slowTestThreshold: 5,
136 |
137 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
138 | // snapshotSerializers: [],
139 |
140 | // The test environment that will be used for testing
141 | testEnvironment: 'node',
142 |
143 | // Options that will be passed to the testEnvironment
144 | // testEnvironmentOptions: {},
145 |
146 | // Adds a location field to test results
147 | // testLocationInResults: false,
148 |
149 | // The glob patterns Jest uses to detect test files
150 | // testMatch: [
151 | // "**/__tests__/**/*.[jt]s?(x)",
152 | // "**/?(*.)+(spec|test).[tj]s?(x)"
153 | // ],
154 |
155 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
156 | // testPathIgnorePatterns: [
157 | // "/node_modules/"
158 | // ],
159 |
160 | // The regexp pattern or array of patterns that Jest uses to detect test files
161 | // testRegex: [],
162 |
163 | // This option allows the use of a custom results processor
164 | // testResultsProcessor: undefined,
165 |
166 | // This option allows use of a custom test runner
167 | // testRunner: "jasmine2",
168 |
169 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
170 | // testURL: "http://localhost",
171 |
172 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
173 | // timers: "real",
174 |
175 | // A map from regular expressions to paths to transformers
176 | // transform: undefined,
177 |
178 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
179 | // transformIgnorePatterns: [
180 | // "/node_modules/",
181 | // "\\.pnp\\.[^\\/]+$"
182 | // ],
183 |
184 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
185 | // unmockedModulePathPatterns: undefined,
186 |
187 | // Indicates whether each individual test should be reported during the run
188 | // verbose: undefined,
189 |
190 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
191 | // watchPathIgnorePatterns: [],
192 |
193 | // Whether to use watchman for file crawling
194 | // watchman: true,
195 | };
196 |
--------------------------------------------------------------------------------
/kafka-penguin/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/kafka-penguin/README.md:
--------------------------------------------------------------------------------
1 | # Home
2 |
3 |    [](https://github.com/oslabs-beta/kafka-penguin/actions) [](https://www.npmjs.com/package/kafka-penguin)
4 |
5 | ### About
6 |
7 | Kafka-Penguin is an easy-to-use, lightweight KafkaJS plugin for message re-processing. It provides developers with three strategies for setting up message re-processing: FailFast, Ignore, and Dead Letter Queue.
8 |
9 | The package allows developers to build event-driven applications with dedicated "fail strategies" modeled after best practices in the field. This in turn allows developers to effectively address bugs in development and deploy more fault-tolerant systems in production.
10 |
11 | This package is meant to work in conjunction with with KafkaJS. For more information on KafkaJS, check out [Getting Started with KafkaJS](https://kafka.js.org/docs/getting-started).
12 |
13 | ### Getting Started
14 |
15 | Install Kafka-Penguin as an npm module and save it to your package.json file as a dependency:
16 |
17 | ```text
18 | npm install kafka-penguin
19 | ```
20 |
21 | Once installed it can now be referenced by simply calling `require('kafka-penguin');`
22 |
23 | ## Example
24 |
25 | All Kafka-Penguin needs is a KafkaJS client to run. Start by passing the client for your preferred strategy and Kafka-Penguin will create bespoke consumers, producers, and admins with built-in functionality to execute the chosen strategy. On the surface, you implement your application exactly as you would with KafkaJS.
26 |
27 | ```text
28 | /* eslint-disable no-console */
29 | import { FailFast } from 'kafka-penguin';
30 |
31 | const exampleClient = require('./clientConfig.ts');
32 |
33 | // Set up the preferred strategy with a configured KafkaJS client
34 | const exampleStrategy = new FailFast(2, exampleClient);
35 |
36 | // Initialize a producer or consumer from the instance of the strategy
37 | const producer = exampleStrategy.producer();
38 |
39 | const message = {
40 | topic: 'wrong-topic',
41 | messages: [
42 | {
43 | key: 'hello',
44 | value: 'world',
45 | },
46 | ],
47 | };
48 |
49 | // Connect, Subscribe, Send, or Run virtually the same as with KafkaJS
50 | producer.connect()
51 | .then(() => console.log('Connected!'))
52 | // The chosen strategy executes under the hood, like in this send method
53 | .then(() => producer.send(message))
54 | .catch((e: any) => console.log('error: ', e.message));
55 | ```
56 |
57 | ## Strategies
58 |
59 | Dive in deeper to any of the strategies for set up, execution, and implementation.
60 |
61 | [FailFast](https://app.gitbook.com/@kafka-penguin-1/s/kafka-penguin/~/drafts/-MYCUDw3CJmXz95ljr5N/strategies/readme/strategies-readme-fail-fast/@merged)
62 |
63 | [Ignore](https://app.gitbook.com/@kafka-penguin-1/s/kafka-penguin/~/drafts/-MYCUDw3CJmXz95ljr5N/strategies/readme/strategies-readme-ignore/@merged)
64 |
65 | [Dead Letter Queue](https://app.gitbook.com/@kafka-penguin-1/s/kafka-penguin/~/drafts/-MYCUDw3CJmXz95ljr5N/strategies/readme/strategies-readme-dlq/@merged)
66 |
67 | ## **Contributors**
68 |
69 | [Ausar English](https://www.linkedin.com/in/ausarenglish) [@ausarenglish](https://github.com/ausarenglish)
70 |
71 | [Kushal Talele](https://www.linkedin.com/in/kushal-talele-29040820b/) [@ktrane1](https://github.com/ktrane1)
72 |
73 | [Timeo Williams](https://www.linkedin.com/in/timeowilliams/) [@timeowilliams](https://github.com/timeowilliams)
74 |
75 | [Ziyad El Baz](https://www.linkedin.com/in/ziyadelbaz) [@zelbaz946](https://github.com/zelbaz946)
76 |
77 | ### License
78 |
79 | This product is licensed under the MIT License - see the [LICENSE.md](https://github.com/oslabs-beta/kafka-penguin/blob/main/LICENSE) file for details.
80 |
81 | This is an open source product. We are not affiliated nor endorsed by either the Apache Software Foundation or KafkaJS.
82 |
83 | This product is accelerated by [OS Labs](https://opensourcelabs.io/).
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/kafka-penguin/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
3 | };
4 |
--------------------------------------------------------------------------------
/kafka-penguin/dist/clientConfig.d.ts:
--------------------------------------------------------------------------------
1 | declare const kafka: any;
2 | export default kafka;
3 |
--------------------------------------------------------------------------------
/kafka-penguin/dist/clientConfig.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const { Kafka } = require('kafkajs');
4 | require('dotenv').config();
5 | // Create the client with the broker list
6 | const kafka = new Kafka({
7 | clientId: 'fail-fast-producer',
8 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
9 | ssl: true,
10 | sasl: {
11 | mechanism: 'plain',
12 | username: process.env.KAFKA_USERNAME,
13 | password: process.env.KAFKA_PASSWORD,
14 | },
15 | });
16 | exports.default = kafka;
17 |
--------------------------------------------------------------------------------
/kafka-penguin/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import { CompressionTypes } from 'kafkajs';
2 | interface messageValue {
3 | topic: string;
4 | messages: object[];
5 | }
6 | export declare class FailFastError extends Error {
7 | message: any;
8 | reference: any;
9 | name: any;
10 | retryCount: number;
11 | strategy: string;
12 | originalError: any;
13 | constructor(e: any);
14 | }
15 | export declare class FailFast {
16 | retry: number;
17 | client: any;
18 | innerProducer: any;
19 | constructor(num: number, kafkaJSClient: any);
20 | producer(): this;
21 | connect(): any;
22 | disconnect(): any;
23 | send(message: messageValue): any;
24 | }
25 | export declare class DeadLetterQueueErrorConsumer extends Error {
26 | message: any;
27 | reference: any;
28 | name: any;
29 | retryCount: number;
30 | strategy: string;
31 | originalError: any;
32 | constructor(e: any);
33 | }
34 | export declare class DeadLetterQueueErrorProducer extends Error {
35 | message: any;
36 | reference: any;
37 | name: any;
38 | retryCount: number;
39 | strategy: string;
40 | originalError: any;
41 | constructor(e: any);
42 | }
43 | export declare class DeadLetterQueue {
44 | client: any;
45 | topic: string;
46 | callback?: (message: any) => boolean;
47 | innerConsumer: any;
48 | admin: any;
49 | innerProducer: any;
50 | constructor(client: any, topic: string, callback?: any);
51 | producer(): any;
52 | consumer(groupId: {
53 | groupId: string;
54 | }): any;
55 | createDLQ(): Promise;
56 | }
57 | export declare class IgnoreErrorProducer extends Error {
58 | message: any;
59 | reference: any;
60 | name: any;
61 | retryCount: number;
62 | strategy: string;
63 | originalError: any;
64 | constructor(e: any);
65 | }
66 | export declare class IgnoreErrorConsumer extends Error {
67 | message: any;
68 | reference: any;
69 | name: any;
70 | retryCount: number;
71 | strategy: string;
72 | originalError: any;
73 | constructor(e: any);
74 | }
75 | interface messageValue {
76 | acks?: Number;
77 | timeout?: Number;
78 | compression?: CompressionTypes;
79 | topic: string;
80 | messages: object[];
81 | }
82 | export default class Ignore {
83 | client: any;
84 | topic: string;
85 | callback?: (message: any) => boolean;
86 | innerConsumer: any;
87 | admin: any;
88 | innerProducer: any;
89 | constructor(client: any, topic: string, callback?: any);
90 | producer(): any;
91 | consumer(groupId: {
92 | groupId: string;
93 | }): any;
94 | }
95 | export {};
96 |
--------------------------------------------------------------------------------
/kafka-penguin/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | Object.defineProperty(exports, "__esModule", { value: true });
12 | exports.IgnoreErrorConsumer = exports.IgnoreErrorProducer = exports.DeadLetterQueue = exports.DeadLetterQueueErrorProducer = exports.DeadLetterQueueErrorConsumer = exports.FailFast = exports.FailFastError = void 0;
13 | // Fail Fast Strategy
14 | class FailFastError extends Error {
15 | constructor(e) {
16 | super(e);
17 | Error.captureStackTrace(this, this.constructor);
18 | this.strategy = 'Fail Fast';
19 | this.reference = `This error was executed as part of the kafka-penguin Fail Fast message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful. As a result, the producer successfully executed a disconnect operation. Refer to the original error for further information`;
20 | this.name = e.name;
21 | this.message = e.message;
22 | this.originalError = e.originalError;
23 | this.retryCount = e.retryCount;
24 | }
25 | }
26 | exports.FailFastError = FailFastError;
27 | class FailFast {
28 | constructor(num, kafkaJSClient) {
29 | this.retry = num;
30 | this.client = kafkaJSClient;
31 | this.innerProducer = null;
32 | }
33 | producer() {
34 | const options = {
35 | retry: { retries: this.retry },
36 | };
37 | // Create a producer from client passing in retry options
38 | // Save to FailFast class
39 | this.innerProducer = this.client.producer(options);
40 | // Return curr FailFast instance instead of a producer
41 | return this;
42 | }
43 | connect() {
44 | return this.innerProducer.connect();
45 | }
46 | disconnect() {
47 | return this.innerProducer.disconnect();
48 | }
49 | send(message) {
50 | return this.innerProducer.send(message)
51 | .catch((e) => {
52 | this.innerProducer.disconnect();
53 | const newError = new FailFastError(e);
54 | // eslint-disable-next-line no-console
55 | console.log(newError);
56 | });
57 | }
58 | }
59 | exports.FailFast = FailFast;
60 | // Dead Letter Queue
61 | class DeadLetterQueueErrorConsumer extends Error {
62 | constructor(e) {
63 | super(e);
64 | Error.captureStackTrace(this, this.constructor);
65 | this.strategy = 'Dead Letter Queue';
66 | this.reference = `This error was executed as part of the kafka-penguin Dead Letter Queue message reprocessing strategy. Your consumer attempted to receive a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
67 | this.name = `${e.name}(Consumer Side)`;
68 | this.message = e.message;
69 | this.originalError = e.originalError;
70 | this.retryCount = e.retryCount;
71 | }
72 | }
73 | exports.DeadLetterQueueErrorConsumer = DeadLetterQueueErrorConsumer;
74 | class DeadLetterQueueErrorProducer extends Error {
75 | constructor(e) {
76 | super(e);
77 | Error.captureStackTrace(this, this.constructor);
78 | this.strategy = 'Dead Letter Queue';
79 | this.reference = `This error was executed as part of the kafka-penguin Dead Letter Queue message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
80 | this.name = `${e.name}(Producer Side)`;
81 | this.message = e.message;
82 | this.originalError = e.originalError;
83 | this.retryCount = e.retryCount;
84 | }
85 | }
86 | exports.DeadLetterQueueErrorProducer = DeadLetterQueueErrorProducer;
87 | class DeadLetterQueue {
88 | constructor(client, topic, callback) {
89 | this.topic = topic;
90 | this.client = client;
91 | this.callback = callback;
92 | this.admin = this.client.admin();
93 | this.innerConsumer = null;
94 | this.innerProducer = this.client.producer();
95 | }
96 | producer() {
97 | // Reference the DLQ instance for closure in the returned object
98 | const dlqInstance = this;
99 | const { innerProducer } = dlqInstance;
100 | // Return an object with all Producer methods adapted to execute a dead letter queue strategy
101 | console.log('INNER PRODUCER', innerProducer);
102 | return Object.assign(Object.assign({}, innerProducer), { connect() {
103 | return innerProducer.connect()
104 | .then(() => {
105 | dlqInstance.createDLQ();
106 | })
107 | .catch((e) => console.log(e));
108 | },
109 | send(message) {
110 | return innerProducer.connect()
111 | .then(() => {
112 | innerProducer.send(Object.assign(Object.assign({}, message), { topic: message.topic, messages: message.messages }))
113 | // Upon error, reroute message to DLQ for the strategy topic
114 | .catch((e) => {
115 | innerProducer.send({
116 | messages: message.messages,
117 | topic: `${dlqInstance.topic}.deadLetterQueue`,
118 | })
119 | .then(innerProducer.disconnect())
120 | .catch((e) => console.log(e));
121 | // Print the error to the console
122 | const newError = new DeadLetterQueueErrorProducer(e);
123 | console.log(newError);
124 | });
125 | });
126 | } });
127 | }
128 | consumer(groupId) {
129 | this.innerConsumer = this.client.consumer(groupId);
130 | const dlqInstance = this;
131 | const { innerConsumer, innerProducer } = dlqInstance;
132 | // Returns an object with all Consumer methods adapter to execute a dead letter queue strategy
133 | return Object.assign(Object.assign({}, innerConsumer), { connect() {
134 | return innerConsumer.connect().then(() => {
135 | dlqInstance.createDLQ();
136 | });
137 | }, subscribe(input) {
138 | return innerConsumer.subscribe(Object.assign(Object.assign({}, input), { topic: dlqInstance.topic, fromBeginning: false }));
139 | },
140 | run(input) {
141 | const { eachMessage } = input;
142 | return innerConsumer.run(Object.assign(Object.assign({}, input), { eachMessage: ({ topic, partitions, message }) => {
143 | try {
144 | // If user doesn't pass in callback, DLQ simply listens and returns errors
145 | if (dlqInstance.callback) {
146 | if (!dlqInstance.callback(message))
147 | throw Error;
148 | eachMessage({ topic, partitions, message });
149 | }
150 | }
151 | catch (e) {
152 | const newError = new DeadLetterQueueErrorConsumer(e);
153 | console.error(newError);
154 | innerProducer.connect()
155 | .then(() => console.log('kafka-penguin: Connected to DLQ topic'))
156 | .then(() => {
157 | innerProducer.send({
158 | topic: `${dlqInstance.topic}.deadLetterQueue`,
159 | messages: [message],
160 | });
161 | })
162 | .then(() => console.log('kafka-penguin: Message published to DLQ'))
163 | .then(() => innerProducer.disconnect())
164 | .then(() => console.log('kafka-penguin: Producer disconnected'))
165 | .catch((e) => console.log('Error with producing to DLQ: ', e));
166 | }
167 | } }));
168 | } });
169 | }
170 | // Creates a new DLQ topic with the original topic name
171 | createDLQ() {
172 | return __awaiter(this, void 0, void 0, function* () {
173 | const adminCreateDLQ = yield this.admin.connect()
174 | .then(() => __awaiter(this, void 0, void 0, function* () {
175 | yield this.admin.createTopics({
176 | topics: [{
177 | topic: `${this.topic}.deadLetterQueue`,
178 | numPartitions: 1,
179 | replicationFactor: 1,
180 | replicaAssignment: [{ partition: 0, replicas: [0, 1, 2] }],
181 | }],
182 | });
183 | }))
184 | .then(() => this.admin.disconnect())
185 | .catch((err) => console.log('Error from createDLQ', err));
186 | return adminCreateDLQ;
187 | });
188 | }
189 | }
190 | exports.DeadLetterQueue = DeadLetterQueue;
191 | // Ignore
192 | class IgnoreErrorProducer extends Error {
193 | constructor(e) {
194 | super(e);
195 | Error.captureStackTrace(this, this.constructor);
196 | this.strategy = 'Ignore';
197 | this.reference = `This error was executed as part of the kafka-penguin Ignore message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful.`;
198 | this.name = `${e.name} (Producer Side)`;
199 | this.message = e.message;
200 | this.originalError = e.originalError;
201 | this.retryCount = e.retryCount;
202 | }
203 | }
204 | exports.IgnoreErrorProducer = IgnoreErrorProducer;
205 | class IgnoreErrorConsumer extends Error {
206 | constructor(e) {
207 | super(e);
208 | Error.captureStackTrace(this, this.constructor);
209 | this.strategy = 'Ignore';
210 | this.reference = `This error was executed as part of the kafka-penguin Ignore message reprocessing strategy. Your consumer attempted to receive a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
211 | this.name = `${e.name} (Consumer Side)`;
212 | this.message = e.message;
213 | this.originalError = e.originalError;
214 | this.retryCount = e.retryCount;
215 | }
216 | }
217 | exports.IgnoreErrorConsumer = IgnoreErrorConsumer;
218 | class Ignore {
219 | constructor(client, topic, callback) {
220 | this.topic = topic;
221 | this.client = client;
222 | this.callback = callback;
223 | this.admin = this.client.admin();
224 | this.innerConsumer = null;
225 | this.innerProducer = this.client.producer();
226 | }
227 | producer() {
228 | // Reference the Ignore instance for closure in the returned object
229 | const ignoreInstance = this;
230 | const { innerProducer } = ignoreInstance;
231 | // Return an object with all Producer methods adapted to execute Ignore strategy
232 | console.log('INNER PRODUCER', innerProducer);
233 | return Object.assign(Object.assign({}, innerProducer), { connect() {
234 | return innerProducer.connect()
235 | .catch((e) => console.log(e));
236 | },
237 | send(message) {
238 | return innerProducer.connect()
239 | .then(() => {
240 | innerProducer.send(Object.assign(Object.assign({}, message), { topic: message.topic, messages: message.messages }))
241 | .catch((e) => {
242 | console.log(e);
243 | // Print the error to the console
244 | const newError = new IgnoreErrorProducer(e);
245 | console.log(newError);
246 | });
247 | });
248 | } });
249 | }
250 | consumer(groupId) {
251 | this.innerConsumer = this.client.consumer(groupId);
252 | const ignoreInstance = this;
253 | const { innerConsumer } = ignoreInstance;
254 | // Returns an object with all Consumer methods adapter to execute ignore strategy
255 | return Object.assign(Object.assign({}, innerConsumer), { connect() {
256 | return innerConsumer.connect();
257 | }, subscribe(input) {
258 | return innerConsumer.subscribe(Object.assign(Object.assign({}, input), { topic: ignoreInstance.topic, fromBeginning: false }));
259 | },
260 | run(input) {
261 | const { eachMessage } = input;
262 | return innerConsumer.run(Object.assign(Object.assign({}, input), { eachMessage: ({ topic, partitions, message }) => {
263 | try {
264 | // If user doesn't pass in callback, DLQ simply listens and returns errors
265 | if (ignoreInstance.callback) {
266 | if (!ignoreInstance.callback(message))
267 | throw Error;
268 | eachMessage({ topic, partitions, message });
269 | }
270 | }
271 | catch (e) {
272 | const newError = new IgnoreErrorConsumer(e);
273 | console.error(newError);
274 | }
275 | } }));
276 | } });
277 | }
278 | }
279 | exports.default = Ignore;
280 |
--------------------------------------------------------------------------------
/kafka-penguin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kafka-penguin",
3 | "version": "0.3.8",
4 | "description": "An easy-to-use, light weight KafkaJS library for message re-processing strategies.",
5 | "main": "./dist/index.js",
6 | "scripts": {
7 | "start": "webpack serve --open",
8 | "build": "tsc",
9 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve --config webpack.config.js --mode development\" \"nodemon demo/server/server.ts\"",
10 | "test": "jest",
11 | "lint": "eslint . --ext .ts"
12 | },
13 | "keywords": [
14 | "Kafka",
15 | "Message Re-processing",
16 | "Micro-services",
17 | "Event-driven",
18 | "Dead Letter Queue"
19 | ],
20 | "author": "",
21 | "license": "MIT",
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/oslabs-beta/kafka-penguin"
25 | },
26 | "dependencies": {
27 | "@babel/plugin-syntax-jsx": "^7.12.13",
28 | "@babel/plugin-transform-runtime": "^7.13.10",
29 | "dotenv": "^8.2.0",
30 | "kafkajs": "^1.15.0",
31 | "regenerator-runtime": "^0.13.7",
32 | "typescript": "^3.0.0",
33 | "util": "^0.12.3"
34 | },
35 | "devDependencies": {
36 | "@babel/core": "^7.13.15",
37 | "@babel/preset-env": "^7.13.15",
38 | "@babel/preset-react": "^7.13.13",
39 | "@babel/preset-typescript": "^7.13.0",
40 | "@types/express": "^4.17.11",
41 | "@types/jest": "^26.0.22",
42 | "@types/mocha": "^8.2.2",
43 | "@typescript-eslint/eslint-plugin": "^4.19.0",
44 | "@typescript-eslint/parser": "^4.19.0",
45 | "@webpack-cli/serve": "^1.3.0",
46 | "awesome-typescript-loader": "^5.2.1",
47 | "babel-cli": "^6.26.0",
48 | "babel-jest": "^26.6.3",
49 | "babel-loader": "^8.2.2",
50 | "babel-preset-env": "^1.7.0",
51 | "eslint": "^7.23.0",
52 | "eslint-config-airbnb-typescript": "^12.3.1",
53 | "eslint-plugin-import": "^2.22.1",
54 | "eslint-plugin-jsx-a11y": "^6.4.1",
55 | "eslint-plugin-prettier": "^3.3.1",
56 | "html-webpack-plugin": "^5.3.1",
57 | "jest": "^26.6.3",
58 | "nodemon": "^2.0.7",
59 | "source-map-loader": "^2.0.1",
60 | "style-loader": "^2.0.0",
61 | "superagent": "^6.1.0",
62 | "supertest": "^6.1.3",
63 | "ts-jest": "^26.5.4",
64 | "ts-loader": "^8.0.18",
65 | "typescript": "^3.0.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/kafka-penguin/src/clientConfig.ts:
--------------------------------------------------------------------------------
1 | const { Kafka } = require('kafkajs')
2 | require('dotenv').config();
3 |
4 | // Create the client with the broker list
5 | const kafka = new Kafka({
6 | clientId: 'fail-fast-producer',
7 | brokers: [process.env.KAFKA_BOOTSTRAP_SERVER],
8 | ssl: true,
9 | sasl: {
10 | mechanism: 'plain',
11 | username: process.env.KAFKA_USERNAME,
12 | password: process.env.KAFKA_PASSWORD,
13 | },
14 | })
15 |
16 | export default kafka
17 |
--------------------------------------------------------------------------------
/kafka-penguin/src/deadletterqueue.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-shadow */
2 | /* eslint-disable import/extensions */
3 | /* eslint-disable import/no-unresolved */
4 | /* eslint-disable no-unused-vars */
5 | /* eslint-disable no-console */
6 | /* eslint-disable no-undef */
7 | import { DeadLetterQueue, DeadLetterQueueErrorConsumer, DeadLetterQueueErrorProducer } from './index';
8 | import testClient from './clientConfig';
9 |
10 | // Dead Letter Queue Tests
11 | describe('Dead Letter Queue Tests', () => {
12 | // Constructor Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 | describe('Constructor', () => {
14 | const testInstance = new DeadLetterQueue(testClient, 'test1', () => true);
15 | const mockClient = {
16 | topic: expect.any(String),
17 | innerProducer: expect.any(Object),
18 | callback: expect.any(Function),
19 | innerConsumer: null,
20 | admin: expect.any(Object),
21 | client: expect.any(Object),
22 | };
23 |
24 | describe('Initial State', () => {
25 | it('Starts off with callback, topic, and client supplied', () => {
26 | expect(testInstance).toBeInstanceOf(DeadLetterQueue);
27 | expect(testInstance).toMatchObject(mockClient);
28 | });
29 | });
30 |
31 | describe('Client is live & configured', () => {
32 | it('Client is supplying the class with producers', () => {
33 | const { client } = testInstance;
34 | const producer = jest.fn(() => client.producer());
35 | producer();
36 | expect(producer).toReturnWith(expect.objectContaining({
37 | send: expect.any(Function),
38 | connect: expect.any(Function),
39 | disconnect: expect.any(Function),
40 | }));
41 | });
42 | it('Client is supplying the class with consumers', () => {
43 | const { client } = testInstance;
44 | const consumer = jest.fn(() => client.consumer({ groupId: 'my-group' }));
45 | consumer();
46 | expect(consumer).toReturnWith(expect.objectContaining({
47 | subscribe: expect.any(Function),
48 | run: expect.any(Function),
49 | }));
50 | });
51 | it('Client is supplying the class with admins', () => {
52 | const { client } = testInstance;
53 | const admin = jest.fn(() => client.admin());
54 | admin();
55 | expect(admin).toReturnWith(expect.any(Object));
56 | });
57 | });
58 | });
59 |
60 | describe('Methods', () => {
61 | let testInstance = new DeadLetterQueue(testClient, 'test1', () => true);
62 |
63 | afterEach(() => {
64 | testInstance = new DeadLetterQueue(testClient, 'test1', () => true);
65 | });
66 |
67 | // Producer Initialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68 |
69 | describe('Producer', () => {
70 | const testingProducer = jest.fn(() => testInstance.producer());
71 |
72 | describe('Returns/SideEffects', () => {
73 | it('returns the DLQ instance', () => {
74 | testingProducer();
75 | expect(testInstance.producer()).toMatchObject(expect.objectContaining({
76 | connect: expect.any(Function),
77 | disconnect: expect.any(Function),
78 | send: expect.any(Function),
79 | }));
80 | });
81 | it('Assigns the producer to a instance of client producer', () => {
82 | testingProducer();
83 | expect(testInstance.innerProducer).toEqual(expect.objectContaining({
84 | send: expect.any(Function),
85 | connect: expect.any(Function),
86 | disconnect: expect.any(Function),
87 | }));
88 | });
89 | });
90 | });
91 |
92 | // Consumer Initialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93 |
94 | describe('Consumer', () => {
95 | const id = { groupId: 'Jest Tests' };
96 | const testingConsumer = jest.fn(() => testInstance.consumer(id));
97 |
98 | describe('Returns/SideEffects', () => {
99 | it('returns the Dead Letter Queue instance', () => {
100 | testingConsumer();
101 | expect(testInstance.consumer(id)).toMatchObject(expect.objectContaining({
102 | connect: expect.any(Function),
103 | disconnect: expect.any(Function),
104 | run: expect.any(Function),
105 | subscribe: expect.any(Function),
106 | }));
107 | });
108 | it('Assigns the producer to a instance of client producer', () => {
109 | testingConsumer();
110 | expect(testInstance.innerConsumer).toEqual(expect.objectContaining({
111 | connect: expect.any(Function),
112 | disconnect: expect.any(Function),
113 | run: expect.any(Function),
114 | subscribe: expect.any(Function),
115 | }));
116 | });
117 | });
118 | });
119 |
120 | // Producer Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121 |
122 | describe('Producer Methods', () => {
123 | const testingProducer = jest.fn(() => testInstance.producer());
124 |
125 | describe('Connect', () => {
126 | describe('Returns/SideEffects', () => {
127 | it('returns the client producer connect method', () => {
128 | testingProducer();
129 | expect(testingProducer).toReturn();
130 | });
131 | it('connect method resolves', () => {
132 | testingProducer();
133 | const { innerProducer } = testInstance;
134 | return innerProducer.connect()
135 | .then((input: any) => expect(input).not.toBeNull())
136 | .finally(() => { innerProducer.disconnect(); });
137 | });
138 | });
139 | });
140 |
141 | describe('Send', () => {
142 | describe('Returns/SideEffects', () => {
143 | it('throws a DLQ Error with a bad message, then sends it to the DLQ', async () => {
144 | testingProducer();
145 | const { innerProducer } = testInstance;
146 | const message = {
147 | topic: 'wrong-topic',
148 | messages: [{
149 | key: 'value',
150 | value: 'key',
151 | }],
152 | };
153 |
154 | return innerProducer.send(message)
155 | .catch((e?: any) => {
156 | innerProducer.send({
157 | messages: message.messages,
158 | topic: `${testInstance.topic}.deadLetterQueue`,
159 | })
160 | .then(innerProducer.disconnect())
161 | .catch((e: Error) => console.log(e));
162 | // Print the error to the console
163 | const newError = new DeadLetterQueueErrorProducer(e);
164 | console.log(newError);
165 | }).finally(() => { innerProducer.disconnect(); });
166 | });
167 | it('disconnects the producer with an bad message', () => {
168 | testingProducer();
169 | const { innerProducer } = testInstance;
170 | const message = {
171 | topic: 'wrong-topic',
172 | messages: [{
173 | key: 'value',
174 | value: 'key',
175 | }],
176 | };
177 | return innerProducer.send(message).catch((e: any) => {
178 | expect(e).toBeInstanceOf(Error);
179 | }).finally(() => { innerProducer.disconnect(); });
180 | });
181 | });
182 | });
183 |
184 | describe('Disconnect', () => {
185 | describe('Returns/SideEffects', () => {
186 | it('returns the client producer disconnect method & disconnects successfully', () => {
187 | testingProducer();
188 | const { innerProducer } = testInstance;
189 | expect(innerProducer.disconnect).toEqual(expect.any(Function));
190 | return innerProducer.disconnect()
191 | .then((input: any) => expect(input).not.toBeNull())
192 | .finally(() => { innerProducer.disconnect(); });
193 | });
194 | });
195 | });
196 | });
197 |
198 | // Consumer Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
199 |
200 | describe('Consumer Methods', () => {
201 | const group = { groupId: 'JestTests' };
202 | const testingConsumer = jest.fn(() => testInstance.consumer(group));
203 |
204 | describe('Connect', () => {
205 | describe('Returns/SideEffects', () => {
206 | it('returns the client consumer connect method', () => {
207 | testingConsumer();
208 | expect(testingConsumer).toReturn();
209 | });
210 | it('connect method resolves', () => {
211 | testingConsumer();
212 | const { innerConsumer } = testInstance;
213 | return innerConsumer.connect()
214 | .then((input: any) => expect(input).not.toBeNull())
215 | .finally(() => { innerConsumer.disconnect(); });
216 | });
217 | });
218 | });
219 |
220 | describe('Send', () => {
221 | const run = jest.fn(async (input) => {
222 | await testInstance.innerConsumer.connect()
223 | .then(() => {
224 | testInstance.innerConsumer.subscribe({
225 | topic: testInstance.topic,
226 | fromBeginning: false,
227 | });
228 | })
229 | .then(() => {
230 | testInstance.innerConsumer.run(input);
231 | })
232 | .catch((e: any) => { expect(e).toBeInstanceOf(Error); })
233 | .finally(() => testInstance.innerConsumer.disconnect());
234 | });
235 |
236 | describe('Returns/SideEffects', () => {
237 | it('throws a DLQ Error with a bad message, then sends it to the DLQ', async () => {
238 | testingConsumer();
239 | return run({
240 | eachMessage: ({ topic, partitions, message }: {
241 | topic: any, partitions: any, message: any
242 | }) => false,
243 | });
244 | });
245 | });
246 | });
247 |
248 | describe('Disconnect', () => {
249 | describe('Returns/SideEffects', () => {
250 | it('returns the client producer disconnect method & disconnects successfully', () => {
251 | testingConsumer();
252 | const { innerConsumer } = testInstance;
253 | expect(innerConsumer.disconnect).toEqual(expect.any(Function));
254 | return innerConsumer.disconnect()
255 | .then((input: any) => expect(input).not.toBeNull())
256 | .finally(() => { innerConsumer.disconnect(); });
257 | });
258 | });
259 | });
260 | });
261 | });
262 | });
263 |
--------------------------------------------------------------------------------
/kafka-penguin/src/failfast.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/extensions */
2 | /* eslint-disable import/no-unresolved */
3 | /* eslint-disable no-console */
4 | /* eslint-disable no-undef */
5 | import { FailFast, FailFastError } from './index';
6 | import testClient from './clientConfig';
7 |
8 | // Fail Fast Tests
9 | describe('FailFast Tests', () => {
10 | describe('Constructor', () => {
11 | const testInstance = new FailFast(3, testClient);
12 | const mockClient = {
13 | retry: expect.any(Number),
14 | innerProducer: null,
15 | client: expect.any(Object),
16 | };
17 |
18 | describe('Initial State', () => {
19 | it('Starts off with retries and client supplied', () => {
20 | expect(testInstance).toBeInstanceOf(FailFast);
21 | expect(testInstance).toMatchObject(mockClient);
22 | });
23 | });
24 |
25 | describe('Client is live & configured', () => {
26 | it('Client is supplying the class with producers', () => {
27 | const { client } = testInstance;
28 | const producer = jest.fn(() => client.producer());
29 | producer();
30 | expect(producer).toReturnWith(expect.objectContaining({
31 | send: expect.any(Function),
32 | connect: expect.any(Function),
33 | disconnect: expect.any(Function),
34 | }));
35 | });
36 | it('Client is supplying the class with consumers', () => {
37 | const { client } = testInstance;
38 | const consumer = jest.fn(() => client.consumer({ groupId: 'my-group' }));
39 | consumer();
40 | expect(consumer).toReturnWith(expect.objectContaining({
41 | subscribe: expect.any(Function),
42 | run: expect.any(Function),
43 | }));
44 | });
45 | it('Client is supplying the class with admins', () => {
46 | const { client } = testInstance;
47 | const admin = jest.fn(() => client.admin());
48 | admin();
49 | expect(admin).toReturnWith(expect.any(Object));
50 | });
51 | });
52 | });
53 |
54 | describe('Methods', () => {
55 | let testInstance = new FailFast(3, testClient);
56 |
57 | afterEach(() => {
58 | testInstance = new FailFast(3, testClient);
59 | });
60 |
61 | describe('Producer', () => {
62 | const testingProducer = jest.fn(() => testInstance.producer());
63 |
64 | describe('Returns/SideEffects', () => {
65 | it('returns the FailFast instance', () => {
66 | testingProducer();
67 | expect(testInstance.producer()).toBe(testInstance);
68 | });
69 | it('Assigns the producer to a instance of client producer', () => {
70 | testingProducer();
71 | expect(testInstance.innerProducer).toEqual(expect.objectContaining({
72 | send: expect.any(Function),
73 | connect: expect.any(Function),
74 | disconnect: expect.any(Function),
75 | }));
76 | });
77 | });
78 | });
79 |
80 | describe('Producer Methods', () => {
81 | const testingProducer = jest.fn(() => testInstance.producer());
82 |
83 | describe('Connect', () => {
84 | describe('Returns/SideEffects', () => {
85 | it('returns the client producer connect method', () => {
86 | testingProducer();
87 | expect(testingProducer).toReturn();
88 | });
89 | it('connect method resolves', () => {
90 | testingProducer();
91 | const { innerProducer } = testInstance;
92 | return innerProducer.connect()
93 | .then((input: any) => expect(input).not.toBeNull())
94 | .finally(() => { innerProducer.disconnect(); });
95 | });
96 | });
97 | });
98 |
99 | describe('Send', () => {
100 | const message = {
101 | topic: 'xyz',
102 | messages: [{
103 | key: 'xyz',
104 | value: 'xyz',
105 | }],
106 | };
107 | const send = jest.fn((msg) => { testInstance.send(msg); });
108 |
109 | describe('Inputs', () => {
110 | it('takes in a message with the message value interface', () => {
111 | testingProducer();
112 | send(message);
113 | expect(send).toHaveBeenCalledWith(expect.objectContaining(message));
114 | });
115 | });
116 | describe('Returns/SideEffects', () => {
117 | it('throws a FailFast Error with a bad message', async () => {
118 | testingProducer();
119 | const { innerProducer } = testInstance;
120 |
121 | return innerProducer.send(message)
122 | .catch((e: any) => {
123 | innerProducer.disconnect();
124 | const newError = new FailFastError(e);
125 | console.log(newError);
126 | expect(newError).toBeInstanceOf(FailFastError);
127 | }).finally(() => { innerProducer.disconnect(); });
128 | });
129 | it('disconnects the producer with an bad message', () => {
130 | testingProducer();
131 | const { innerProducer } = testInstance;
132 | return innerProducer.send(message).catch((e: any) => {
133 | expect(e).toBeInstanceOf(Error);
134 | }).finally(() => { innerProducer.disconnect(); });
135 | });
136 | });
137 | });
138 |
139 | describe('Disconnect', () => {
140 | describe('Returns/SideEffects', () => {
141 | it('returns the client producer disconnect method & disconnects successfully', () => {
142 | testingProducer();
143 | const { innerProducer } = testInstance;
144 | expect(innerProducer.disconnect).toEqual(expect.any(Function));
145 | return innerProducer.disconnect().then((input: any) => expect(input).not.toBeNull());
146 | });
147 | });
148 | });
149 | });
150 | });
151 | });
152 |
--------------------------------------------------------------------------------
/kafka-penguin/src/ignore.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-shadow */
2 | /* eslint-disable import/extensions */
3 | /* eslint-disable import/no-unresolved */
4 | /* eslint-disable no-unused-vars */
5 | /* eslint-disable no-console */
6 | /* eslint-disable no-undef */
7 | import { Ignore, IgnoreErrorConsumer, IgnoreErrorProducer} from './index';
8 | import testClient from './clientConfig';
9 |
10 | // Dead Letter Queue Tests
11 | describe('Dead Letter Queue Tests', () => {
12 | // Constructor Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 | describe('Constructor', () => {
14 | const testInstance = new Ignore(testClient, 'test1', () => true);
15 | const mockClient = {
16 | topic: expect.any(String),
17 | innerProducer: expect.any(Object),
18 | callback: expect.any(Function),
19 | innerConsumer: null,
20 | admin: expect.any(Object),
21 | client: expect.any(Object),
22 | };
23 |
24 | describe('Initial State', () => {
25 | it('Starts off with callback, topic, and client supplied', () => {
26 | expect(testInstance).toBeInstanceOf(Ignore);
27 | expect(testInstance).toMatchObject(mockClient);
28 | });
29 | });
30 |
31 | describe('Client is live & configured', () => {
32 | it('Client is supplying the class with producers', () => {
33 | const { client } = testInstance;
34 | const producer = jest.fn(() => client.producer());
35 | producer();
36 | expect(producer).toReturnWith(expect.objectContaining({
37 | send: expect.any(Function),
38 | connect: expect.any(Function),
39 | disconnect: expect.any(Function),
40 | }));
41 | });
42 | it('Client is supplying the class with consumers', () => {
43 | const { client } = testInstance;
44 | const consumer = jest.fn(() => client.consumer({ groupId: 'my-group' }));
45 | consumer();
46 | expect(consumer).toReturnWith(expect.objectContaining({
47 | subscribe: expect.any(Function),
48 | run: expect.any(Function),
49 | }));
50 | });
51 | it('Client is supplying the class with admins', () => {
52 | const { client } = testInstance;
53 | const admin = jest.fn(() => client.admin());
54 | admin();
55 | expect(admin).toReturnWith(expect.any(Object));
56 | });
57 | });
58 | });
59 |
60 | describe('Methods', () => {
61 | let testInstance = new Ignore(testClient, 'test1', () => true);
62 |
63 | afterEach(() => {
64 | testInstance = new Ignore(testClient, 'test1', () => true);
65 | });
66 |
67 | // Producer Initialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68 |
69 | describe('Producer', () => {
70 | const testingProducer = jest.fn(() => testInstance.producer());
71 |
72 | describe('Returns/SideEffects', () => {
73 | it('returns the DLQ instance', () => {
74 | testingProducer();
75 | expect(testInstance.producer()).toMatchObject(expect.objectContaining({
76 | connect: expect.any(Function),
77 | disconnect: expect.any(Function),
78 | send: expect.any(Function),
79 | }));
80 | });
81 | it('Assigns the producer to a instance of client producer', () => {
82 | testingProducer();
83 | expect(testInstance.innerProducer).toEqual(expect.objectContaining({
84 | send: expect.any(Function),
85 | connect: expect.any(Function),
86 | disconnect: expect.any(Function),
87 | }));
88 | });
89 | });
90 | });
91 |
92 | // Consumer Initialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93 |
94 | describe('Consumer', () => {
95 | const id = { groupId: 'Jest Tests' };
96 | const testingConsumer = jest.fn(() => testInstance.consumer(id));
97 |
98 | describe('Returns/SideEffects', () => {
99 | it('returns the Dead Letter Queue instance', () => {
100 | testingConsumer();
101 | expect(testInstance.consumer(id)).toMatchObject(expect.objectContaining({
102 | connect: expect.any(Function),
103 | disconnect: expect.any(Function),
104 | run: expect.any(Function),
105 | subscribe: expect.any(Function),
106 | }));
107 | });
108 | it('Assigns the producer to a instance of client producer', () => {
109 | testingConsumer();
110 | expect(testInstance.innerConsumer).toEqual(expect.objectContaining({
111 | connect: expect.any(Function),
112 | disconnect: expect.any(Function),
113 | run: expect.any(Function),
114 | subscribe: expect.any(Function),
115 | }));
116 | });
117 | });
118 | });
119 |
120 | // Producer Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121 |
122 | describe('Producer Methods', () => {
123 | const testingProducer = jest.fn(() => testInstance.producer());
124 |
125 | describe('Connect', () => {
126 | describe('Returns/SideEffects', () => {
127 | it('returns the client producer connect method', () => {
128 | testingProducer();
129 | expect(testingProducer).toReturn();
130 | });
131 | it('connect method resolves', () => {
132 | testingProducer();
133 | const { innerProducer } = testInstance;
134 | return innerProducer.connect()
135 | .then((input: any) => expect(input).not.toBeNull())
136 | .finally(() => { innerProducer.disconnect(); });
137 | });
138 | });
139 | });
140 |
141 | describe('Send', () => {
142 | describe('Returns/SideEffects', () => {
143 | it('throws a DLQ Error with a bad message, then sends it to the DLQ', async () => {
144 | testingProducer();
145 | const { innerProducer } = testInstance;
146 | const message = {
147 | topic: 'wrong-topic',
148 | messages: [{
149 | key: 'value',
150 | value: 'key',
151 | }],
152 | };
153 |
154 | return innerProducer.send(message)
155 | .catch((e?: any) => {
156 | innerProducer.send({
157 | messages: message.messages,
158 | topic: `${testInstance.topic}`,
159 | })
160 | .then(innerProducer.disconnect())
161 | .catch((e: Error) => console.log(e));
162 | // Print the error to the console
163 | const newError = new IgnoreErrorProducer(e);
164 | console.log(newError);
165 | }).finally(() => { innerProducer.disconnect(); });
166 | });
167 | it('disconnects the producer with an bad message', () => {
168 | testingProducer();
169 | const { innerProducer } = testInstance;
170 | const message = {
171 | topic: 'wrong-topic',
172 | messages: [{
173 | key: 'value',
174 | value: 'key',
175 | }],
176 | };
177 | return innerProducer.send(message).catch((e: any) => {
178 | expect(e).toBeInstanceOf(Error);
179 | }).finally(() => { innerProducer.disconnect(); });
180 | });
181 | });
182 | });
183 |
184 | describe('Disconnect', () => {
185 | describe('Returns/SideEffects', () => {
186 | it('returns the client producer disconnect method & disconnects successfully', () => {
187 | testingProducer();
188 | const { innerProducer } = testInstance;
189 | expect(innerProducer.disconnect).toEqual(expect.any(Function));
190 | return innerProducer.disconnect()
191 | .then((input: any) => expect(input).not.toBeNull())
192 | .finally(() => { innerProducer.disconnect(); });
193 | });
194 | });
195 | });
196 | });
197 |
198 | // Consumer Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
199 |
200 | describe('Consumer Methods', () => {
201 | const group = { groupId: 'JestTests' };
202 | const testingConsumer = jest.fn(() => testInstance.consumer(group));
203 |
204 | describe('Connect', () => {
205 | describe('Returns/SideEffects', () => {
206 | it('returns the client consumer connect method', () => {
207 | testingConsumer();
208 | expect(testingConsumer).toReturn();
209 | });
210 | it('connect method resolves', () => {
211 | testingConsumer();
212 | const { innerConsumer } = testInstance;
213 | return innerConsumer.connect()
214 | .then((input: any) => expect(input).not.toBeNull())
215 | .finally(() => { innerConsumer.disconnect(); });
216 | });
217 | });
218 | });
219 |
220 | describe('Send', () => {
221 | const run = jest.fn(async (input) => {
222 | await testInstance.innerConsumer.connect()
223 | .then(() => {
224 | testInstance.innerConsumer.subscribe({
225 | topic: testInstance.topic,
226 | fromBeginning: false,
227 | });
228 | })
229 | .then(() => {
230 | testInstance.innerConsumer.run(input);
231 | })
232 | .catch((e: any) => { expect(e).toBeInstanceOf(Error); })
233 | .finally(() => testInstance.innerConsumer.disconnect());
234 | });
235 |
236 | describe('Returns/SideEffects', () => {
237 | it('throws a DLQ Error with a bad message, then sends it to the DLQ', async () => {
238 | testingConsumer();
239 | return run({
240 | eachMessage: ({ topic, partitions, message }: {
241 | topic: any, partitions: any, message: any
242 | }) => false,
243 | });
244 | });
245 | });
246 | });
247 |
248 | describe('Disconnect', () => {
249 | describe('Returns/SideEffects', () => {
250 | it('returns the client producer disconnect method & disconnects successfully', () => {
251 | testingConsumer();
252 | const { innerConsumer } = testInstance;
253 | expect(innerConsumer.disconnect).toEqual(expect.any(Function));
254 | return innerConsumer.disconnect()
255 | .then((input: any) => expect(input).not.toBeNull())
256 | .finally(() => { innerConsumer.disconnect(); });
257 | });
258 | });
259 | });
260 | });
261 | });
262 | });
--------------------------------------------------------------------------------
/kafka-penguin/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-redeclare */
2 | /* eslint-disable no-shadow */
3 | /* eslint-disable no-console */
4 | /* eslint-disable no-unused-vars */
5 | /* eslint-disable max-classes-per-file */
6 | import { CompressionTypes } from 'kafkajs';
7 |
8 | interface messageValue {
9 | topic: string,
10 | messages: object[],
11 | }
12 | // Fail Fast Strategy
13 |
14 | export class FailFastError extends Error {
15 | message: any;
16 |
17 | reference: any;
18 |
19 | name: any;
20 |
21 | retryCount: number;
22 |
23 | strategy: string;
24 |
25 | originalError: any;
26 |
27 | constructor(e: any) {
28 | super(e);
29 | Error.captureStackTrace(this, this.constructor);
30 | this.strategy = 'Fail Fast';
31 | this.reference = `This error was executed as part of the kafka-penguin Fail Fast message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful. As a result, the producer successfully executed a disconnect operation. Refer to the original error for further information`;
32 | this.name = e.name;
33 | this.message = e.message;
34 | this.originalError = e.originalError;
35 | this.retryCount = e.retryCount;
36 | }
37 | }
38 |
39 | export class FailFast {
40 | retry: number;
41 |
42 | client: any;
43 |
44 | innerProducer: any;
45 |
46 | constructor(num: number, kafkaJSClient: any) {
47 | this.retry = num;
48 | this.client = kafkaJSClient;
49 | this.innerProducer = null;
50 | }
51 |
52 | producer() {
53 | const options = {
54 | retry:
55 | { retries: this.retry },
56 | };
57 | // Create a producer from client passing in retry options
58 | // Save to FailFast class
59 | this.innerProducer = this.client.producer(options);
60 | // Return curr FailFast instance instead of a producer
61 | return this;
62 | }
63 |
64 | connect() {
65 | return this.innerProducer.connect();
66 | }
67 |
68 | disconnect() {
69 | return this.innerProducer.disconnect();
70 | }
71 |
72 | send(message: messageValue) {
73 | return this.innerProducer.send(message)
74 | .catch((e: any) => {
75 | this.innerProducer.disconnect();
76 | const newError = new FailFastError(e);
77 | // eslint-disable-next-line no-console
78 | console.log(newError);
79 | });
80 | }
81 | }
82 |
83 | // Dead Letter Queue
84 |
85 | export class DeadLetterQueueErrorConsumer extends Error {
86 | message: any;
87 |
88 | reference: any;
89 |
90 | name: any;
91 |
92 | retryCount: number;
93 |
94 | strategy: string;
95 |
96 | originalError: any;
97 |
98 | constructor(e: any) {
99 | super(e);
100 | Error.captureStackTrace(this, this.constructor);
101 | this.strategy = 'Dead Letter Queue';
102 | this.reference = `This error was executed as part of the kafka-penguin Dead Letter Queue message reprocessing strategy. Your consumer attempted to receive a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
103 | this.name = `${e.name}(Consumer Side)`;
104 | this.message = e.message;
105 | this.originalError = e.originalError;
106 | this.retryCount = e.retryCount;
107 | }
108 | }
109 |
110 | export class DeadLetterQueueErrorProducer extends Error {
111 | message: any;
112 |
113 | reference: any;
114 |
115 | name: any;
116 |
117 | retryCount: number;
118 |
119 | strategy: string;
120 |
121 | originalError: any;
122 |
123 | constructor(e: any) {
124 | super(e);
125 | Error.captureStackTrace(this, this.constructor);
126 | this.strategy = 'Dead Letter Queue';
127 | this.reference = `This error was executed as part of the kafka-penguin Dead Letter Queue message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
128 | this.name = `${e.name}(Producer Side)`;
129 | this.message = e.message;
130 | this.originalError = e.originalError;
131 | this.retryCount = e.retryCount;
132 | }
133 | }
134 |
135 | interface consumerRunInput {
136 | eachMessage: ({
137 | topic,
138 | partitions,
139 | message,
140 | }: {
141 | topic: string,
142 | partitions: number,
143 | message: any
144 | }) => void,
145 | eachBatchAutoResolve: boolean,
146 | }
147 |
148 | interface consumerSubscribeInput {
149 | groupId?: String,
150 | partitionAssigners?: any,
151 | sessionTimeout?: Number,
152 | rebalanceTimeout?: Number,
153 | heartbeatInterval?: Number,
154 | metadataMaxAge?: Number,
155 | allowAutoTopicCreation?: Boolean,
156 | maxBytesPerPartition?: Number,
157 | minBytes?: Number,
158 | maxBytes?: Number,
159 | maxWaitTimeInMs?: Number,
160 | retry?: Object,
161 | maxInFlightRequests?: Number,
162 | rackId?: String
163 | }
164 |
165 | interface input {
166 | eachMessage: ({
167 | topic,
168 | partitions,
169 | message,
170 | }: {
171 | topic: string,
172 | partitions: number,
173 | message: any
174 | }) => void
175 | }
176 | export class DeadLetterQueue {
177 | client: any;
178 |
179 | topic: string;
180 |
181 | callback?: (message: any) => boolean;
182 |
183 | innerConsumer: any;
184 |
185 | admin: any;
186 |
187 | innerProducer: any;
188 |
189 | constructor(client: any, topic: string, callback?: any) {
190 | this.topic = topic;
191 | this.client = client;
192 | this.callback = callback;
193 | this.admin = this.client.admin();
194 | this.innerConsumer = null;
195 | this.innerProducer = this.client.producer();
196 | }
197 |
198 | producer() {
199 | // Reference the DLQ instance for closure in the returned object
200 | const dlqInstance = this;
201 | const { innerProducer } = dlqInstance;
202 | // Return an object with all Producer methods adapted to execute a dead letter queue strategy
203 | console.log('INNER PRODUCER', innerProducer);
204 | return {
205 | ...innerProducer,
206 | connect() {
207 | return innerProducer.connect()
208 | .then(() => {
209 | dlqInstance.createDLQ();
210 | })
211 | .catch((e: Error) => console.log(e));
212 | },
213 | send(message: messageValue) {
214 | return innerProducer.connect()
215 | .then(() => {
216 | innerProducer.send({
217 | ...message,
218 | topic: message.topic,
219 | messages: message.messages,
220 | })
221 | // Upon error, reroute message to DLQ for the strategy topic
222 | .catch((e?: any) => {
223 | innerProducer.send({
224 | messages: message.messages,
225 | topic: `${dlqInstance.topic}.deadLetterQueue`,
226 | })
227 | .then(innerProducer.disconnect())
228 | .catch((e: Error) => console.log(e));
229 | // Print the error to the console
230 | const newError = new DeadLetterQueueErrorProducer(e);
231 | console.log(newError);
232 | });
233 | });
234 | },
235 | };
236 | }
237 |
238 | consumer(groupId: { groupId: string }) {
239 | this.innerConsumer = this.client.consumer(groupId);
240 | const dlqInstance = this;
241 | const { innerConsumer, innerProducer } = dlqInstance;
242 | // Returns an object with all Consumer methods adapter to execute a dead letter queue strategy
243 |
244 | return {
245 | ...innerConsumer,
246 | connect() {
247 | return innerConsumer.connect().then(() => {
248 | dlqInstance.createDLQ();
249 | });
250 | },
251 | subscribe(input?: consumerSubscribeInput) {
252 | return innerConsumer.subscribe({
253 | ...input,
254 | topic: dlqInstance.topic,
255 | fromBeginning: false,
256 | });
257 | },
258 | run(input: consumerRunInput) {
259 | const { eachMessage } = input;
260 | return innerConsumer.run({
261 | ...input,
262 | eachMessage: ({ topic, partitions, message }: {
263 | topic: string, partitions: number, message: any
264 | }) => {
265 | try {
266 | // If user doesn't pass in callback, DLQ simply listens and returns errors
267 | if (dlqInstance.callback) {
268 | if (!dlqInstance.callback(message)) throw Error;
269 | eachMessage({ topic, partitions, message });
270 | }
271 | } catch (e) {
272 | const newError = new DeadLetterQueueErrorConsumer(e);
273 | console.error(newError);
274 | innerProducer.connect()
275 | .then(() => console.log('kafka-penguin: Connected to DLQ topic'))
276 | .then(() => {
277 | innerProducer.send({
278 | topic: `${dlqInstance.topic}.deadLetterQueue`,
279 | messages: [message],
280 | });
281 | })
282 | .then(() => console.log('kafka-penguin: Message published to DLQ'))
283 | .then(() => innerProducer.disconnect())
284 | .then(() => console.log('kafka-penguin: Producer disconnected'))
285 | .catch((e: any) => console.log('Error with producing to DLQ: ', e));
286 | }
287 | },
288 | });
289 | },
290 | };
291 | }
292 |
293 | // Creates a new DLQ topic with the original topic name
294 | async createDLQ() {
295 | const adminCreateDLQ = await this.admin.connect()
296 | .then(async () => {
297 | await this.admin.createTopics({
298 | topics: [{
299 | topic: `${this.topic}.deadLetterQueue`,
300 | numPartitions: 1,
301 | replicationFactor: 1,
302 | replicaAssignment: [{ partition: 0, replicas: [0, 1, 2] }],
303 | }],
304 | });
305 | })
306 | .then(() => this.admin.disconnect())
307 | .catch((err: any) => console.log('Error from createDLQ', err));
308 | return adminCreateDLQ;
309 | }
310 | }
311 |
312 | // Ignore
313 |
314 | export class IgnoreErrorProducer extends Error {
315 | message: any;
316 |
317 | reference: any;
318 |
319 | name: any;
320 |
321 | retryCount: number;
322 |
323 | strategy: string;
324 |
325 | originalError: any;
326 |
327 | constructor(e: any) {
328 | super(e);
329 | Error.captureStackTrace(this, this.constructor);
330 | this.strategy = 'Ignore';
331 | this.reference = `This error was executed as part of the kafka-penguin Ignore message reprocessing strategy. Your producer attempted to deliver a message ${e.retryCount + 1} times but was unsuccessful.`;
332 | this.name = `${e.name} (Producer Side)`;
333 | this.message = e.message;
334 | this.originalError = e.originalError;
335 | this.retryCount = e.retryCount;
336 | }
337 | }
338 |
339 | export class IgnoreErrorConsumer extends Error {
340 | message: any;
341 |
342 | reference: any;
343 |
344 | name: any;
345 |
346 | retryCount: number;
347 |
348 | strategy: string;
349 |
350 | originalError: any;
351 |
352 | constructor(e: any) {
353 | super(e);
354 | Error.captureStackTrace(this, this.constructor);
355 | this.strategy = 'Ignore';
356 | this.reference = `This error was executed as part of the kafka-penguin Ignore message reprocessing strategy. Your consumer attempted to receive a message ${e.retryCount + 1} times but was unsuccessful. As a result, the message was sent to a Dead Letter Queue. Refer to the original error for further information`;
357 | this.name = `${e.name} (Consumer Side)`;
358 | this.message = e.message;
359 | this.originalError = e.originalError;
360 | this.retryCount = e.retryCount;
361 | }
362 | }
363 |
364 | interface messageValue {
365 | acks?: Number,
366 | timeout?: Number,
367 | compression?: CompressionTypes,
368 | topic: string,
369 | messages: object[],
370 | }
371 |
372 | interface consumerRunInput {
373 | eachMessage: ({
374 | topic,
375 | partitions,
376 | message,
377 | }: {
378 | topic: string,
379 | partitions: number,
380 | message: any
381 | }) => void,
382 | eachBatchAutoResolve: boolean,
383 | }
384 |
385 | interface consumerSubscribeInput {
386 | groupId?: String,
387 | partitionAssigners?: any,
388 | sessionTimeout?: Number,
389 | rebalanceTimeout?: Number,
390 | heartbeatInterval?: Number,
391 | metadataMaxAge?: Number,
392 | allowAutoTopicCreation?: Boolean,
393 | maxBytesPerPartition?: Number,
394 | minBytes?: Number,
395 | maxBytes?: Number,
396 | maxWaitTimeInMs?: Number,
397 | retry?: Object,
398 | maxInFlightRequests?: Number,
399 | rackId?: String
400 | }
401 |
402 | export class Ignore {
403 | client: any;
404 |
405 | topic: string;
406 |
407 | callback?: (message: any) => boolean;
408 |
409 | innerConsumer: any;
410 |
411 | admin: any;
412 |
413 | innerProducer: any;
414 |
415 | constructor(client: any, topic: string, callback?: any) {
416 | this.topic = topic;
417 | this.client = client;
418 | this.callback = callback;
419 | this.admin = this.client.admin();
420 | this.innerConsumer = null;
421 | this.innerProducer = this.client.producer();
422 | }
423 |
424 | producer() {
425 | // Reference the Ignore instance for closure in the returned object
426 | const ignoreInstance = this;
427 | const { innerProducer } = ignoreInstance;
428 | // Return an object with all Producer methods adapted to execute Ignore strategy
429 | console.log('INNER PRODUCER', innerProducer);
430 | return {
431 | ...innerProducer,
432 | connect() {
433 | return innerProducer.connect()
434 | .catch((e: Error) => console.log(e));
435 | },
436 | send(message: messageValue) {
437 | return innerProducer.connect()
438 | .then(() => {
439 | innerProducer.send({
440 | ...message,
441 | topic: message.topic,
442 | messages: message.messages,
443 | })
444 | .catch((e: Error) => {
445 | console.log(e);
446 | // Print the error to the console
447 | const newError = new IgnoreErrorProducer(e);
448 | console.log(newError);
449 | });
450 | });
451 | },
452 | };
453 | }
454 |
455 | consumer(groupId: { groupId: string }) {
456 | this.innerConsumer = this.client.consumer(groupId);
457 | const ignoreInstance = this;
458 | const { innerConsumer } = ignoreInstance;
459 | // Returns an object with all Consumer methods adapter to execute ignore strategy
460 | return {
461 | ...innerConsumer,
462 | connect() {
463 | return innerConsumer.connect();
464 | },
465 | subscribe(input?: consumerSubscribeInput) {
466 | return innerConsumer.subscribe({
467 | ...input,
468 | topic: ignoreInstance.topic,
469 | fromBeginning: false,
470 | });
471 | },
472 | run(input: consumerRunInput) {
473 | const { eachMessage } = input;
474 | return innerConsumer.run({
475 | ...input,
476 | eachMessage: (
477 | { topic, partitions, message }: {
478 | topic: string, partitions: number, message: any
479 | // eslint-disable-next-line comma-dangle
480 | }
481 | ) => {
482 | try {
483 | // If user doesn't pass in callback
484 | if (ignoreInstance.callback) {
485 | if (!ignoreInstance.callback(message)) throw Error;
486 | eachMessage({ topic, partitions, message });
487 | }
488 | } catch (e) {
489 | const newError = new IgnoreErrorConsumer(e);
490 | console.error("kafka Error:", newError);
491 | }
492 | },
493 | })
494 | },
495 | };
496 | }
497 | }
498 |
--------------------------------------------------------------------------------
/kafka-penguin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "strict": true,
6 | "declaration": true,
7 | "outDir": "./dist"
8 | },
9 | "exclude": [
10 | "tests",
11 | "dist",
12 | "node_modules",
13 | "../demo",
14 | "./src/failfast.test.ts",
15 | "./src/deadletterqueue.ts"
16 | ]
17 | }
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact",
5 | "typescript",
6 | "typescriptreact"
7 | ],
8 | "[javascriptreact]": {
9 | "editor.codeActionsOnSave": {
10 | "source.fixAll.eslint": true
11 | }
12 | },
13 | "editor.formatOnSave": true,
14 | "[typescript]": {
15 | "editor.formatOnSave": false,
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll.eslint": true
18 | }
19 | },
20 | "[typescriptreact]": {
21 | "editor.formatOnSave": false,
22 | "editor.codeActionsOnSave": {
23 | "source.fixAll.eslint": true
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/strategies/README.md:
--------------------------------------------------------------------------------
1 | # Strategies
2 |
3 | #### FailFast
4 |
5 | Stops processing as soon as an error occurs.
6 |
7 | {% page-ref page="failfast.md" %}
8 |
9 | #### Ignore
10 |
11 | Handle message processing failures by ignoring. Achieves non-blocking for your queue
12 |
13 | {% page-ref page="strategies-ignore.md" %}
14 |
15 | **Dead Letter Queue**
16 |
17 | Handle message processing failures by forwarding problematic messages to a dead-letter queue \(DLQ\).
18 |
19 | {% page-ref page="dead-letter-queue.md" %}
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/strategies/dead-letter-queue.md:
--------------------------------------------------------------------------------
1 | # Dead Letter Queue
2 |
3 | ## About
4 |
5 | This strategy creates another topic that acts as a repository for erroneous messages. It works parallel with your normal topic and is meant to keep flows from producers or consumers unblocked \(while storing problematic messages for later reprocessing\). Potential use cases for this strategy include services with data streaming, non-ACID or transactional message flows, or any system that simply needs to "just keep running".
6 |
7 | ## Syntax
8 |
9 | **DeadLetterQueue\(kafka-client, topic, callback\)**
10 |
11 | `kafka-client` A configured [KafkaJS client](https://kafka.js.org/docs/configuration) provided by the developer.
12 |
13 | `topic` The target topic that producers or consumers will publish or subscribe to in this strategy instance. Kafka-Penguin currently supports one topic per strategy instance. If a dead letter queue for this topic has not been created, the strategy will automatically create it upon producer or consumer connect.
14 |
15 | `callback` A callback that must return a boolean value. The callback will take in one argument: the messages received by the consumer. During execution, the strategy will pass to the callback each message consumed; any message which returns `false` will be rerouted to the topic-specific dead letter queue. This allows the developer to customize the strategy to catch specific conditions when consuming.
16 |
17 | #### **Producer**
18 |
19 | `DeadLetterQueue.producer` Returns a producer initialized from the strategy instance. The producer has "adapted" methods which execute the strategy under the hood.
20 |
21 | `producer.connect` Connects the producer to the Kafka cluster indicated in the configured KafkaJS client. This method will also create a topic-specific dead letter queue if one does not already exist.
22 |
23 | `producer.send(message)` This method takes in one argument, `messages` that are passed in with the same requirements as the counterpart method on KafkaJS, and sends it to the Kafka cluster. However, this `send` is adapted to send to the strategy's dead letter queue upon error.
24 |
25 | #### Consumer
26 |
27 | `DeadLetterQueue.consumer` Returns a consumer initialized from the strategy instance. The consumer has "adapted" methods which execute the strategy under the hood.
28 |
29 | `consumer.connect` Connects the consumer to the Kafka cluster indicated in the configured KafkaJS client. This method will also create a topic-specific dead letter queue if one does not already exist.
30 |
31 | `consumer.run` Starts consuming messages from the Kafka cluster to the consumer client. The `run` method also utilizes `eachMessage`to pass each message received through the callback provided at strategy instantiation. If the callback returns `false`, the `run` method automatically creates a temporary producer, it produces the message to the dead letter queue, and then discards that producer.
32 |
33 | ## Example
34 |
35 | #### Producer
36 |
37 | ```javascript
38 | const producerClientDLQ = require('./clientConfig.ts')
39 | import { DeadLetterQueue } from 'kafka-penguin'
40 |
41 |
42 | // This example simulates an error where the producer sends to a bad topic
43 | const topicGood = 'test-topic-DLQ';
44 | const topicBad = "topic-non-existent"
45 |
46 | // Set up the Dead Letter Queue (DLQ) strategy with a configured KafkaJS client, a topic, and a callback that evaluates to a boolean
47 | const exampleDLQProducer = new DeadLetterQueue(producerClientDLQ, topicGood, true);
48 |
49 | // Initialize a producer from the new instance of the Dead Letter Queue strategy
50 | const producerDLQ = exampleDLQProducer.producer();
51 |
52 | // Connecting the producer creates a DLQ topic in case of bad messages
53 | // If an error occurs, the strategy moves the message to the topic specific DLQ
54 | // The producer is able to keep publishing good messages to the topic
55 | producerDLQ.connect()
56 | .then(() => producerDLQ.send({
57 | topic: topicGood,
58 | messages: [
59 | {
60 | key: 'message 1',
61 | value: 'Good Message',
62 | },
63 | ],
64 | }))
65 | .then(() => producerDLQ.send ({
66 | topic: topicBad,
67 | messages: [
68 | {
69 | key: 'message 2',
70 | value: 'Bad Message',
71 | }
72 | ]
73 | }))
74 | .then(() => producerDLQ.send ({
75 | topic: topicGood,
76 | messages: [
77 | {
78 | key: 'message 3',
79 | value: 'Good Message',
80 | }
81 | ]
82 | }))
83 | .then(() => producerDLQ.disconnect())
84 | .catch((e: any) => {
85 | console.log(e);
86 |
87 |
88 | ```
89 |
90 | **Consumer**
91 |
92 | ```javascript
93 | const client = require('./clientConfig.ts');
94 | import { DeadLetterQueue } from 'kafka-penguin';
95 |
96 | const topic = 'test-topic-DLQ';
97 |
98 | // This allows the consumer to evaluate each message according to a condition
99 | // The callback must return a boolean value
100 | const callback = (message) => {
101 | try {
102 | JSON.parse(message.value);
103 | const callback = (message) => {
104 | return true;
105 | };
106 |
107 | // Set up the Dead Letter Queue (DLQ) strategy with a configured KafkaJS client, a topic, and the evaluating callback
108 | const exampleDLQConsumer = new DeadLetterQueue(client, topic, callback);
109 |
110 | // Initialize a consumer from the new instance of the Dead Letter Queue strategy
111 | const consumerDLQ = exampleDLQConsumer.consumer({ groupId: 'testID' });
112 |
113 |
114 |
115 | // Connecting the consumer creates a DLQ topic in case of bad messages
116 | // If the callback returns false, the strategy moves the message to the topic specific DLQ
117 | // The consumer is able to keep consuming good messages from the topic
118 | consumerDLQ.connect()
119 | .then(consumerDLQ.subscribe())
120 | .then(() => consumerDLQ.run({
121 | eachMessage: ({ topic, partitions, message }) => {
122 | console.log(JSON.parse(message.value));
123 | },
124 | }))
125 | .catch((e) => console.log(`Error message from consumer: ${e}`));
126 |
127 |
128 |
129 | ```
130 |
131 |
--------------------------------------------------------------------------------
/strategies/failfast.md:
--------------------------------------------------------------------------------
1 | # FailFast
2 |
3 | ## About
4 |
5 | This strategy executes a purposeful disconnect after a producer has sent an erroneous message. Potential use cases for this strategy include micro-services where there is a low tolerance for failure, as well as acid and/or transactional workflows. This strategy also works well for development environments in Agile-based workflows.
6 |
7 | ## Syntax
8 |
9 | #### FailFast\(retries, kafka-client\)
10 |
11 | `retries` Number of times the producer attempts to send the message before disconnecting and throwing an error.
12 |
13 | `kafka-client` A configured [KafkaJS client](https://kafka.js.org/docs/configuration) provided by the developer.
14 |
15 | **Producer**
16 |
17 | `FailFast.producer` Returns a producer initialized from the strategy instance. The producer has "adapted" methods that execute the strategy under the hood.
18 |
19 | `producer.connect` Connects the producer to the Kafka cluster indicated in the configured KafkaJS client.
20 |
21 | `producer.send(message)` This method takes in one argument, `message` that is passed in with the same requirements as the counterpart method on KafkaJS, and sends it to the Kafka cluster. However, this `send` will disconnect the producer once it reaches the set number of retries.
22 |
23 | ## Example
24 |
25 | **Producer**
26 |
27 | ```javascript
28 | import { FailFast } from 'kafka-penguin'
29 | const FailFastClient = require('./clientConfig.ts')
30 |
31 | // Set up Fail Fast with the number of retried and a configured KafkaJS client
32 | const exampleFailFast = new FailFast(2, FailFastClient)
33 |
34 | // Initialize a producer from the new instance of Fail Fast
35 | const producer = exampleFailFast.producer();
36 |
37 |
38 | // Example error of a producer sending to a non-existent topic
39 | const message = {
40 | topic: 'topic-non-existent',
41 | messages: [
42 | {
43 | key: 'hello'
44 | }]
45 | }
46 |
47 | // FailFast will attempt to send the message to the Kafka cluster.
48 | // After the retry count is reached, the producer will automatically disconnect and an error is thrown.
49 | producer.connect()
50 | .then(() => console.log('Connected!'))
51 | .then(() => producer.send(message))
52 | ```
53 |
54 |
--------------------------------------------------------------------------------
/strategies/strategies-ignore.md:
--------------------------------------------------------------------------------
1 | # Ignore
2 |
3 | ## About
4 |
5 | This strategy enables you to continuously process messages even if some messages are erroneous, problematic, or fail for some reason. It is meant to keep flows from producers or consumers unblocked even if there are failures. Potential use cases for this strategy include services and systems that rely upon a constant stream of data.
6 |
7 | ## Syntax
8 |
9 | **Ignore\(kafka-client, topic, callback\)**
10 |
11 | `kafka-client` A configured [KafkaJS client](https://kafka.js.org/docs/configuration) provided by the developer.
12 |
13 | `topic` The target topic that producers or consumers will publish or subscribe to in this strategy instance. Kafka-Penguin currently supports one topic per strategy instance.
14 |
15 | `callback` A callback that must return a boolean value. The callback will take in one argument: the messages received by the consumer. During execution, the strategy will pass to the callback each message consumed; Even if a message which returns `false`, the producer or consumer will continue to produce or consume messages uninterrupted.
16 |
17 | #### **Producer**
18 |
19 | `Ignore.producer` Returns a producer initialized from the strategy instance. The producer has "adapted" methods which execute the strategy under the hood.
20 |
21 | `producer.connect` Connects the producer to the Kafka cluster indicated in the configured KafkaJS client.
22 |
23 | `producer.send(message)` This method takes in one argument, `messages` that are passed in with the same requirements as the counterpart method on KafkaJS, and sends it to the Kafka cluster.
24 |
25 | #### Consumer
26 |
27 | `Ignore.consumer` Returns a consumer initialized from the strategy instance. The consumer has "adapted" methods which execute the strategy under the hood.
28 |
29 | `consumer.connect` Connects the consumer to the Kafka cluster indicated in the configured KafkaJS client.
30 |
31 | `consumer.run` Starts consuming messages from the Kafka cluster to the consumer client. The `run` method also utilizes `eachMessage`to pass each message received through the callback provided at strategy instantiation.
32 |
33 | ## Example
34 |
35 | #### Producer:
36 |
37 | ```text
38 | /* eslint-disable no-console */
39 | import { Ignore } from './kafka-penguin/src/index'
40 |
41 | const producerClientIgnore = require('./clientConfig.ts');
42 |
43 | // This example simulates an error where the producer sends to a bad topic
44 | const topicGood = 'test-topic';
45 | const topicBad = 'topic-non-existent';
46 |
47 | // Set up the Ignore strategy
48 | // Configure it with a configured KafkaJS client, a topic, and a callback that returns boolean
49 | const exampleIgnoreProducer = new Ignore(producerClientIgnore, topicGood, true);
50 |
51 | // Initialize a producer from the new instance of the Ignore strategy
52 | const producerIgnore = exampleIgnoreProducer.producer();
53 |
54 | // Connecting the producer and send messages.
55 | // If an error occurs with a message, the strategy ignores erroneous message and continues
56 | // publishing good messages to the topic
57 | producerIgnore.connect()
58 | .then(() => producerIgnore.send({
59 | topic: topicGood,
60 | messages: [
61 | {
62 | key: 'message 1',
63 | value: JSON.stringify('Good Message'),
64 | },
65 | ],
66 | }))
67 | .then(() => producerIgnore.send({
68 | topic: topicGood,
69 | messages: [
70 | {
71 | key: 'message 2',
72 | value: 'Bad Message',
73 | },
74 | ],
75 | }))
76 | .then(() => producerIgnore.send({
77 | topic: topicGood,
78 | messages: [
79 | {
80 | key: 'message 3',
81 | value: JSON.stringify('Good Message'),
82 | },
83 | ],
84 | }))
85 | .then(() => producerIgnore.disconnect())
86 | .catch((e: any) => {
87 | console.log(e);
88 | });
89 | ```
90 |
91 | #### Consumer:
92 |
93 | ```text
94 | /* eslint-disable no-console */
95 | import { Ignore } from './kafka-penguin/src/index';
96 |
97 | const client = require('./clientConfig.ts');
98 | const topic = 'test-topic';
99 |
100 | // This allows the consumer to evaluate each message according to a condition
101 | // The callback must return a boolean value
102 | const callback = (message) => {
103 | try {
104 | JSON.parse(message.value);
105 | } catch (e) {
106 | return false;
107 | }
108 | return true;
109 | };
110 |
111 | // Set up the Ignore strategy
112 | // with a configured KafkaJS client, a topic, and the evaluating callback
113 | const exampleIgnoreConsumer = new Ignore(client, topic, callback);
114 |
115 | // Initialize a consumer from the new instance of the Dead Letter Queue strategy
116 | const consumerIgnore = exampleIgnoreConsumer.consumer({ groupId: 'testID' });
117 |
118 | // Connecting the consumer to consume messages. bad messages
119 | // If the callback evaluates a message as erroneous by returning false, the strategy
120 | // enables the consumer to keep consuming good messages from the topic
121 | consumerIgnore.connect()
122 | .then(consumerIgnore.subscribe())
123 | .then(() => consumerIgnore.run({
124 | eachMessage: ({ message }) => {
125 | // if (message.value.length < 5) return true;
126 | // return false;
127 | console.log("message value:", message.value.toString())
128 | },
129 | }))
130 | .catch((e) => console.log(`Error message from consumer: ${e}`));
131 | ```
132 |
133 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "commonJS",
5 | "jsx": "react",
6 | "sourceMap": true,
7 | "outDir": "./build",
8 | "removeComments": true,
9 | "allowSyntheticDefaultImports": true,
10 | "esModuleInterop": true,
11 | "preserveConstEnums": true
12 | },
13 | "include": [
14 | "./demo/client/**/*"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------