Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Add 'factory' support #1336

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/nxt_conf_validation.c
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,11 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = {
.type = NXT_CONF_VLDT_STRING,
.validator = nxt_conf_vldt_targets_exclusive,
.u.string = "callable",
}, {
.name = nxt_string("factory"),
.type = NXT_CONF_VLDT_BOOLEAN,
.validator = nxt_conf_vldt_targets_exclusive,
.u.string = "factory",
}, {
.name = nxt_string("prefix"),
.type = NXT_CONF_VLDT_STRING,
Expand All @@ -865,6 +870,9 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_target_members[] = {
}, {
.name = nxt_string("callable"),
.type = NXT_CONF_VLDT_STRING,
}, {
.name = nxt_string("factory"),
.type = NXT_CONF_VLDT_BOOLEAN,
}, {
.name = nxt_string("prefix"),
.type = NXT_CONF_VLDT_STRING,
Expand All @@ -883,6 +891,9 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_notargets_members[] = {
}, {
.name = nxt_string("callable"),
.type = NXT_CONF_VLDT_STRING,
}, {
.name = nxt_string("factory"),
.type = NXT_CONF_VLDT_BOOLEAN,
}, {
.name = nxt_string("prefix"),
.type = NXT_CONF_VLDT_STRING,
Expand Down
27 changes: 26 additions & 1 deletion src/python/nxt_python.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,13 @@ nxt_python_set_target(nxt_task_t *task, nxt_python_target_t *target,
char *callable, *module_name;
PyObject *module, *obj;
nxt_str_t str;
nxt_bool_t is_factory = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like default value assigning can be optimised by using else branch for if (value != NULL) { condition later. It avoids reassigning in case when value != NULL is true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That test is just to see if the factory variable is set. It may be set to false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean something like

    value = nxt_conf_get_object_member(conf, &factory_flag_str, NULL);          
    if (!value) {                                                        
        if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                        
            nxt_alert(task, "\"%s\" in module \"%s\" is not a callable object", 
                      callable, module_name);                                   
            goto fail;                                                          
        }                                                                       
    } else {                                                                    
        nxt_bool_t  is_factory = nxt_conf_get_boolean(value);
                               
        if (is_factory) {                                                       
            if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                    
                nxt_alert(task,                                                 
                          "factory \"%s\" in module \"%s\" ",                   
                          "can not be called to fetch callable", callable,      
                          module_name);                                         
                goto fail;                                                      
            }                                                                   
                                                                                
            obj = PyObject_CallObject(obj, NULL);                               
            if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                    
                nxt_alert(task,                                                 
                          "factory \"%s\" in module \"%s\" ",                   
                          "did not return callable object", callable, module_name);                                         
                goto fail;                                                      
            }                                                                   
        }                                                                       
    }

Aside from adding an extra level of indentation. If is_factory is false we still need to do the nxt_slow_path(PyCallable_Check(obj) == 0 check...

We also want to distinguish between a factory failure vs an ordinary callable failure and without duplicating the above callable check.

If on the other hand you mean something like (untested)

    value = nxt_conf_get_object_member(conf, &factory_flag_str, NULL);          
    if (value != NULL && nxt_conf_get_boolean(value) == 1) {                                                    
        if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                        
            nxt_alert(task,                                                     
                      "factory \"%s\" in module \"%s\" ",                       
                      "can not be called to fetch callable", callable,
                      module_name);
            goto fail;                                                          
        }                                                                       
                                                                                
        obj = PyObject_CallObject(obj, NULL);                                   
        if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                        
            nxt_alert(task,                                                     
                      "factory \"%s\" in module \"%s\" ",                       
                      "did not return callable object", callable, module_name); 
            goto fail;                                                          
        }                                                                       
                                                                                
    } else if (nxt_slow_path(PyCallable_Check(obj) == 0)) {                     
        nxt_alert(task, "\"%s\" in module \"%s\" is not a callable object",     
                  callable, module_name);                                       
        goto fail;                                                              
    }

It's not terrible...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author has already added what I suggested:

    if (value != NULL) {
        is_factory = nxt_conf_get_boolean(value);
    } else {
        is_factory = 0;
    }

Thanks @gourav-kandoria

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of

if (value != NULL && nxt_conf_get_boolean(value) == 1) {

and I wouldn't want to see that kind of thing spread, but I could live with it in this case...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author has already added what I suggested:

    if (value != NULL) {
        is_factory = nxt_conf_get_boolean(value);
    } else {
        is_factory = 0;
    }

Thanks @gourav-kandoria

Sorry, there is no way that is an improvement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is I explicitly asked for it to be done the way it was to avoid doing it the above way which just introduces unnecessary code.

You mention about optimisations. I'm curious what use case you're optimising for?

This is not a hot path.

Even then I doubt the above is an optimisation as it introduces more code, which generates more assembly and will likely have a larger i-cache footprint.

To illustrate take the following exampe

int main(void)
{
	int x = 42;
	int i = 0;

	if (x == 42)
		i = x;

	return 0;
}

This produces the following assembly

.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$42, -4(%rbp)
	movl	$0, -8(%rbp)
	cmpl	$42, -4(%rbp)
	jne	.L2
	movl	-4(%rbp), %eax
	movl	%eax, -8(%rbp)
.L2:
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

You can see thw two movl instructions initialising the variables.

We have one compare and jump.

Now lets look at what happens when we make it a if/then/else

int main(void)
{
	int x = 42;
	int i;

	if (x == 42)
		i = x;
	else
		i = 0;

	return 0;
}

That produces the following assembly

.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$42, -4(%rbp)
	cmpl	$42, -4(%rbp)
	jne	.L2
	movl	-4(%rbp), %eax
	movl	%eax, -8(%rbp)
	jmp	.L3
.L2:
	movl	$0, -8(%rbp)
.L3:
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

Yes, there is only one movl instruction at the start, but we now have two jumps.

In the common case that I would suggest is that factory is not set, is_factory is set to 0 (once) in both cases.

But again this is all kind of moot, as this is not a hot path and cleaner code wins.

@gourav-kandoria

I would strongly suggest reverting this change...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is not a hot path

Then it make sense to optimise it using nxt_slow_path().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is not a hot path

By that I mean nxt_python_set_target() as a whole, it's only called at startup and maybe reconfigure time, not thousands or even hundreds or tens of times a second.

Then it make sense to optimise it using nxt_slow_path().

We could, I'm not sue what we're optimising for here though.

likely/unlikely should really only be used in cases where you know for certain it's one way or the other a majority of time in all cases/workloads.

We seem to sprinkle these things around willy nilly.

Error branches are perhaps still a valid use... but again you really need to measure/profile this stuff...

Copy link
Contributor Author

@gourav-kandoria gourav-kandoria Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ac000 sure, can revert it. BTW good to see this type of discussion. gave me some perspective about how to think about things at this level. just before reverting. @andrey-zelenkov are you also okay with @ac000 viewpoint. would wait for your reply before making any change as I would like to make only one and final change now 😅 😅

nxt_conf_value_t *value;

static nxt_str_t module_str = nxt_string("module");
static nxt_str_t callable_str = nxt_string("callable");
static nxt_str_t prefix_str = nxt_string("prefix");
static nxt_str_t factory_flag_str = nxt_string("factory");

module = obj = NULL;

Expand Down Expand Up @@ -449,7 +451,30 @@ nxt_python_set_target(nxt_task_t *task, nxt_python_target_t *target,
goto fail;
}

if (nxt_slow_path(PyCallable_Check(obj) == 0)) {
value = nxt_conf_get_object_member(conf, &factory_flag_str, NULL);
if (value != NULL) {
is_factory = nxt_conf_get_boolean(value);
}

if (is_factory) {
if (nxt_slow_path(PyCallable_Check(obj) == 0)) {
nxt_alert(task,
"factory \"%s\" in module \"%s\" "
"can not be called to fetch callable",
callable, module_name);
goto fail;
}

obj = PyObject_CallObject(obj, NULL);
if (nxt_slow_path(PyCallable_Check(obj) == 0)) {
nxt_alert(task,
"factory \"%s\" in module \"%s\" "
"did not return callable object",
callable, module_name);
goto fail;
}

} else if (nxt_slow_path(PyCallable_Check(obj) == 0)) {
nxt_alert(task, "\"%s\" in module \"%s\" is not a callable object",
callable, module_name);
goto fail;
Expand Down
23 changes: 23 additions & 0 deletions test/python/factory/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
def wsgi_a(env, start_response):
start_response("200", [("Content-Length", "1")])
return [b"1"]


def wsgi_b(env, start_response):
start_response("200", [("Content-Length", "1")])
return [b"2"]


def wsgi_a_factory():
return wsgi_a


def wsgi_b_factory():
return wsgi_b


wsgi_invalid_callable = None


def wsgi_factory_returning_invalid_callable():
return wsgi_invalid_callable
140 changes: 140 additions & 0 deletions test/test_python_factory.py
andrey-zelenkov marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from unit.applications.lang.python import ApplicationPython
from unit.option import option

prerequisites = {"modules": {"python": "all"}}

client = ApplicationPython()


def test_python_factory_targets():
python_dir = f"{option.test_dir}/python"

assert "success" in client.conf(
{
"listeners": {
"*:8080": {"pass": "applications/targets/1"},
"*:8081": {"pass": "applications/targets/2"},
"*:8082": {"pass": "applications/targets/factory-1"},
"*:8083": {"pass": "applications/targets/factory-2"},
},
"applications": {
"targets": {
"type": client.get_application_type(),
"working_directory": f"{python_dir}/factory/",
"path": f"{python_dir}/factory/",
"targets": {
"1": {
"module": "wsgi",
"callable": "wsgi_a",
"factory": False,
},
"2": {
"module": "wsgi",
"callable": "wsgi_b",
"factory": False,
},
"factory-1": {
"module": "wsgi",
"callable": "wsgi_a_factory",
"factory": True,
},
"factory-2": {
"module": "wsgi",
"callable": "wsgi_b_factory",
"factory": True,
},
},
}
},
}
)

resp = client.get(port=8080)
assert resp["status"] == 200
assert resp["body"] == "1"

resp = client.get(port=8081)
assert resp["status"] == 200
assert resp["body"] == "2"

resp = client.get(port=8082)
assert resp["status"] == 200
assert resp["body"] == "1"

resp = client.get(port=8083)
assert resp["status"] == 200
assert resp["body"] == "2"


def test_python_factory_without_targets():
python_dir = f"{option.test_dir}/python"

assert "success" in client.conf(
{
"listeners": {
"*:8080": {"pass": "applications/python-app-factory"},
"*:8081": {"pass": "applications/python-app"},
},
"applications": {
"python-app-factory": {
"type": client.get_application_type(),
"working_directory": f"{python_dir}/factory/",
"path": f"{python_dir}/factory/",
"module": "wsgi",
"callable": "wsgi_a_factory",
"factory": True,
},
"python-app": {
"type": client.get_application_type(),
"working_directory": f"{python_dir}/factory/",
"path": f"{python_dir}/factory/",
"module": "wsgi",
"callable": "wsgi_b",
"factory": False,
},
},
}
)

resp = client.get(port=8080)
assert resp["status"] == 200
assert resp["body"] == "1"

resp = client.get(port=8081)
assert resp["status"] == 200
assert resp["body"] == "2"


def test_python_factory_invalid_callable_value(skip_alert):
skip_alert(
r"failed to apply new conf",
r"did not return callable object",
r"can not be called to fetch callable",
)
python_dir = f"{option.test_dir}/python"

invalid_callable_values = [
"wsgi_factory_returning_invalid_callable",
"wsgi_invalid_callable",
]

for callable_value in invalid_callable_values:
assert "error" in client.conf(
{
"listeners": {"*:8080": {"pass": "applications/targets/1"}},
"applications": {
"targets": {
"type": client.get_application_type(),
"working_directory": f"{python_dir}/factory/",
"path": f"{python_dir}/factory/",
"targets": {
"1": {
"module": "wsgi",
"callable": callable_value,
"factory": True,
},
},
}
},
}
)
Loading