mlrun 1.3.1rc5__py3-none-any.whl → 1.4.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/__main__.py +57 -4
- mlrun/api/api/endpoints/marketplace.py +57 -4
- mlrun/api/api/endpoints/runs.py +2 -0
- mlrun/api/api/utils.py +102 -0
- mlrun/api/crud/__init__.py +1 -0
- mlrun/api/crud/marketplace.py +133 -44
- mlrun/api/crud/notifications.py +80 -0
- mlrun/api/crud/runs.py +2 -0
- mlrun/api/crud/secrets.py +1 -0
- mlrun/api/db/base.py +32 -0
- mlrun/api/db/session.py +3 -11
- mlrun/api/db/sqldb/db.py +162 -1
- mlrun/api/db/sqldb/models/models_mysql.py +41 -0
- mlrun/api/db/sqldb/models/models_sqlite.py +35 -0
- mlrun/api/main.py +54 -1
- mlrun/api/migrations_mysql/versions/c905d15bd91d_notifications.py +70 -0
- mlrun/api/migrations_sqlite/versions/959ae00528ad_notifications.py +61 -0
- mlrun/api/schemas/__init__.py +1 -0
- mlrun/api/schemas/marketplace.py +18 -8
- mlrun/api/{db/filedb/__init__.py → schemas/notification.py} +17 -1
- mlrun/api/utils/singletons/db.py +8 -14
- mlrun/builder.py +37 -26
- mlrun/config.py +12 -2
- mlrun/data_types/spark.py +9 -2
- mlrun/datastore/base.py +10 -1
- mlrun/datastore/sources.py +1 -1
- mlrun/db/__init__.py +6 -4
- mlrun/db/base.py +1 -2
- mlrun/db/httpdb.py +32 -6
- mlrun/db/nopdb.py +463 -0
- mlrun/db/sqldb.py +47 -7
- mlrun/execution.py +3 -0
- mlrun/feature_store/api.py +26 -12
- mlrun/feature_store/common.py +1 -1
- mlrun/feature_store/steps.py +110 -13
- mlrun/k8s_utils.py +10 -0
- mlrun/model.py +43 -0
- mlrun/projects/operations.py +5 -2
- mlrun/projects/pipelines.py +4 -3
- mlrun/projects/project.py +50 -10
- mlrun/run.py +5 -4
- mlrun/runtimes/__init__.py +2 -6
- mlrun/runtimes/base.py +82 -31
- mlrun/runtimes/function.py +22 -0
- mlrun/runtimes/kubejob.py +10 -8
- mlrun/runtimes/serving.py +1 -1
- mlrun/runtimes/sparkjob/__init__.py +0 -1
- mlrun/runtimes/sparkjob/abstract.py +0 -2
- mlrun/serving/states.py +2 -2
- mlrun/utils/helpers.py +1 -1
- mlrun/utils/notifications/notification/__init__.py +1 -1
- mlrun/utils/notifications/notification/base.py +14 -13
- mlrun/utils/notifications/notification/console.py +6 -3
- mlrun/utils/notifications/notification/git.py +19 -12
- mlrun/utils/notifications/notification/ipython.py +6 -3
- mlrun/utils/notifications/notification/slack.py +13 -12
- mlrun/utils/notifications/notification_pusher.py +185 -37
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/METADATA +6 -2
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/RECORD +64 -63
- mlrun/api/db/filedb/db.py +0 -518
- mlrun/db/filedb.py +0 -899
- mlrun/runtimes/sparkjob/spark2job.py +0 -59
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/LICENSE +0 -0
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/WHEEL +0 -0
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/entry_points.txt +0 -0
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/top_level.txt +0 -0
mlrun/api/db/base.py
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import datetime
|
|
15
|
+
import typing
|
|
15
16
|
import warnings
|
|
16
17
|
from abc import ABC, abstractmethod
|
|
17
18
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
18
19
|
|
|
20
|
+
import mlrun.model
|
|
19
21
|
from mlrun.api import schemas
|
|
20
22
|
|
|
21
23
|
|
|
@@ -103,6 +105,7 @@ class DBInterface(ABC):
|
|
|
103
105
|
max_partitions: int = 0,
|
|
104
106
|
requested_logs: bool = None,
|
|
105
107
|
return_as_run_structs: bool = True,
|
|
108
|
+
with_notifications: bool = False,
|
|
106
109
|
):
|
|
107
110
|
pass
|
|
108
111
|
|
|
@@ -575,3 +578,32 @@ class DBInterface(ABC):
|
|
|
575
578
|
self, session, name: str, project: str
|
|
576
579
|
) -> schemas.BackgroundTask:
|
|
577
580
|
pass
|
|
581
|
+
|
|
582
|
+
@abstractmethod
|
|
583
|
+
def store_run_notifications(
|
|
584
|
+
self,
|
|
585
|
+
session,
|
|
586
|
+
notification_objects: typing.List[mlrun.model.Notification],
|
|
587
|
+
run_uid: str,
|
|
588
|
+
project: str,
|
|
589
|
+
):
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
@abstractmethod
|
|
593
|
+
def list_run_notifications(
|
|
594
|
+
self,
|
|
595
|
+
session,
|
|
596
|
+
run_uid: str,
|
|
597
|
+
project: str,
|
|
598
|
+
) -> typing.List[mlrun.model.Notification]:
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
def delete_run_notifications(
|
|
602
|
+
self,
|
|
603
|
+
session,
|
|
604
|
+
name: str = None,
|
|
605
|
+
run_uid: str = None,
|
|
606
|
+
project: str = None,
|
|
607
|
+
commit: bool = True,
|
|
608
|
+
):
|
|
609
|
+
pass
|
mlrun/api/db/session.py
CHANGED
|
@@ -15,22 +15,14 @@
|
|
|
15
15
|
from sqlalchemy.orm import Session
|
|
16
16
|
|
|
17
17
|
from mlrun.api.db.sqldb.session import create_session as sqldb_create_session
|
|
18
|
-
from mlrun.config import config
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
def create_session(
|
|
22
|
-
|
|
23
|
-
if db_type == "filedb":
|
|
24
|
-
return None
|
|
25
|
-
else:
|
|
26
|
-
return sqldb_create_session()
|
|
20
|
+
def create_session() -> Session:
|
|
21
|
+
return sqldb_create_session()
|
|
27
22
|
|
|
28
23
|
|
|
29
24
|
def close_session(db_session):
|
|
30
|
-
|
|
31
|
-
# will be None when it's filedb session
|
|
32
|
-
if db_session is not None:
|
|
33
|
-
db_session.close()
|
|
25
|
+
db_session.close()
|
|
34
26
|
|
|
35
27
|
|
|
36
28
|
def run_function_with_new_db_session(func):
|
mlrun/api/db/sqldb/db.py
CHANGED
|
@@ -31,7 +31,9 @@ from sqlalchemy.orm import Session, aliased
|
|
|
31
31
|
import mlrun
|
|
32
32
|
import mlrun.api.db.session
|
|
33
33
|
import mlrun.api.utils.projects.remotes.follower
|
|
34
|
+
import mlrun.api.utils.singletons.k8s
|
|
34
35
|
import mlrun.errors
|
|
36
|
+
import mlrun.model
|
|
35
37
|
from mlrun.api import schemas
|
|
36
38
|
from mlrun.api.db.base import DBInterface
|
|
37
39
|
from mlrun.api.db.sqldb.helpers import (
|
|
@@ -53,6 +55,7 @@ from mlrun.api.db.sqldb.models import (
|
|
|
53
55
|
Function,
|
|
54
56
|
Log,
|
|
55
57
|
MarketplaceSource,
|
|
58
|
+
Notification,
|
|
56
59
|
Project,
|
|
57
60
|
Run,
|
|
58
61
|
Schedule,
|
|
@@ -325,6 +328,7 @@ class SQLDB(DBInterface):
|
|
|
325
328
|
max_partitions: int = 0,
|
|
326
329
|
requested_logs: bool = None,
|
|
327
330
|
return_as_run_structs: bool = True,
|
|
331
|
+
with_notifications: bool = False,
|
|
328
332
|
):
|
|
329
333
|
project = project or config.default_project
|
|
330
334
|
query = self._find_runs(session, uid, project, labels)
|
|
@@ -369,9 +373,28 @@ class SQLDB(DBInterface):
|
|
|
369
373
|
if not return_as_run_structs:
|
|
370
374
|
return query.all()
|
|
371
375
|
|
|
376
|
+
# Purposefully not using outer join to avoid returning runs without notifications
|
|
377
|
+
if with_notifications:
|
|
378
|
+
query = query.join(Notification, Run.id == Notification.run)
|
|
379
|
+
|
|
372
380
|
runs = RunList()
|
|
373
381
|
for run in query:
|
|
374
|
-
|
|
382
|
+
run_struct = run.struct
|
|
383
|
+
if with_notifications:
|
|
384
|
+
run_struct.setdefault("spec", {}).setdefault("notifications", [])
|
|
385
|
+
run_struct.setdefault("status", {}).setdefault("notifications", {})
|
|
386
|
+
for notification in run.notifications:
|
|
387
|
+
(
|
|
388
|
+
notification_spec,
|
|
389
|
+
notification_status,
|
|
390
|
+
) = self._transform_notification_record_to_spec_and_status(
|
|
391
|
+
notification
|
|
392
|
+
)
|
|
393
|
+
run_struct["spec"]["notifications"].append(notification_spec)
|
|
394
|
+
run_struct["status"]["notifications"][
|
|
395
|
+
notification.name
|
|
396
|
+
] = notification_status
|
|
397
|
+
runs.append(run_struct)
|
|
375
398
|
|
|
376
399
|
return runs
|
|
377
400
|
|
|
@@ -1689,6 +1712,10 @@ class SQLDB(DBInterface):
|
|
|
1689
1712
|
self._verify_empty_list_of_project_related_resources(name, logs, "logs")
|
|
1690
1713
|
runs = self._find_runs(session, None, name, []).all()
|
|
1691
1714
|
self._verify_empty_list_of_project_related_resources(name, runs, "runs")
|
|
1715
|
+
notifications = self._get_db_notifications(session, project=name)
|
|
1716
|
+
self._verify_empty_list_of_project_related_resources(
|
|
1717
|
+
name, notifications, "notifications"
|
|
1718
|
+
)
|
|
1692
1719
|
schedules = self.list_schedules(session, project=name)
|
|
1693
1720
|
self._verify_empty_list_of_project_related_resources(
|
|
1694
1721
|
name, schedules, "schedules"
|
|
@@ -1709,6 +1736,7 @@ class SQLDB(DBInterface):
|
|
|
1709
1736
|
def delete_project_related_resources(self, session: Session, name: str):
|
|
1710
1737
|
self.del_artifacts(session, project=name)
|
|
1711
1738
|
self._delete_logs(session, name)
|
|
1739
|
+
self.delete_run_notifications(session, project=name)
|
|
1712
1740
|
self.del_runs(session, project=name)
|
|
1713
1741
|
self.delete_schedules(session, name)
|
|
1714
1742
|
self._delete_functions(session, name)
|
|
@@ -2832,6 +2860,13 @@ class SQLDB(DBInterface):
|
|
|
2832
2860
|
query = query.filter(Run.uid.in_(uid))
|
|
2833
2861
|
return self._add_labels_filter(session, query, Run, labels)
|
|
2834
2862
|
|
|
2863
|
+
def _get_db_notifications(
|
|
2864
|
+
self, session, name: str = None, run_id: int = None, project: str = None
|
|
2865
|
+
):
|
|
2866
|
+
return self._query(
|
|
2867
|
+
session, Notification, name=name, run=run_id, project=project
|
|
2868
|
+
).all()
|
|
2869
|
+
|
|
2835
2870
|
def _latest_uid_filter(self, session, query):
|
|
2836
2871
|
# Create a sub query of latest uid (by updated) per (project,key)
|
|
2837
2872
|
subq = (
|
|
@@ -3168,6 +3203,35 @@ class SQLDB(DBInterface):
|
|
|
3168
3203
|
# TODO: handle transforming the functions/workflows/artifacts references to real objects
|
|
3169
3204
|
return schemas.Project(**project_record.full_object)
|
|
3170
3205
|
|
|
3206
|
+
def _transform_notification_record_to_spec_and_status(
|
|
3207
|
+
self,
|
|
3208
|
+
notification_record: Notification,
|
|
3209
|
+
) -> typing.Tuple[dict, dict]:
|
|
3210
|
+
notification_spec = self._transform_notification_record_to_schema(
|
|
3211
|
+
notification_record
|
|
3212
|
+
).to_dict()
|
|
3213
|
+
notification_status = {
|
|
3214
|
+
"status": notification_spec.pop("status", None),
|
|
3215
|
+
"sent_time": notification_spec.pop("sent_time", None),
|
|
3216
|
+
}
|
|
3217
|
+
return notification_spec, notification_status
|
|
3218
|
+
|
|
3219
|
+
@staticmethod
|
|
3220
|
+
def _transform_notification_record_to_schema(
|
|
3221
|
+
notification_record: Notification,
|
|
3222
|
+
) -> mlrun.model.Notification:
|
|
3223
|
+
return mlrun.model.Notification(
|
|
3224
|
+
kind=notification_record.kind,
|
|
3225
|
+
name=notification_record.name,
|
|
3226
|
+
message=notification_record.message,
|
|
3227
|
+
severity=notification_record.severity,
|
|
3228
|
+
when=notification_record.when.split(","),
|
|
3229
|
+
condition=notification_record.condition,
|
|
3230
|
+
params=notification_record.params,
|
|
3231
|
+
status=notification_record.status,
|
|
3232
|
+
sent_time=notification_record.sent_time,
|
|
3233
|
+
)
|
|
3234
|
+
|
|
3171
3235
|
def _move_and_reorder_table_items(
|
|
3172
3236
|
self, session, moved_object, move_to=None, move_from=None
|
|
3173
3237
|
):
|
|
@@ -3543,3 +3607,100 @@ class SQLDB(DBInterface):
|
|
|
3543
3607
|
):
|
|
3544
3608
|
return True
|
|
3545
3609
|
return False
|
|
3610
|
+
|
|
3611
|
+
def store_run_notifications(
|
|
3612
|
+
self,
|
|
3613
|
+
session,
|
|
3614
|
+
notification_objects: typing.List[mlrun.model.Notification],
|
|
3615
|
+
run_uid: str,
|
|
3616
|
+
project: str,
|
|
3617
|
+
):
|
|
3618
|
+
# iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
|
|
3619
|
+
run = self._get_run(session, run_uid, project, 0)
|
|
3620
|
+
if not run:
|
|
3621
|
+
raise mlrun.errors.MLRunNotFoundError(
|
|
3622
|
+
f"Run not found: uid={run_uid}, project={project}"
|
|
3623
|
+
)
|
|
3624
|
+
|
|
3625
|
+
run_notifications = {
|
|
3626
|
+
notification.name: notification
|
|
3627
|
+
for notification in self._get_db_notifications(session, run_id=run.id)
|
|
3628
|
+
}
|
|
3629
|
+
notifications = []
|
|
3630
|
+
for notification_model in notification_objects:
|
|
3631
|
+
new_notification = False
|
|
3632
|
+
notification = run_notifications.get(notification_model.name, None)
|
|
3633
|
+
if not notification:
|
|
3634
|
+
new_notification = True
|
|
3635
|
+
notification = Notification(
|
|
3636
|
+
name=notification_model.name, run=run.id, project=project
|
|
3637
|
+
)
|
|
3638
|
+
|
|
3639
|
+
notification.kind = notification_model.kind
|
|
3640
|
+
notification.message = notification_model.message
|
|
3641
|
+
notification.severity = notification_model.severity
|
|
3642
|
+
notification.when = ",".join(notification_model.when)
|
|
3643
|
+
notification.condition = notification_model.condition
|
|
3644
|
+
notification.params = notification_model.params
|
|
3645
|
+
notification.status = (
|
|
3646
|
+
notification_model.status
|
|
3647
|
+
or mlrun.api.schemas.NotificationStatus.PENDING
|
|
3648
|
+
)
|
|
3649
|
+
notification.sent_time = notification_model.sent_time
|
|
3650
|
+
|
|
3651
|
+
logger.debug(
|
|
3652
|
+
f"Storing {'new' if new_notification else 'existing'} notification",
|
|
3653
|
+
notification_name=notification.name,
|
|
3654
|
+
run_uid=run_uid,
|
|
3655
|
+
project=project,
|
|
3656
|
+
)
|
|
3657
|
+
notifications.append(notification)
|
|
3658
|
+
|
|
3659
|
+
self._upsert(session, notifications)
|
|
3660
|
+
|
|
3661
|
+
def list_run_notifications(
|
|
3662
|
+
self,
|
|
3663
|
+
session,
|
|
3664
|
+
run_uid: str,
|
|
3665
|
+
project: str = "",
|
|
3666
|
+
) -> typing.List[mlrun.model.Notification]:
|
|
3667
|
+
|
|
3668
|
+
# iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
|
|
3669
|
+
run = self._get_run(session, run_uid, project, 0)
|
|
3670
|
+
if not run:
|
|
3671
|
+
return []
|
|
3672
|
+
|
|
3673
|
+
return [
|
|
3674
|
+
self._transform_notification_record_to_schema(notification)
|
|
3675
|
+
for notification in self._query(session, Notification, run=run.id).all()
|
|
3676
|
+
]
|
|
3677
|
+
|
|
3678
|
+
def delete_run_notifications(
|
|
3679
|
+
self,
|
|
3680
|
+
session,
|
|
3681
|
+
name: str = None,
|
|
3682
|
+
run_uid: str = None,
|
|
3683
|
+
project: str = None,
|
|
3684
|
+
commit: bool = True,
|
|
3685
|
+
):
|
|
3686
|
+
run_id = None
|
|
3687
|
+
if run_uid:
|
|
3688
|
+
|
|
3689
|
+
# iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
|
|
3690
|
+
run = self._get_run(session, run_uid, project, 0)
|
|
3691
|
+
if not run:
|
|
3692
|
+
raise mlrun.errors.MLRunNotFoundError(
|
|
3693
|
+
f"Run not found: uid={run_uid}, project={project}"
|
|
3694
|
+
)
|
|
3695
|
+
run_id = run.id
|
|
3696
|
+
|
|
3697
|
+
project = project or config.default_project
|
|
3698
|
+
if project == "*":
|
|
3699
|
+
project = None
|
|
3700
|
+
|
|
3701
|
+
query = self._get_db_notifications(session, name, run_id, project)
|
|
3702
|
+
for notification in query:
|
|
3703
|
+
session.delete(notification)
|
|
3704
|
+
|
|
3705
|
+
if commit:
|
|
3706
|
+
session.commit()
|
|
@@ -139,6 +139,46 @@ with warnings.catch_warnings():
|
|
|
139
139
|
def get_identifier_string(self) -> str:
|
|
140
140
|
return f"{self.project}/{self.name}/{self.uid}"
|
|
141
141
|
|
|
142
|
+
class Notification(Base, mlrun.utils.db.BaseModel):
|
|
143
|
+
__tablename__ = "notifications"
|
|
144
|
+
__table_args__ = (UniqueConstraint("name", "run", name="_notifications_uc"),)
|
|
145
|
+
|
|
146
|
+
id = Column(Integer, primary_key=True)
|
|
147
|
+
project = Column(String(255, collation=SQLCollationUtil.collation()))
|
|
148
|
+
name = Column(
|
|
149
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
150
|
+
)
|
|
151
|
+
kind = Column(
|
|
152
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
153
|
+
)
|
|
154
|
+
message = Column(
|
|
155
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
156
|
+
)
|
|
157
|
+
severity = Column(
|
|
158
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
159
|
+
)
|
|
160
|
+
when = Column(
|
|
161
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
162
|
+
)
|
|
163
|
+
condition = Column(
|
|
164
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
165
|
+
)
|
|
166
|
+
params = Column("params", JSON)
|
|
167
|
+
run = Column(Integer, ForeignKey("runs.id"))
|
|
168
|
+
|
|
169
|
+
# TODO: Separate table for notification state.
|
|
170
|
+
# Currently, we are only supporting one notification being sent per DB row (either on completion or on error).
|
|
171
|
+
# In the future, we might want to support multiple notifications per DB row, and we might want to support on
|
|
172
|
+
# start, therefore we need to separate the state from the notification itself (e.g. this table can be table
|
|
173
|
+
# with notification_id, state, when, last_sent, etc.). This will require some refactoring in the code.
|
|
174
|
+
sent_time = Column(
|
|
175
|
+
sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3),
|
|
176
|
+
nullable=True,
|
|
177
|
+
)
|
|
178
|
+
status = Column(
|
|
179
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
180
|
+
)
|
|
181
|
+
|
|
142
182
|
class Log(Base, mlrun.utils.db.BaseModel):
|
|
143
183
|
__tablename__ = "logs"
|
|
144
184
|
|
|
@@ -182,6 +222,7 @@ with warnings.catch_warnings():
|
|
|
182
222
|
|
|
183
223
|
labels = relationship(Label, cascade="all, delete-orphan")
|
|
184
224
|
tags = relationship(Tag, cascade="all, delete-orphan")
|
|
225
|
+
notifications = relationship(Notification, cascade="all, delete-orphan")
|
|
185
226
|
|
|
186
227
|
def get_identifier_string(self) -> str:
|
|
187
228
|
return f"{self.project}/{self.uid}/{self.iteration}"
|
|
@@ -151,6 +151,40 @@ with warnings.catch_warnings():
|
|
|
151
151
|
def get_identifier_string(self) -> str:
|
|
152
152
|
return f"{self.project}/{self.uid}"
|
|
153
153
|
|
|
154
|
+
class Notification(Base, mlrun.utils.db.BaseModel):
|
|
155
|
+
__tablename__ = "notifications"
|
|
156
|
+
__table_args__ = (UniqueConstraint("name", "run", name="_notifications_uc"),)
|
|
157
|
+
|
|
158
|
+
id = Column(Integer, primary_key=True)
|
|
159
|
+
project = Column(String(255, collation=SQLCollationUtil.collation()))
|
|
160
|
+
name = Column(
|
|
161
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
162
|
+
)
|
|
163
|
+
kind = Column(
|
|
164
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
165
|
+
)
|
|
166
|
+
message = Column(
|
|
167
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
168
|
+
)
|
|
169
|
+
severity = Column(
|
|
170
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
171
|
+
)
|
|
172
|
+
when = Column(
|
|
173
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
174
|
+
)
|
|
175
|
+
condition = Column(
|
|
176
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
177
|
+
)
|
|
178
|
+
params = Column("params", JSON)
|
|
179
|
+
run = Column(Integer, ForeignKey("runs.id"))
|
|
180
|
+
sent_time = Column(
|
|
181
|
+
TIMESTAMP(),
|
|
182
|
+
nullable=True,
|
|
183
|
+
)
|
|
184
|
+
status = Column(
|
|
185
|
+
String(255, collation=SQLCollationUtil.collation()), nullable=False
|
|
186
|
+
)
|
|
187
|
+
|
|
154
188
|
class Run(Base, mlrun.utils.db.HasStruct):
|
|
155
189
|
__tablename__ = "runs"
|
|
156
190
|
__table_args__ = (
|
|
@@ -178,6 +212,7 @@ with warnings.catch_warnings():
|
|
|
178
212
|
requested_logs = Column(BOOLEAN)
|
|
179
213
|
updated = Column(TIMESTAMP, default=datetime.utcnow)
|
|
180
214
|
labels = relationship(Label)
|
|
215
|
+
notifications = relationship(Notification, cascade="all, delete-orphan")
|
|
181
216
|
|
|
182
217
|
def get_identifier_string(self) -> str:
|
|
183
218
|
return f"{self.project}/{self.uid}/{self.iteration}"
|
mlrun/api/main.py
CHANGED
|
@@ -24,12 +24,14 @@ import sqlalchemy.orm
|
|
|
24
24
|
import uvicorn
|
|
25
25
|
from fastapi.exception_handlers import http_exception_handler
|
|
26
26
|
|
|
27
|
+
import mlrun.api.db.base
|
|
27
28
|
import mlrun.api.schemas
|
|
28
29
|
import mlrun.api.utils.clients.chief
|
|
29
30
|
import mlrun.api.utils.clients.log_collector
|
|
30
31
|
import mlrun.errors
|
|
31
32
|
import mlrun.lists
|
|
32
33
|
import mlrun.utils
|
|
34
|
+
import mlrun.utils.notifications
|
|
33
35
|
import mlrun.utils.version
|
|
34
36
|
from mlrun.api.api.api import api_router
|
|
35
37
|
from mlrun.api.db.session import close_session, create_session
|
|
@@ -56,6 +58,14 @@ from mlrun.utils import logger
|
|
|
56
58
|
API_PREFIX = "/api"
|
|
57
59
|
BASE_VERSIONED_API_PREFIX = f"{API_PREFIX}/v1"
|
|
58
60
|
|
|
61
|
+
# When pushing notifications, push notifications only for runs that entered a terminal state
|
|
62
|
+
# since the last time we pushed notifications.
|
|
63
|
+
# On the first time we push notifications, we'll push notifications for all runs that are in a terminal state
|
|
64
|
+
# and their notifications haven't been sent yet.
|
|
65
|
+
# TODO: find better solution than a global variable for chunking the list of runs
|
|
66
|
+
# for which to push notifications
|
|
67
|
+
_last_notification_push_time: datetime.datetime = None
|
|
68
|
+
|
|
59
69
|
|
|
60
70
|
app = fastapi.FastAPI(
|
|
61
71
|
title="MLRun",
|
|
@@ -512,18 +522,20 @@ async def _align_worker_state_with_chief_state(
|
|
|
512
522
|
|
|
513
523
|
|
|
514
524
|
def _monitor_runs():
|
|
525
|
+
db = get_db()
|
|
515
526
|
db_session = create_session()
|
|
516
527
|
try:
|
|
517
528
|
for kind in RuntimeKinds.runtime_with_handlers():
|
|
518
529
|
try:
|
|
519
530
|
runtime_handler = get_runtime_handler(kind)
|
|
520
|
-
runtime_handler.monitor_runs(
|
|
531
|
+
runtime_handler.monitor_runs(db, db_session)
|
|
521
532
|
except Exception as exc:
|
|
522
533
|
logger.warning(
|
|
523
534
|
"Failed monitoring runs. Ignoring",
|
|
524
535
|
exc=err_to_str(exc),
|
|
525
536
|
kind=kind,
|
|
526
537
|
)
|
|
538
|
+
_push_terminal_run_notifications(db, db_session)
|
|
527
539
|
finally:
|
|
528
540
|
close_session(db_session)
|
|
529
541
|
|
|
@@ -545,6 +557,47 @@ def _cleanup_runtimes():
|
|
|
545
557
|
close_session(db_session)
|
|
546
558
|
|
|
547
559
|
|
|
560
|
+
def _push_terminal_run_notifications(db: mlrun.api.db.base.DBInterface, db_session):
|
|
561
|
+
"""
|
|
562
|
+
Get all runs with notification configs which became terminal since the last call to the function
|
|
563
|
+
and push their notifications if they haven't been pushed yet.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
# Import here to avoid circular import
|
|
567
|
+
import mlrun.api.api.utils
|
|
568
|
+
|
|
569
|
+
# When pushing notifications, push notifications only for runs that entered a terminal state
|
|
570
|
+
# since the last time we pushed notifications.
|
|
571
|
+
# On the first time we push notifications, we'll push notifications for all runs that are in a terminal state
|
|
572
|
+
# and their notifications haven't been sent yet.
|
|
573
|
+
global _last_notification_push_time
|
|
574
|
+
|
|
575
|
+
runs = db.list_runs(
|
|
576
|
+
db_session,
|
|
577
|
+
project="*",
|
|
578
|
+
states=mlrun.runtimes.constants.RunStates.terminal_states(),
|
|
579
|
+
last_update_time_from=_last_notification_push_time,
|
|
580
|
+
with_notifications=True,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
if not len(runs):
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
# Unmasking the run parameters from secrets before handing them over to the notification handler
|
|
587
|
+
# as importing the `Secrets` crud in the notification handler will cause a circular import
|
|
588
|
+
unmasked_runs = [
|
|
589
|
+
mlrun.api.api.utils.unmask_notification_params_secret_on_task(run)
|
|
590
|
+
for run in runs
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
logger.debug(
|
|
594
|
+
"Got terminal runs with configured notifications", runs_amount=len(runs)
|
|
595
|
+
)
|
|
596
|
+
mlrun.utils.notifications.NotificationPusher(unmasked_runs).push(db)
|
|
597
|
+
|
|
598
|
+
_last_notification_push_time = datetime.datetime.now(datetime.timezone.utc)
|
|
599
|
+
|
|
600
|
+
|
|
548
601
|
async def _stop_logs():
|
|
549
602
|
"""
|
|
550
603
|
Stop logs for runs that are in terminal state and last updated in the previous interval
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright 2018 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""notifications
|
|
16
|
+
|
|
17
|
+
Revision ID: c905d15bd91d
|
|
18
|
+
Revises: 88e656800d6a
|
|
19
|
+
Create Date: 2022-09-20 10:44:41.727488
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
import sqlalchemy as sa
|
|
23
|
+
from alembic import op
|
|
24
|
+
from sqlalchemy.dialects import mysql
|
|
25
|
+
|
|
26
|
+
# revision identifiers, used by Alembic.
|
|
27
|
+
revision = "c905d15bd91d"
|
|
28
|
+
down_revision = "88e656800d6a"
|
|
29
|
+
branch_labels = None
|
|
30
|
+
depends_on = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def upgrade():
|
|
34
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
35
|
+
op.create_table(
|
|
36
|
+
"notifications",
|
|
37
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
38
|
+
sa.Column("project", sa.String(length=255, collation="utf8_bin")),
|
|
39
|
+
sa.Column("name", sa.String(length=255, collation="utf8_bin"), nullable=False),
|
|
40
|
+
sa.Column("kind", sa.String(length=255, collation="utf8_bin"), nullable=False),
|
|
41
|
+
sa.Column(
|
|
42
|
+
"message", sa.String(length=255, collation="utf8_bin"), nullable=False
|
|
43
|
+
),
|
|
44
|
+
sa.Column(
|
|
45
|
+
"severity", sa.String(length=255, collation="utf8_bin"), nullable=False
|
|
46
|
+
),
|
|
47
|
+
sa.Column("when", sa.String(length=255, collation="utf8_bin"), nullable=False),
|
|
48
|
+
sa.Column(
|
|
49
|
+
"condition", sa.String(length=255, collation="utf8_bin"), nullable=False
|
|
50
|
+
),
|
|
51
|
+
sa.Column("params", sa.JSON(), nullable=True),
|
|
52
|
+
sa.Column("run", sa.Integer(), nullable=True),
|
|
53
|
+
sa.Column("sent_time", mysql.TIMESTAMP(fsp=3), nullable=True),
|
|
54
|
+
sa.Column(
|
|
55
|
+
"status", sa.String(length=255, collation="utf8_bin"), nullable=False
|
|
56
|
+
),
|
|
57
|
+
sa.ForeignKeyConstraint(
|
|
58
|
+
["run"],
|
|
59
|
+
["runs.id"],
|
|
60
|
+
),
|
|
61
|
+
sa.PrimaryKeyConstraint("id"),
|
|
62
|
+
sa.UniqueConstraint("name", "run", name="_notifications_uc"),
|
|
63
|
+
)
|
|
64
|
+
# ### end Alembic commands ###
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def downgrade():
|
|
68
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
69
|
+
op.drop_table("notifications")
|
|
70
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright 2018 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""notifications
|
|
16
|
+
|
|
17
|
+
Revision ID: 959ae00528ad
|
|
18
|
+
Revises: 803438ecd005
|
|
19
|
+
Create Date: 2022-09-20 10:40:41.354209
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
import sqlalchemy as sa
|
|
23
|
+
from alembic import op
|
|
24
|
+
|
|
25
|
+
# revision identifiers, used by Alembic.
|
|
26
|
+
revision = "959ae00528ad"
|
|
27
|
+
down_revision = "803438ecd005"
|
|
28
|
+
branch_labels = None
|
|
29
|
+
depends_on = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def upgrade():
|
|
33
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
34
|
+
op.create_table(
|
|
35
|
+
"notifications",
|
|
36
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
37
|
+
sa.Column("project", sa.String(length=255)),
|
|
38
|
+
sa.Column("name", sa.String(length=255), nullable=False),
|
|
39
|
+
sa.Column("kind", sa.String(length=255), nullable=False),
|
|
40
|
+
sa.Column("message", sa.String(length=255), nullable=False),
|
|
41
|
+
sa.Column("severity", sa.String(length=255), nullable=False),
|
|
42
|
+
sa.Column("when", sa.String(length=255), nullable=False),
|
|
43
|
+
sa.Column("condition", sa.String(length=255), nullable=False),
|
|
44
|
+
sa.Column("params", sa.JSON(), nullable=True),
|
|
45
|
+
sa.Column("run", sa.Integer(), nullable=True),
|
|
46
|
+
sa.Column("sent_time", sa.TIMESTAMP(), nullable=True),
|
|
47
|
+
sa.Column("status", sa.String(length=255), nullable=False),
|
|
48
|
+
sa.ForeignKeyConstraint(
|
|
49
|
+
["run"],
|
|
50
|
+
["runs.id"],
|
|
51
|
+
),
|
|
52
|
+
sa.PrimaryKeyConstraint("id"),
|
|
53
|
+
sa.UniqueConstraint("name", "run", name="_notifications_uc"),
|
|
54
|
+
)
|
|
55
|
+
# ### end Alembic commands ###
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def downgrade():
|
|
59
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
60
|
+
op.drop_table("notifications")
|
|
61
|
+
# ### end Alembic commands ###
|
mlrun/api/schemas/__init__.py
CHANGED
|
@@ -107,6 +107,7 @@ from .model_endpoints import (
|
|
|
107
107
|
ModelEndpointStatus,
|
|
108
108
|
ModelMonitoringStoreKinds,
|
|
109
109
|
)
|
|
110
|
+
from .notification import NotificationSeverity, NotificationStatus
|
|
110
111
|
from .object import ObjectKind, ObjectMetadata, ObjectSpec, ObjectStatus
|
|
111
112
|
from .pipeline import PipelinesFormat, PipelinesOutput, PipelinesPagination
|
|
112
113
|
from .project import (
|