You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by tz...@apache.org on 2021/03/17 10:45:48 UTC

[flink-statefun-playground] 02/02: [FLINK-21824] Add a simple JSON kafka producer.

This is an automated email from the ASF dual-hosted git repository.

tzulitai pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/flink-statefun-playground.git

commit 58cd52c4b0473da1e8d1db4a60de0acef75d1607
Author: Igal Shilman <ig...@gmail.com>
AuthorDate: Tue Mar 16 15:42:42 2021 +0100

    [FLINK-21824] Add a simple JSON kafka producer.
    
    This closes #1.
---
 .../statefun-playground-producer/Dockerfile        |  34 ++++++
 .../statefun-playground-producer/README.md         |  42 ++++++++
 .../statefun-playground-producer/example.json      |   2 +
 .../statefun-playground-producer/main.py           | 114 +++++++++++++++++++++
 .../statefun-playground-producer/requirements.txt  |   2 +
 5 files changed, 194 insertions(+)

diff --git a/playground-internal/statefun-playground-producer/Dockerfile b/playground-internal/statefun-playground-producer/Dockerfile
new file mode 100644
index 0000000..82b3a78
--- /dev/null
+++ b/playground-internal/statefun-playground-producer/Dockerfile
@@ -0,0 +1,34 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM python:3.9-alpine
+
+RUN mkdir -p /opt/statefun/
+COPY example.json /opt/statefun/
+
+RUN mkdir -p /app
+WORKDIR /app
+
+COPY requirements.txt /app
+RUN pip install -r requirements.txt
+
+COPY main.py /app
+
+
+CMD ["python3", "/app/main.py"]
+
+
+
diff --git a/playground-internal/statefun-playground-producer/README.md b/playground-internal/statefun-playground-producer/README.md
new file mode 100644
index 0000000..41dd067
--- /dev/null
+++ b/playground-internal/statefun-playground-producer/README.md
@@ -0,0 +1,42 @@
+# Kafak Producer
+
+# What
+This is a simple, single threaded `Python` script that produces JSONs into a `Kafka` topic.
+Since in StateFun world, we also require keys (they are used for the initial routing) a JSON path
+string needs to be provided. 
+
+# Environment Variables
+
+(taken directly from `main.py`)
+
+```
+APP_PATH = Arg(key="APP_PATH", default="/opt/statefun/example.json", type=str)
+APP_KAFKA_HOST = Arg(key="APP_KAFKA_HOST", default="kafka-broker:9092", type=str)
+APP_KAFKA_TOPIC = Arg(key="APP_KAFKA_TOPIC", default="input", type=str)
+APP_DELAY_SECONDS = Arg(key="APP_DELAY_SECONDS", default="1", type=int)
+APP_LOOP = Arg(key="APP_LOOP", default="true", type=lambda s: s.lower() == "true")
+APP_JSON_PATH = Arg(key="APP_JSON_PATH", default="name", type=parse)
+```
+
+# Example:
+
+Imagine we have the following file at `/opt/statefun/example.json`
+
+```
+> cat /opt/statefun/example.json
+{"name" : "Gorge", "last" : "Costanza"}
+{"name" : "Bob", "last" : "Mop"}
+```
+
+Then we need to set:
+
+```
+export APP_PATH=/opt/statefun/example.json
+export APP_JSON_PATH='name' # note there is no leading dot.
+```
+
+
+# Docker
+
+* Build via: `docker build . -t app`
+* Run `docker run -v $(pwd):/mnt -e 'APP_PATH=/mnt/example.json' -it app`
diff --git a/playground-internal/statefun-playground-producer/example.json b/playground-internal/statefun-playground-producer/example.json
new file mode 100644
index 0000000..d84107a
--- /dev/null
+++ b/playground-internal/statefun-playground-producer/example.json
@@ -0,0 +1,2 @@
+{"name" : "Gorge", "last" : "Costanza"}
+{"name" : "Bob", "last" : "Mop"}
diff --git a/playground-internal/statefun-playground-producer/main.py b/playground-internal/statefun-playground-producer/main.py
new file mode 100644
index 0000000..94ce8b4
--- /dev/null
+++ b/playground-internal/statefun-playground-producer/main.py
@@ -0,0 +1,114 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+import json
+import signal
+import sys
+import time
+import os
+from collections import namedtuple
+from itertools import cycle
+
+from kafka.errors import NoBrokersAvailable
+from kafka import KafkaProducer
+from jsonpath_ng import parse
+
+Arg = namedtuple('Arg', ['key', 'default', 'type'])
+
+APP_PATH = Arg(key="APP_PATH", default="/opt/statefun/example.json", type=str)
+APP_KAFKA_HOST = Arg(key="APP_KAFKA_HOST", default="kafka-broker:9092", type=str)
+APP_KAFKA_TOPIC = Arg(key="APP_KAFKA_TOPIC", default="input", type=str)
+APP_DELAY_SECONDS = Arg(key="APP_DELAY_SECONDS", default="1", type=int)
+APP_LOOP = Arg(key="APP_LOOP", default="true", type=lambda s: s.lower() == "true")
+APP_JSON_PATH = Arg(key="APP_JSON_PATH", default="name", type=parse)
+
+
+def env(arg: Arg):
+    val = os.environ.get(arg.key, arg.default)
+    return arg.type(val)
+
+
+def read_jsons(path: str):
+    with open(path) as f:
+        for line in f.read().splitlines():
+            yield json.loads(line)
+
+
+def create_requests(path: str, loop: bool, json_path):
+    jsons = [js for js in read_jsons(path)]
+    if loop:
+        jsons = cycle(jsons)
+    for js in jsons:
+        matches = json_path.find(js)
+        if len(matches) != 1:
+            raise ValueError(f"Unable to find exactly one key at {js}, please check the correctness of your {APP_JSON_PATH.key} value")
+        match = matches[0]
+        yield match.value, js
+
+
+class KProducer(object):
+
+    def __init__(self, broker, topic):
+        self.producer = KafkaProducer(bootstrap_servers=[broker])
+        self.topic = topic
+
+    def send(self, key: str, value: str):
+        key = key.encode('utf-8')
+        value = value.encode('utf-8')
+        self.producer.send(topic=self.topic, key=key, value=value)
+        self.producer.flush()
+
+
+def produce(producer, delay_seconds: int, requests):
+    for key, js in requests:
+        value = json.dumps(js)
+        producer.send(key=key, value=value)
+        if delay_seconds > 0:
+            time.sleep(delay_seconds)
+
+
+def handler(number, frame):
+    sys.exit(0)
+
+
+def main():
+    # setup an exit signal handler
+    signal.signal(signal.SIGTERM, handler)
+    signal.signal(signal.SIGINT, handler)
+    # get the key,value request generators
+    requests = create_requests(path=env(APP_PATH), loop=env(APP_LOOP), json_path=env(APP_JSON_PATH))
+    # produce forever
+    while True:
+        try:
+            producer = KProducer(broker=env(APP_KAFKA_HOST), topic=env(APP_KAFKA_TOPIC))
+            produce(producer=producer, delay_seconds=env(APP_DELAY_SECONDS), requests=requests)
+            print("Done producing, good bye!", flush=True)
+            return
+        except SystemExit:
+            print("Good bye!", flush=True)
+            return
+        except NoBrokersAvailable:
+            print("No brokers available... retrying in 2 seconds.")
+            time.sleep(2)
+            continue
+        except Exception as e:
+            print(e)
+            return
+
+
+if __name__ == "__main__":
+    main()
diff --git a/playground-internal/statefun-playground-producer/requirements.txt b/playground-internal/statefun-playground-producer/requirements.txt
new file mode 100644
index 0000000..2de9e5c
--- /dev/null
+++ b/playground-internal/statefun-playground-producer/requirements.txt
@@ -0,0 +1,2 @@
+kafka-python
+jsonpath-ng