Skip to content

Commit a73eff4

Browse files
authored
Merge pull request #160 from arenaxr/mongodb
feat(persist): add direct pymongo persist db connection for all reads, rm REST persist reads
2 parents 204d61f + 7087f3f commit a73eff4

11 files changed

Lines changed: 205 additions & 99 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ drf-yasg==1.21.14
1313
python-dotenv==1.2.1
1414
requests==2.32.5
1515
django-autocomplete-light==3.12.1
16+
pymongo==4.10.1

users/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
def post_migration_callback(sender, **kwargs):
66
from . import startup
77

8-
startup.setup_socialapps()
8+
startup.setup_databases()
99

1010

1111
class UsersConfig(AppConfig):

users/forms.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from django.contrib.auth.models import User
66

77
from .models import Device, Namespace, Scene
8-
from .mqtt import TOPIC_SUPPORTED_API_VERSIONS, all_scenes_read_token
9-
from .persistence import get_persist_scenes_ns
8+
from .persistence import read_persist_scenes_by_namespace
109

1110

1211
class SocialSignupForm(_SocialSignupForm):
@@ -30,9 +29,7 @@ def clean(self):
3029
self.add_error("username", msg)
3130
# reject usernames in form on signup: Namespace used in persist db
3231
else:
33-
version = TOPIC_SUPPORTED_API_VERSIONS[0] # TODO (mwfarb): resolve missing request.version
34-
token = all_scenes_read_token(version)
35-
if len(get_persist_scenes_ns(token, username)) > 0:
32+
if len(read_persist_scenes_by_namespace([username])) > 0:
3633
msg = f"Sorry, '{username}' is a persistence namespace."
3734
self.add_error("username", msg)
3835

users/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919
RE_NS_SLASH_ID, "Only alphanumeric, underscore, hyphen, in namespace/idname format allowed."
2020
)
2121

22+
# assign accessible model for persist collection
23+
def get_arenaobjects_collection():
24+
from .persist_db import get_persist_db
25+
return get_persist_db()['arenaobjects']
26+
27+
# arenaobjects schema reference:
28+
# https://github.com/arenaxr/arena-persist/blob/master/server.js#L28-L42
29+
# object_id: {type: String, required: true, index: true},
30+
# type: {type: String, required: true, index: true},
31+
# attributes: {type: Object, required: true, default: {}},
32+
# expireAt: {type: Date, expires: 0},
33+
# realm: {type: String, required: true, index: true},
34+
# namespace: {type: String, required: true, index: true, default: 'public'},
35+
# sceneId: {type: String, required: true, index: true},
36+
# private: {type: Boolean},
37+
# program_id: {type: String},
38+
2239

2340
class NamespaceDefault:
2441
def __init__(self, name=""):

users/mqtt.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,6 @@
2929
TOPIC_SUPPORTED_API_VERSIONS = [API_V2]
3030

3131

32-
def all_scenes_read_token(version):
33-
config = settings.PUBSUB
34-
privkeyfile = settings.MQTT_TOKEN_PRIVKEY
35-
if not os.path.exists(privkeyfile):
36-
print("Error: keyfile not found" + privkeyfile)
37-
return None
38-
with open(privkeyfile) as privatefile:
39-
private_key = privatefile.read()
40-
41-
realm = config["mqtt_realm"]
42-
username = config["mqtt_username"]
43-
44-
payload = {}
45-
payload["sub"] = username
46-
payload["exp"] = datetime.datetime.utcnow() + DEF_JWT_DURATION
47-
48-
if version == API_V2:
49-
payload["subs"] = [f"{realm}/s/+/+/o/+/+"] # v2
50-
else:
51-
payload["subs"] = [f"{realm}/s/#"] # v1
52-
53-
token = jwt.encode(payload, private_key, algorithm="RS256")
54-
return token
55-
56-
5732
def generate_arena_token(
5833
*,
5934
user,

users/persist_db.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
import logging
3+
4+
from pymongo import MongoClient
5+
from pymongo.database import Database
6+
7+
'''
8+
persist_db.py: This Mongo connection manager is triggered at startup and checked
9+
before each database call, this allows a lazy connection to Mongo. There is no
10+
explicit disconnect from Mongo. PyMongo manages its own connection pooling and the OS
11+
handles socket cleanup on process exit. Plus, Django shutdown is hard to detect.
12+
'''
13+
14+
client: MongoClient = None
15+
db: Database = None
16+
17+
logging.getLogger("pymongo").setLevel(logging.WARNING)
18+
19+
def get_persist_db():
20+
global client, db
21+
if db is not None:
22+
return db
23+
24+
# connect to mongodb, read-only
25+
print("arena_persist: connecting...")
26+
client = MongoClient("mongodb://mongodb/arena_persist?readPreference=primaryPreferred")
27+
28+
try:
29+
dba = client.admin
30+
server_status = dba.command('serverStatus')
31+
current_connections = server_status['connections']['current']
32+
print(f"arena_persist: current connections: {current_connections}")
33+
total_connections = server_status['connections']['totalCreated']
34+
print(f"arena_persist: total connections created: {total_connections}")
35+
36+
except Exception as e:
37+
print(f"arena_persist: error: {e}")
38+
39+
db = client.arena_persist
40+
return db

users/persistence.py

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,102 @@
11
import json
2+
from datetime import datetime
23

34
import requests
5+
from bson import ObjectId
46
from requests.exceptions import HTTPError
57

8+
from .models import get_arenaobjects_collection
69
from .utils import get_rest_host
710

811
PERSIST_TIMEOUT = 30 # 30 seconds
912

1013

11-
def get_scene_objects(token, scene):
12-
# get scene from persist
13-
verify, host = get_rest_host()
14-
url = f"https://{host}/persist/{scene}"
15-
result = _urlopen(url, token, "GET", verify)
16-
if result:
17-
return json.loads(result)
18-
return []
14+
# Mongo DB PyMongo queries for Persist:
15+
# https://pymongo.readthedocs.io/en/stable/index.html
1916

2017

21-
def delete_scene_objects(token, scene):
22-
# delete scene from persist
23-
verify, host = get_rest_host()
24-
url = f"https://{host}/persist/{scene}"
25-
result = _urlopen(url, token, "DELETE", verify)
26-
return result
18+
class MongoJSONEncoder(json.JSONEncoder):
19+
def default(self, o):
20+
if isinstance(o, ObjectId):
21+
return str(o) # Convert ObjectId to string
22+
if isinstance(o, datetime):
23+
return o.isoformat() # Convert datetime to ISO format
24+
return super().default(o) # Call the default method for other types
2725

2826

29-
def get_persist_ns_all(token):
30-
# request all scenes from persist
31-
verify, host = get_rest_host()
32-
url = f"https://{host}/persist/!allnamespaces"
33-
result = _urlopen(url, token, "GET", verify)
34-
if result:
35-
return json.loads(result)
36-
return []
27+
def read_persist_ns_all():
28+
arenaobjects = get_arenaobjects_collection().aggregate([{
29+
"$group": {
30+
"_id": {
31+
"namespace": "$namespace",
32+
}
33+
}
34+
}
35+
])
36+
return [doc['_id']['namespace'] for doc in arenaobjects]
3737

3838

39-
def get_persist_scenes_all(token):
40-
# request all scenes from persist
41-
verify, host = get_rest_host()
42-
url = f"https://{host}/persist/!allscenes"
43-
result = _urlopen(url, token, "GET", verify)
44-
if result:
45-
return json.loads(result)
46-
return []
39+
def read_persist_scenes_all():
40+
arenaobjects = get_arenaobjects_collection().aggregate([
41+
{
42+
"$group": {
43+
"_id": {
44+
"namespace": "$namespace",
45+
"sceneId": "$sceneId",
46+
}
47+
}
48+
}
49+
])
50+
unique_scenes = []
51+
for doc in arenaobjects:
52+
ns = doc['_id']['namespace']
53+
sc = doc['_id']['sceneId']
54+
unique_scenes.append(f"{ns}/{sc}")
55+
56+
return unique_scenes
57+
4758

59+
def read_persist_scenes_by_namespace(namespaces):
60+
arenaobjects = get_arenaobjects_collection().aggregate([
61+
{
62+
"$match": {
63+
"namespace": {"$in": namespaces}
64+
}
65+
},
66+
{
67+
"$group": {
68+
"_id": {
69+
"namespace": "$namespace",
70+
"sceneId": "$sceneId",
71+
}
72+
}
73+
}
74+
])
75+
unique_scenes = []
76+
for doc in arenaobjects:
77+
ns = doc['_id']['namespace']
78+
sc = doc['_id']['sceneId']
79+
unique_scenes.append(f"{ns}/{sc}")
4880

49-
def get_persist_scenes_ns(token, namespace):
50-
# request all namespace scenes from persist
81+
return unique_scenes
82+
83+
84+
def read_persist_scene_objects(namespace, scene):
85+
query = {"namespace": namespace, "sceneId": scene}
86+
arenaobjects = get_arenaobjects_collection().find(query)
87+
json_str = MongoJSONEncoder().encode(list(arenaobjects))
88+
return json.loads(json_str)
89+
90+
91+
# Mongo DB REST queries for Persist:
92+
93+
94+
def delete_scene_objects(token, scene):
95+
# delete scene objects from persist
5196
verify, host = get_rest_host()
52-
url = f"https://{host}/persist/{namespace}/!allscenes"
53-
result = _urlopen(url, token, "GET", verify)
54-
if result:
55-
return json.loads(result)
56-
return []
97+
url = f"https://{host}/persist/{scene}"
98+
result = _urlopen(url, token, "DELETE", verify)
99+
return result
57100

58101

59102
def _urlopen(url, token, method, verify):

users/serializers.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class Meta:
1313
"name",
1414
"editors",
1515
"viewers",
16-
"is_default",
1716
]
1817

1918

@@ -42,7 +41,6 @@ class Meta:
4241
"anonymous_users",
4342
"video_conference",
4443
"users",
45-
"is_default",
4644
]
4745

4846

users/startup.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,22 @@
33
from allauth.socialaccount.models import SocialApp
44
from django.conf import settings
55
from django.contrib.sites.models import Site
6+
from users.models import (
7+
SCENE_ANON_USERS_DEF,
8+
SCENE_PUBLIC_READ_DEF,
9+
SCENE_PUBLIC_WRITE_DEF,
10+
SCENE_USERS_DEF,
11+
SCENE_VIDEO_CONF_DEF,
12+
Scene,
13+
)
614

715

8-
def setup_socialapps():
16+
from users.persist_db import get_persist_db
17+
18+
def setup_databases():
19+
# Force db connection
20+
get_persist_db()
21+
922
# Sites db must have a SITE_ID row that equals our host
1023
host = os.getenv("HOSTNAME")
1124
site_id = settings.SITE_ID
@@ -17,7 +30,9 @@ def setup_socialapps():
1730
# check that another site is not using our host, remove
1831
host_inst = Site.objects.filter(domain=host)
1932
if host_inst.exists():
20-
host_inst.delete()
33+
count, _ = host_inst.delete()
34+
if count > 0:
35+
print(f"Deleted Site entries matching host: {count}")
2136
# update proper site_id with host
2237
id_inst.name = host
2338
id_inst.domain = host
@@ -27,4 +42,19 @@ def setup_socialapps():
2742
id_inst.save()
2843

2944
# social apps are defined in settings.py, so remove legacy allauth entires that might conflict
30-
SocialApp.objects.filter(provider='google').delete()
45+
count, _ = SocialApp.objects.filter(provider='google').delete()
46+
if count > 0:
47+
print(f"Deleted legacy SocialApp entries: {count}")
48+
49+
# check for scene permissions where Scene.is_default is True and remove them
50+
count, _ = Scene.objects.filter(
51+
public_read=SCENE_PUBLIC_READ_DEF,
52+
public_write=SCENE_PUBLIC_WRITE_DEF,
53+
anonymous_users=SCENE_ANON_USERS_DEF,
54+
video_conference=SCENE_VIDEO_CONF_DEF,
55+
users=SCENE_USERS_DEF,
56+
editors__isnull=True,
57+
viewers__isnull=True,
58+
).delete()
59+
if count > 0:
60+
print(f"Deleted redundant default Scene permission entries: {count}")

users/templates/users/user_profile.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ <h5>Namespace Permissions
135135
<tr>
136136
<!--name-->
137137
<td><b>{{ namespace.name }}</b>
138-
{% if namespace.is_default %}
139-
<b> (default)</b>
140-
{% endif %}
141138
</td>
142139
<!--user account-->
143140
<td>
@@ -215,9 +212,6 @@ <h5>Scene Permissions <button id="button_expand_add_scene" type="button" title="
215212
<!--name-->
216213
<td><a
217214
href="{{ request.scheme }}://{{ request.META.HTTP_HOST }}/{{ scene.name }}"><b>{{ scene.name }}</b></a>
218-
{% if scene.is_default %}
219-
<b> (default)</b>
220-
{% endif %}
221215
</td>
222216
{% if user.is_staff %}
223217
<!--persisted in db-->

0 commit comments

Comments
 (0)