forked from feast-dev/feast
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathingest.py
More file actions
235 lines (202 loc) · 7.42 KB
/
Copy pathingest.py
File metadata and controls
235 lines (202 loc) · 7.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import logging
import multiprocessing
import os
import time
from functools import partial
from multiprocessing import Process, Queue, Pool
from typing import Iterable
import pandas as pd
import pyarrow as pa
from feast.feature_set import FeatureSet
from feast.type_map import convert_df_to_feature_rows, convert_dict_to_proto_values
from feast.types.FeatureRow_pb2 import FeatureRow
from kafka import KafkaProducer
from tqdm import tqdm
from feast.constants import DATETIME_COLUMN
_logger = logging.getLogger(__name__)
GRPC_CONNECTION_TIMEOUT_DEFAULT = 3 # type: int
GRPC_CONNECTION_TIMEOUT_APPLY = 300 # type: int
FEAST_SERVING_URL_ENV_KEY = "FEAST_SERVING_URL" # type: str
FEAST_CORE_URL_ENV_KEY = "FEAST_CORE_URL" # type: str
BATCH_FEATURE_REQUEST_WAIT_TIME_SECONDS = 300
CPU_COUNT = os.cpu_count() # type: int
KAFKA_CHUNK_PRODUCTION_TIMEOUT = 120 # type: int
def _kafka_feature_row_producer(
feature_row_queue: Queue, row_count: int, brokers, topic, ctx: dict, pbar: tqdm
):
# Callback for failed production to Kafka
def on_error(e):
# Save last exception
ctx["last_exception"] = e
# Increment error count
if "error_count" in ctx:
ctx["error_count"] += 1
else:
ctx["error_count"] = 1
# Callback for succeeded production to Kafka
def on_success(meta):
pbar.update()
producer = KafkaProducer(bootstrap_servers=brokers)
processed_rows = 0
while processed_rows < row_count:
if feature_row_queue.empty():
time.sleep(1)
producer.flush(timeout=KAFKA_CHUNK_PRODUCTION_TIMEOUT)
else:
while not feature_row_queue.empty():
row = feature_row_queue.get()
if row is not None:
# Push row to Kafka
producer.send(topic, row.SerializeToString()).add_callback(
on_success
).add_errback(on_error)
processed_rows += 1
# Force an occasional flush
if processed_rows % 10000 == 0:
producer.flush(timeout=KAFKA_CHUNK_PRODUCTION_TIMEOUT)
del row
pbar.refresh()
# Ensure that all rows are pushed
producer.flush(timeout=KAFKA_CHUNK_PRODUCTION_TIMEOUT)
# Using progress bar as counter is much faster than incrementing dict
ctx["success_count"] = pbar.n
pbar.close()
def _encode_pa_chunks(
tbl: pa.lib.Table,
fs: FeatureSet,
max_workers: int,
df_datetime_dtype: pd.DataFrame.dtypes,
chunk_size: int = 5000,
) -> Iterable[FeatureRow]:
"""
Generator function to encode rows in PyArrow table to FeatureRows by
breaking up the table into batches.
Each batch will have its rows spread accross a pool of workers to be
transformed into FeatureRow objects.
:param tbl: PyArrow table to be processed.
:type tbl: pa.lib.Table
:param fs: FeatureSet describing PyArrow table.
:type fs: FeatureSet
:param max_workers: Maximum number of workers.
:type max_workers: int
:param df_datetime_dtype: Pandas dtype of datetime column.
:type df_datetime_dtype: pd.DataFrame.dtypes
:param chunk_size: Maximum size of each chunk when PyArrow table is batched.
:type chunk_size: int
:return: Iterable FeatureRow object.
:rtype: Iterable[FeatureRow]
"""
pool = Pool(max_workers)
# Create a partial function with static non-iterable arguments
func = partial(
convert_dict_to_proto_values,
df_datetime_dtype=df_datetime_dtype,
feature_set=fs,
)
for batch in tbl.to_batches(max_chunksize=chunk_size):
m_df = batch.to_pandas()
results = pool.map_async(func, m_df.to_dict("records"))
yield from results.get()
pool.close()
pool.join()
return
def ingest_table_to_kafka(
feature_set: FeatureSet,
table: pa.lib.Table,
max_workers: int,
chunk_size: int = 5000,
disable_pbar: bool = False,
timeout: int = None,
) -> None:
"""
:param feature_set: FeatureSet describing PyArrow table.
:type feature_set: FeatureSet
:param table: PyArrow table to be processed.
:type table: pa.lib.Table
:param max_workers: Maximum number of workers.
:type max_workers: int
:param chunk_size: Maximum size of each chunk when PyArrow table is batched.
:type chunk_size: int
:param disable_pbar: Flag to indicate if tqdm progress bar should be
disabled.
:type disable_pbar: bool Disable printing of ingestion progress bar
:param timeout: Maximum time before method times out.
:return: None
:rtype: None
"""
pbar = tqdm(unit="rows", total=table.num_rows, disable=disable_pbar)
# Use a small DataFrame to validate feature set schema
ref_df = table.to_batches(max_chunksize=100)[0].to_pandas()
df_datetime_dtype = ref_df[DATETIME_COLUMN].dtype
# Validate feature set schema
validate_dataframe(ref_df, feature_set)
# Create queue through which encoding and production will coordinate
row_queue = Queue()
# Create a context object to send and receive information across processes
ctx = multiprocessing.Manager().dict(
{"success_count": 0, "error_count": 0, "last_exception": ""}
)
# Create producer to push feature rows to Kafka
ingestion_process = Process(
target=_kafka_feature_row_producer,
args=(
row_queue,
table.num_rows,
feature_set.get_kafka_source_brokers(),
feature_set.get_kafka_source_topic(),
ctx,
pbar,
),
)
try:
# Start ingestion process
print(
f"\n(ingest table to kafka) Ingestion started for {feature_set.name}:{feature_set.version}"
)
ingestion_process.start()
for row in _encode_pa_chunks(
tbl=table,
fs=feature_set,
max_workers=max_workers,
chunk_size=chunk_size,
df_datetime_dtype=df_datetime_dtype,
):
row_queue.put(row)
while row_queue.qsize() > chunk_size:
time.sleep(0.1)
row_queue.put(None)
except Exception as ex:
_logger.error(f"Exception occurred: {ex}")
finally:
ingestion_process.join(timeout=timeout)
failed_message = (
""
if ctx["error_count"] == 0
else f"\nFail: {ctx['error_count']}/{table.num_rows}"
)
last_exception_message = (
""
if ctx["last_exception"] == ""
else f"\nLast exception:\n{ctx['last_exception']}"
)
print(
f"\nIngestion statistics:"
f"\nSuccess: {ctx['success_count']}/{table.num_rows}"
f"{failed_message}"
f"{last_exception_message}"
)
def validate_dataframe(dataframe: pd.DataFrame, fs: FeatureSet):
if "datetime" not in dataframe.columns:
raise ValueError(
f'Dataframe does not contain entity "datetime" in columns {dataframe.columns}'
)
for entity in fs.entities:
if entity.name not in dataframe.columns:
raise ValueError(
f"Dataframe does not contain entity {entity.name} in columns {dataframe.columns}"
)
for feature in fs.features:
if feature.name not in dataframe.columns:
raise ValueError(
f"Dataframe does not contain feature {feature.name} in columns {dataframe.columns}"
)