Skip to content

Commit e72215f

Browse files
committed
Merge branch 'master' of github.com:StackStorm/st2 into optimize_escaped_dict_fields
2 parents 1a932ca + f48877f commit e72215f

39 files changed

Lines changed: 7865 additions & 144 deletions

CHANGELOG.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ Changed
187187

188188
* Updated cryptography dependency to version 3.3.2 to avoid CVE-2020-36242 (security) #5151
189189

190+
* Update most of the code in the StackStorm API and services layer to utilize ``orjson`` library
191+
for serializing and de-serializing json.
192+
193+
That should result in better json serialization and deserialization performance.
194+
195+
The change should be fully backward compatible, only difference is that API JSON responses now
196+
won't be indented using 4 spaces by default (indenting adds unnecessary overhead and if needed,
197+
the response can be pretty formatted on the client side using ``jq`` or similar). (improvement)
198+
#5153
199+
200+
Contributed by @Kami
201+
190202
Fixed
191203
~~~~~
192204

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,10 @@ micro-benchmarks: requirements .micro-benchmarks
530530
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file -s -v st2common/benchmarks/micro/test_mongo_field_types.py -k "test_save_large_execution"
531531
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file -s -v st2common/benchmarks/micro/test_mongo_field_types.py -k "test_read_large_execution"
532532
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file -s -v st2common/benchmarks/micro/test_mongo_field_types.py -k "test_save_multiple_fields"
533-
533+
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:dict_keys_count_and_depth -s -v st2common/benchmarks/micro/test_fast_deepcopy.py -k "test_fast_deepcopy_with_dict_values"
534+
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file -s -v st2common/benchmarks/micro/test_fast_deepcopy.py -k "test_fast_deepcopy_with_json_fixture_file"
535+
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file,param:indent_sort_keys_tuple -s -v st2common/benchmarks/micro/test_json_serialization_and_deserialization.py -k "test_json_dumps"
536+
. $(VIRTUALENV_DIR)/bin/activate; pytest --benchmark-only --benchmark-name=short --benchmark-columns=min,max,mean,stddev,median,ops,rounds --benchmark-group-by=group,param:fixture_file -s -v st2common/benchmarks/micro/test_json_serialization_and_deserialization.py -k "test_json_loads"
534537

535538
.PHONY: .cleanmongodb
536539
.cleanmongodb:

contrib/runners/action_chain_runner/action_chain_runner/action_chain_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from st2common.util import jinja as jinja_utils
4949
from st2common.util import param as param_utils
5050
from st2common.util.config_loader import get_config
51-
from st2common.util.ujson import fast_deepcopy
51+
from st2common.util.deep_copy import fast_deepcopy_dict
5252

5353
__all__ = ["ActionChainRunner", "ChainHolder", "get_runner", "get_metadata"]
5454

@@ -85,7 +85,7 @@ def init_vars(self, action_parameters, action_context=None):
8585
)
8686

8787
def restore_vars(self, ctx_vars):
88-
self.vars.update(fast_deepcopy(ctx_vars))
88+
self.vars.update(fast_deepcopy_dict(ctx_vars))
8989

9090
def validate(self):
9191
"""

contrib/runners/local_runner/tests/integration/test_localrunner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ def test_script_with_parameters_parameter_serialization(self):
453453
self.assertIn("PARAM_FLOAT=2.55", result["stdout"])
454454
self.assertIn("PARAM_BOOLEAN=1", result["stdout"])
455455
self.assertIn("PARAM_LIST=a,b,c", result["stdout"])
456-
self.assertIn('PARAM_OBJECT={"foo": "bar"}', result["stdout"])
456+
self.assertIn('PARAM_OBJECT={"foo":"bar"}', result["stdout"])
457457

458458
action_parameters = {
459459
"param_string": "test string",
@@ -515,12 +515,12 @@ def test_script_with_parameters_parameter_serialization(self):
515515
self.assertIn("PARAM_FLOAT=2.55", result["stdout"])
516516
self.assertIn("PARAM_BOOLEAN=1", result["stdout"])
517517
self.assertIn("PARAM_LIST=a,b,c", result["stdout"])
518-
self.assertIn('PARAM_OBJECT={"foo": "bar"}', result["stdout"])
518+
self.assertIn('PARAM_OBJECT={"foo":"bar"}', result["stdout"])
519519

520520
output_dbs = ActionExecutionOutput.query(output_type="stdout")
521521
self.assertEqual(len(output_dbs), 6)
522522
self.assertEqual(output_dbs[0].data, "PARAM_STRING=test string\n")
523-
self.assertEqual(output_dbs[5].data, 'PARAM_OBJECT={"foo": "bar"}\n')
523+
self.assertEqual(output_dbs[5].data, 'PARAM_OBJECT={"foo":"bar"}\n')
524524

525525
output_dbs = ActionExecutionOutput.query(output_type="stderr")
526526
self.assertEqual(len(output_dbs), 0)

contrib/runners/orquesta_runner/orquesta_runner/orquesta_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from st2common.services import action as ac_svc
3636
from st2common.services import workflows as wf_svc
3737
from st2common.util import api as api_util
38-
from st2common.util import ujson
38+
from st2common.util import deep_copy
3939

4040
__all__ = ["OrquestaRunner", "get_runner", "get_metadata"]
4141

@@ -59,7 +59,7 @@ def _get_notify_config(self):
5959
)
6060

6161
def _construct_context(self, wf_ex):
62-
ctx = ujson.fast_deepcopy(self.context)
62+
ctx = deep_copy.fast_deepcopy_dict(self.context)
6363
ctx["workflow_execution"] = str(wf_ex.id)
6464

6565
return ctx

contrib/runners/python_runner/python_runner/python_action_wrapper.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828

2929
import distutils.sysconfig
3030

31+
# NOTE: We intentionally use orjson directly here instead of json_encode - orjson.dumps relies
32+
# on config option which we don't parse for the action wrapper since it speeds things down - action
33+
# wrapper should rely on as little imports as possible to make Python runner executions fast -
34+
# that's very important.
35+
import orjson
36+
3137
# Note: This work-around is required to fix the issue with other Python modules which live
3238
# inside this directory polluting and masking sys.path for Python runner actions.
3339
# Since this module is ran as a Python script inside a subprocess, directory where the script
@@ -46,7 +52,6 @@
4652
sys.path.insert(0, distutils.sysconfig.get_python_lib())
4753

4854
import sys
49-
import json
5055
import argparse
5156

5257
import six
@@ -233,19 +238,25 @@ def run(self):
233238
# Special case if result object is not JSON serializable - aka user wanted to return a
234239
# non-simple type (e.g. class instance or other non-JSON serializable type)
235240
try:
236-
json.dumps(action_output["result"])
237-
except TypeError:
241+
orjson.dumps(action_output["result"])
242+
except (TypeError, orjson.JSONDecodeError):
238243
action_output["result"] = str(action_output["result"])
239244

240245
try:
241-
print_output = json.dumps(action_output)
246+
print_output = orjson.dumps(action_output)
242247
except Exception:
243-
print_output = str(action_output)
248+
print_output = str(action_output).encode("utf-8")
249+
250+
# Data is bytes so we use sys.stdout.buffer which works with bytes and not sys.stdout
251+
# which works with strings / unicodes.
252+
# This way it also works correctly with unicode sequences.
253+
# Technically we could also write to sys.stdout, but this would require additional
254+
# conversion back and forth
244255

245256
# Print output to stdout so the parent can capture it
246-
sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
247-
sys.stdout.write(print_output + "\n")
248-
sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
257+
sys.stdout.buffer.write(ACTION_OUTPUT_RESULT_DELIMITER.encode("utf-8"))
258+
sys.stdout.buffer.write(print_output + b"\n")
259+
sys.stdout.buffer.write(ACTION_OUTPUT_RESULT_DELIMITER.encode("utf-8"))
249260
sys.stdout.flush()
250261

251262
def _get_action_instance(self):
@@ -317,9 +328,9 @@ def _get_action_instance(self):
317328
)
318329
args = parser.parse_args()
319330

320-
config = json.loads(args.config) if args.config else {}
331+
config = orjson.loads(args.config) if args.config else {}
321332
user = args.user
322-
parent_args = json.loads(args.parent_args) if args.parent_args else []
333+
parent_args = orjson.loads(args.parent_args) if args.parent_args else []
323334
log_level = args.log_level
324335

325336
if not isinstance(config, dict):
@@ -330,7 +341,7 @@ def _get_action_instance(self):
330341
if args.parameters:
331342
LOG.debug("Getting parameters from argument")
332343
args_parameters = args.parameters
333-
args_parameters = json.loads(args_parameters) if args_parameters else {}
344+
args_parameters = orjson.loads(args_parameters) if args_parameters else {}
334345
parameters.update(args_parameters)
335346

336347
if args.stdin_parameters:
@@ -349,7 +360,7 @@ def _get_action_instance(self):
349360
stdin_data = sys.stdin.readline().strip()
350361

351362
try:
352-
stdin_parameters = json.loads(stdin_data)
363+
stdin_parameters = orjson.loads(stdin_data)
353364
stdin_parameters = stdin_parameters.get("parameters", {})
354365
except Exception as e:
355366
msg = (

contrib/runners/python_runner/python_runner/python_runner.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import os
1919
import re
2020
import sys
21-
import json
2221
import uuid
2322
import functools
2423
from subprocess import list2cmdline
@@ -54,6 +53,8 @@
5453
from st2common.util.shell import quote_unix
5554
from st2common.services.action import store_execution_output_data
5655
from st2common.runners.utils import make_read_and_store_stream_func
56+
from st2common.util.jsonify import json_decode
57+
from st2common.util.jsonify import json_encode
5758

5859
from python_runner import python_action_wrapper
5960

@@ -134,7 +135,7 @@ def run(self, action_parameters):
134135
LOG.debug("Getting user.")
135136
user = self.get_user()
136137
LOG.debug("Serializing parameters.")
137-
serialized_parameters = json.dumps(
138+
serialized_parameters = json_encode(
138139
action_parameters if action_parameters else {}
139140
)
140141
LOG.debug("Getting virtualenv_path.")
@@ -166,9 +167,9 @@ def run(self, action_parameters):
166167
LOG.debug("Setting args.")
167168

168169
if self._use_parent_args:
169-
parent_args = json.dumps(sys.argv[1:])
170+
parent_args = json_encode(sys.argv[1:])
170171
else:
171-
parent_args = json.dumps([])
172+
parent_args = json_encode([])
172173

173174
args = [
174175
python_path,
@@ -197,7 +198,7 @@ def run(self, action_parameters):
197198
args.append("--parameters=%s" % (serialized_parameters))
198199

199200
if self._config:
200-
args.append("--config=%s" % (json.dumps(self._config)))
201+
args.append("--config=%s" % (json_encode(self._config)))
201202

202203
if self._log_level != PYTHON_RUNNER_DEFAULT_LOG_LEVEL:
203204
# We only pass --log-level parameter if non default log level value is specified
@@ -362,7 +363,7 @@ def _get_output_values(self, exit_code, stdout, stderr, timed_out):
362363
# Parse the serialized action result object (if available)
363364
if action_result:
364365
try:
365-
action_result = json.loads(action_result)
366+
action_result = json_decode(action_result)
366367
except Exception as e:
367368
# Failed to de-serialize the result, probably it contains non-simple type or similar
368369
LOG.warning(

fixed-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ python-dateutil==2.8.0
6363
python-statsd==2.1.0
6464
prometheus_client==0.1.1
6565
ujson==1.35
66-
orjson==3.4.8
66+
orjson==3.5.0
6767
zipp>=0.5,<=1.0.0

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ networkx==1.11
3636
nose
3737
nose-parallel==0.3.1
3838
nose-timer==0.7.5
39-
orjson==3.4.8
39+
orjson==3.5.0
4040
oslo.config<1.13,>=1.12.1
4141
oslo.utils<5.0,>=4.0.0
4242
paramiko==2.7.1

st2api/st2api/controllers/v1/inquiries.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
# limitations under the License.
1515

1616
import copy
17-
import json
1817

1918
import six
2019
from oslo_config import cfg
@@ -32,6 +31,7 @@
3231
from st2common.rbac.backends import get_rbac_backend
3332
from st2common import router as api_router
3433
from st2common.services import inquiry as inquiry_service
34+
from st2common.util.jsonify import json_decode
3535

3636

3737
__all__ = ["InquiriesController"]
@@ -89,7 +89,7 @@ def get_all(
8989
# a list of dicts, and then individually convert these to InquiryResponseAPI instances
9090
inquiries = [
9191
inqy_api_models.InquiryResponseAPI.from_model(raw_inquiry, skip_db=True)
92-
for raw_inquiry in json.loads(raw_inquiries.body)
92+
for raw_inquiry in json_decode(raw_inquiries.body)
9393
]
9494

9595
# Repackage into Response with correct headers

0 commit comments

Comments
 (0)