Skip to content

Commit 4092656

Browse files
Development (#25)
* longer wait time * speed up unit retrieval with serverside stage aggregation * added execution timing for frequently used db methods * implemented exec timer for async functions * increased units pending revision fetching speed 3x * access collections with properties * removed unused mount * Reworked unit upload and update for x3 speed increase. Removed unused methods. * Version bump * Add WS endpoint for workbench status streaming * Add optional dotenv parsing * Implement SSE stream instead of WS * Fixed type annotation * update black version * reworked connection close pipeline * improved logging * Implement background printing tasks * Update docker-compose.yml * remove .gitignore * mark endpoint as deprecated * optimize txn hash pushing to db * removed polling and optimized state push using async event * bugfix: thread wouldn't unblock until txn posted. implemented async wrapper * implemented single field updates * async short url generator. up to 4s faster passport saving * updated packages * version bump * cached files auto remove * added some docker volumes * added db connection close on quit * update .gitignore * removed duplicate code * removed redundant db field * refactored unit retrieval * implemented workbench shutdown sequence * renamed actions * implemented multiplatform Dockerfile * multiplatform image building * bugfix * bugfix * removed arm v7 form the list of platforms * added comments * updated robonomics-interface version * updated Dockerfiles to matched unified code style * updated Dockerfile names * updated action to reuse in other repos * update type hints to 3.10 format * minor changes * bugfix * dependency update * update base version * update Python version in action * bugfix * updated dependencies * action bugfix attempt * added type annotation * update mypy config * update dep versions in requirements.txt * return caching in action * return caching in action * moved type alias * added back union checking plugin * filter out AssertionErrors * replace variable returns with HTTPExceptions * add black daemon * minor refactoring * fixed typos * fixed typo * minor update to passport code logic * filter access logs for certain endpoints * add healthcheck * added connectivity checks for external services * fix typo * bugfix * Update _Barcode.py (#22) * getting component staged for revision from the parent's id (MAP-257) * rewritten pre-commit config * isort * improved type annotations * bugfix * bugfix MAP-332 * Added ECS logging support for the ELK stack integration * slightly modified README * ECS logs now written to file instead of console * experimental: cache Docker layers * improved logging * updated pc hooks * Implemented SSE notifications * Optimized imports * Allow assigning built units with unreleased passport (MAP-355) * Tagged endpoints * update deps * update deps * notify user of a forbidden transition * add error notifications * fix mypy error * replaced concat with fstring * extracted singular connection func * add fstring instead of concat * added more checks and error notifications * added more checks and error notifications * added datalog success notification * version bump * bugfix * add more notifications * replaced json err responses with HTTP exceptions * more notifications and minor improvements * bring back bg datalog posting * reworked messenger interface, suppressed Runtime Error (temp fix) * Fixed 'RuntimeError: no running event loop' * User notifications translated to russian * Improved error notifications * Minor improvements to Messenger.py * Improved reliability for unit wrap up operation * Improved reliability for operation ending * Minor fixes * Updated dependencies * Added more user notifications * Improved image annotations for passport QR codes * Add unit's combined assembly time to the passport * Faster operation ending on composite units * Attempt to fix QR code side logos * Reworked unit component assignment logic * Avoid reassigning the same unit twice * Remove exception info from the printing error notifications * add prometheus metrics * renamed env var for mongodb uri * version bump * add docker secrets parsing * insert exception message into a metric label * reworked Robonomics async wrapper * version bump * add custom tracked exception * remove dependency compilation stage as target binaries are now available * bugfix * add version posting to prometheus * simplify error messages * Added 6 new metrics for basic worker productivity stats * bugfix * Fixed app version reporting inside Docker * Updated dependencies * Bugfix in version file parsing * Attempt connection error fix * Now sorting schemas in alphabetical order * Dependencies Updated * Removed unused Dockerfile Co-authored-by: Pavel Tarasov <34866682+PaTara43@users.noreply.github.com>
1 parent f872a9e commit 4092656

File tree

13 files changed

+687
-588
lines changed

13 files changed

+687
-588
lines changed

Dockerfile

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,23 @@
1-
# There are no binaries of this packages as of today for ARM platforms,
2-
# so compilation is required
3-
FROM python:3.10 as dependency-compilation
4-
WORKDIR /tmp
5-
RUN apt-get update && apt-get install -y\
6-
build-essential\
7-
libssl-dev\
8-
libffi-dev\
9-
python3-dev
10-
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
11-
RUN /root/.cargo/bin/rustup default nightly
12-
RUN pip install --upgrade pip
13-
RUN . /root/.cargo/env && pip install py-bip39-bindings
14-
RUN . /root/.cargo/env && pip install py-ed25519-bindings
15-
RUN . /root/.cargo/env && pip install py-sr25519-bindings
16-
RUN . /root/.cargo/env && pip install cryptography
17-
181
# At this stage we convert Poetry's dependency file into a more traditional
192
# requirements.txt to avoid installing Poetry into the final container.
203
# Although very slow, this cannot be skipped, as we need to resolve dependencies
214
# for the exact platform the client is using.
225
FROM python:3.10 as requirements-stage
236
WORKDIR /tmp
24-
COPY --from=dependency-compilation /root/.cache/pip /root/.cache/pip
7+
RUN pip install --upgrade pip
258
RUN pip install poetry
269
COPY ./pyproject.toml ./poetry.lock* /tmp/
2710
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
11+
RUN poetry version | grep -o [0-9.]* > version.txt
2812

2913
# Final container build. Uses pre-compiled dependencies and requirements.txt
3014
# obtained in the previous steps
3115
FROM python:3.10
3216
WORKDIR /src
3317
COPY --from=requirements-stage /tmp/requirements.txt /src/requirements.txt
34-
COPY --from=dependency-compilation /root/.cache/pip /root/.cache/pip
3518
RUN pip install --no-cache-dir --upgrade -r /src/requirements.txt
3619
COPY ./src /src
20+
COPY --from=requirements-stage /tmp/version.txt /src/version.txt
3721
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=12 \
3822
CMD curl --fail http://localhost:5000/docs || exit 1
3923
ENTRYPOINT ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000"]

Dockerfile-quick

Lines changed: 0 additions & 7 deletions
This file was deleted.

poetry.lock

Lines changed: 494 additions & 490 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "Feecc-Workbench-Daemon"
3-
version = "1.3.1"
3+
version = "1.3.2"
44
description = "Workbench software for the Feecc QA system"
55
authors = ["arseniiarsenii <arseniivelichko2@gmail.com>"]
66
license = "Apache 2.0"
@@ -12,36 +12,36 @@ PyYAML = "^6.0"
1212
brother-ql = "^0.9.4"
1313
qrcode = "^7.3.1"
1414
python-barcode = "^0.14.0"
15-
fastapi = "^0.78.0"
16-
uvicorn = "^0.17.6"
15+
fastapi = "^0.81.0"
16+
uvicorn = "^0.18.3"
1717
dnspython = "^2.2.1"
1818
loguru = "^0.6.0"
1919
motor = "^3.0.0"
2020
httpx = "^0.23.0"
2121
environ-config = "^22.1.0"
22-
yarl = "^1.7.2"
23-
sse-starlette = "^0.10.3"
22+
yarl = "^1.8.1"
23+
sse-starlette = "^1.1.6"
2424
python-dotenv = "^0.20.0"
25-
robonomics-interface = "^1.1.0"
25+
robonomics-interface = "^1.2.2"
2626
ecs-logging = "^2.0.0"
2727
aioprometheus = "^22.5.0"
2828

2929
[tool.poetry.dev-dependencies]
30-
mypy = "^0.960"
31-
flake8 = "^4.0.1"
32-
types-PyYAML = "^6.0.10"
33-
types-requests = "^2.28.3"
34-
pytest = "^7.1.2"
30+
mypy = "^0.971"
31+
flake8 = "^5.0.4"
32+
types-PyYAML = "^6.0.11"
33+
types-requests = "^2.28.9"
34+
pytest = "^7.1.3"
3535
vulture = "^2.5"
3636
pre-commit = "^2.20.0"
3737
pytest-cov = "^3.0.0"
38-
ssort = "^0.11.5"
38+
ssort = "^0.11.6"
3939
black = {extras = ["d"], version = "^22.6.0"}
4040
flake8-new-union-types = "^0.4.1"
4141
flake8-pep585 = "^0.1.5.1"
42-
pyupgrade = "^2.37.2"
42+
pyupgrade = "^2.37.3"
4343
isort = "^5.10.1"
44-
poetryup = "^0.7.2"
44+
poetryup = "^0.10.0"
4545

4646
[tool.pytest.ini_options]
4747
markers = ["short_url"]

src/_workbench_router.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def get_schema_list_entry(schema: mdl.ProductionSchema) -> mdl.SchemaListEntry:
152152
for schema in sorted(all_schemas.values(), key=lambda s: bool(s.is_composite), reverse=True)
153153
if schema.schema_id not in handled_schemas
154154
]
155+
available_schemas.sort(key=lambda le: len(le.schema_name))
155156

156157
return mdl.SchemasList(
157158
status_code=status.HTTP_200_OK,

src/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import uvicorn
24
from aioprometheus.asgi.middleware import MetricsMiddleware
35
from aioprometheus.asgi.starlette import metrics
@@ -45,6 +47,8 @@
4547
def startup_event() -> None:
4648
check_service_connectivity()
4749
MongoDbWrapper()
50+
app_version = os.getenv("VERSION", "Unknown")
51+
logger.info(f"Runtime app version: {app_version}")
4852

4953

5054
@app.on_event("shutdown")

src/feecc_workbench/Unit.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ._Barcode import Barcode
1212
from .Employee import Employee
1313
from .Messenger import messenger
14+
from .metrics import metrics
1415
from .models import ProductionSchema
1516
from .ProductionStage import ProductionStage
1617
from .Types import AdditionalInfo
@@ -230,5 +231,6 @@ async def end_operation(
230231
f"Unit has no more pending production stages. Unit status changed: {prev_status.value} -> "
231232
f"{self.status.value}"
232233
)
234+
metrics.register_complete_unit(None, self)
233235

234236
self.employee = None

src/feecc_workbench/WorkBench.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .exceptions import StateForbiddenError
1414
from .ipfs import publish_file
1515
from .Messenger import messenger
16+
from .metrics import metrics
1617
from .models import ProductionSchema
1718
from .passport_generator import construct_unit_passport
1819
from .printer import print_image
@@ -73,14 +74,15 @@ async def create_new_unit(self, schema: ProductionSchema) -> Unit:
7374
os.remove(unit.barcode.filename)
7475

7576
await self._database.push_unit(unit)
77+
metrics.register_create_unit(self.employee, unit)
7678

7779
return unit
7880

7981
def _validate_state_transition(self, new_state: State) -> None:
8082
"""check if state transition can be performed using the map"""
8183
if new_state not in STATE_TRANSITION_MAP.get(self.state, []):
8284
message = f"State transition from {self.state.value} to {new_state.value} is not allowed."
83-
messenger.error(f"Переход из состояния {self.state.value} в состояние {new_state.value} невозможен")
85+
messenger.error("Недопустимая смена состояния")
8486
raise StateForbiddenError(message)
8587

8688
def switch_state(self, new_state: State) -> None:
@@ -102,6 +104,7 @@ def log_in(self, employee: Employee) -> None:
102104
messenger.success(f"Авторизован {employee.position} {employee.name}")
103105

104106
self.switch_state(State.AUTHORIZED_IDLING_STATE)
107+
metrics.register_log_in(employee)
105108

106109
@logger.catch(reraise=True, exclude=(StateForbiddenError, AssertionError))
107110
def log_out(self) -> None:
@@ -115,6 +118,7 @@ def log_out(self) -> None:
115118
message = f"Employee {self.employee.name} was logged out at the workbench no. {self.number}"
116119
logger.info(message)
117120
messenger.success(f"{self.employee.name} вышел из системы")
121+
metrics.register_log_out(self.employee)
118122
self.employee = None
119123

120124
self.switch_state(State.AWAIT_LOGIN_STATE)
@@ -143,11 +147,7 @@ def _get_unit_list(unit_: Unit) -> list[Unit]:
143147

144148
if not (override or unit.status in allowed):
145149
message = f"Can only assign unit with status: {', '.join(s.value for s in allowed)}. Unit status is {unit.status.value}. Forbidden."
146-
messenger.warning(
147-
f"На стол могут быть помещены изделия со статусами:"
148-
f" {', '.join(s.value.upper() for s in allowed)}."
149-
f" Статус изделия: {unit.status.value.upper()}. Отказано."
150-
)
150+
messenger.warning("Сборка изделия уже была завершена, пасспорт выпущен. Отказано.")
151151
raise AssertionError(message)
152152

153153
self.unit = unit
@@ -267,6 +267,7 @@ async def end_operation(self, additional_info: AdditionalInfo | None = None, pre
267267
await self._database.push_unit(self.unit, include_components=False)
268268

269269
self.switch_state(State.UNIT_ASSIGNED_IDLING_STATE)
270+
metrics.register_complete_operation(self.employee, self.unit)
270271

271272
@logger.catch(reraise=True, exclude=(StateForbiddenError, AssertionError))
272273
async def upload_unit_passport(self) -> None:
@@ -338,6 +339,7 @@ async def _bg_generate_short_url(url: str, unit_internal_id: str) -> None:
338339
asyncio.create_task(post_to_datalog(cid, self.unit.internal_id))
339340

340341
await self._database.push_unit(self.unit)
342+
metrics.register_generate_passport(self.employee, self.unit)
341343

342344
async def shutdown(self) -> None:
343345
logger.info("Workbench shutdown sequence initiated")

src/feecc_workbench/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
from collections.abc import Iterable
34

45
import environ
56
from dotenv import load_dotenv
@@ -79,10 +80,29 @@ class HidDevicesNames:
7980
hid_devices = environ.group(HidDevicesNames)
8081

8182

83+
def export_docker_secrets(secret_names: Iterable[str]) -> None:
84+
"""
85+
Export all the requested docker secrets into the environment
86+
87+
A secret and the corresponding variable are expected to have the same name,
88+
but uppercase for the variable and lowercase for the secret file.
89+
"""
90+
for secret in secret_names:
91+
secret_path = f"/run/secrets/{secret.lower()}"
92+
if not os.path.exists(secret_path):
93+
continue
94+
with open(secret_path) as f:
95+
content = f.read()
96+
os.environ[secret.upper()] = content
97+
logger.debug(f"Loaded up {secret.upper()} secret from Docker secrets")
98+
99+
82100
if __name__ == "__main__":
83101
print(environ.generate_help(AppConfig))
84102

85103
try:
104+
docker_secrets = ["mongodb_uri", "robonomics_account_seed", "yourls_username", "yourls_password"]
105+
export_docker_secrets(docker_secrets)
86106
CONFIG = environ.to_config(AppConfig)
87107
except environ.MissingEnvValueError as e:
88108
logger.critical(f"Missing required environment variable '{e}'. Exiting.")

src/feecc_workbench/exceptions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
from copy import copy
12
from typing import Any
23

34
from .metrics import Metrics
45

56

67
class TrackedException(Exception):
8+
"""An exception that increments Prometheus metric counter for itself"""
9+
710
_labels: dict[str, str] = {}
811

912
def __init__(self, *args: Any) -> None:
13+
labels = copy(self._labels)
14+
if args:
15+
labels["message"] = args[0]
1016
Metrics().register(
1117
name=self.__class__.__name__,
1218
description=self.__class__.__doc__,
13-
labels=self._labels,
19+
labels=labels,
1420
)
1521
super().__init__(*args)
1622

@@ -31,3 +37,9 @@ class StateForbiddenError(TrackedException):
3137
"""Raised when state transition is forbidden"""
3238

3339
pass
40+
41+
42+
class RobonomicsError(TrackedException):
43+
"""Raised when Robonmics transactions fail"""
44+
45+
pass

0 commit comments

Comments
 (0)