xref: /unit/src/nxt_java.c (revision 1684:a10e10f3071f)
1 
2 /*
3  * Copyright (C) NGINX, Inc.
4  */
5 
6 
7 #include <jni.h>
8 
9 #include <nxt_main.h>
10 #include <nxt_runtime.h>
11 #include <nxt_router.h>
12 #include <nxt_unit.h>
13 #include <nxt_unit_field.h>
14 #include <nxt_unit_request.h>
15 #include <nxt_unit_response.h>
16 #include <nxt_unit_websocket.h>
17 
18 #include <java/nxt_jni.h>
19 
20 #include "java/nxt_jni_Thread.h"
21 #include "java/nxt_jni_Context.h"
22 #include "java/nxt_jni_Request.h"
23 #include "java/nxt_jni_Response.h"
24 #include "java/nxt_jni_InputStream.h"
25 #include "java/nxt_jni_OutputStream.h"
26 #include "java/nxt_jni_URLClassLoader.h"
27 
28 #include "nxt_jars.h"
29 
30 #include NXT_JAVA_MOUNTS_H
31 
32 static nxt_int_t nxt_java_setup(nxt_task_t *task, nxt_process_t *process,
33     nxt_common_app_conf_t *conf);
34 static nxt_int_t nxt_java_start(nxt_task_t *task,
35     nxt_process_data_t *data);
36 static void nxt_java_request_handler(nxt_unit_request_info_t *req);
37 static void nxt_java_websocket_handler(nxt_unit_websocket_frame_t *ws);
38 static void nxt_java_close_handler(nxt_unit_request_info_t *req);
39 static int nxt_java_ready_handler(nxt_unit_ctx_t *ctx);
40 static void *nxt_java_thread_func(void *main_ctx);
41 static int nxt_java_init_threads(nxt_java_app_conf_t *c);
42 static void nxt_java_join_threads(nxt_unit_ctx_t *ctx,
43     nxt_java_app_conf_t *c);
44 
45 static uint32_t  compat[] = {
46     NXT_VERNUM, NXT_DEBUG,
47 };
48 
49 char  *nxt_java_modules;
50 
51 static pthread_t       *nxt_java_threads;
52 static pthread_attr_t  *nxt_java_thread_attr;
53 
54 
55 #define NXT_STRING(x)   _NXT_STRING(x)
56 #define _NXT_STRING(x)  #x
57 
58 NXT_EXPORT nxt_app_module_t  nxt_app_module = {
59     sizeof(compat),
60     compat,
61     nxt_string("java"),
62     NXT_STRING(NXT_JAVA_VERSION),
63     nxt_java_mounts,
64     nxt_nitems(nxt_java_mounts),
65     nxt_java_setup,
66     nxt_java_start,
67 };
68 
69 typedef struct {
70     JavaVM               *jvm;
71     jobject              cl;
72     jobject              ctx;
73     nxt_java_app_conf_t  *conf;
74 } nxt_java_data_t;
75 
76 
77 static nxt_int_t
78 nxt_java_setup(nxt_task_t *task, nxt_process_t *process,
79     nxt_common_app_conf_t *conf)
80 {
81     char        *path, *relpath, *p, *rootfs;
82     size_t      jars_dir_len, rootfs_len;
83     const char  *unit_jars;
84 
85     rootfs = (char *) process->isolation.rootfs;
86     rootfs_len = 0;
87 
88     unit_jars = conf->u.java.unit_jars;
89     if (unit_jars == NULL) {
90         if (rootfs != NULL) {
91             unit_jars = "/";
92         } else {
93             unit_jars = NXT_JARS;
94         }
95     }
96 
97     relpath = strdup(unit_jars);
98     if (nxt_slow_path(relpath == NULL)) {
99         return NXT_ERROR;
100     }
101 
102     if (rootfs != NULL) {
103         jars_dir_len = strlen(unit_jars);
104         rootfs_len = strlen(rootfs);
105 
106         path = nxt_malloc(jars_dir_len + rootfs_len + 1);
107         if (nxt_slow_path(path == NULL)) {
108             free(relpath);
109             return NXT_ERROR;
110         }
111 
112         p = nxt_cpymem(path, process->isolation.rootfs, rootfs_len);
113         p = nxt_cpymem(p, relpath, jars_dir_len);
114         *p = '\0';
115 
116         free(relpath);
117 
118     } else {
119         path = relpath;
120     }
121 
122     nxt_java_modules = realpath(path, NULL);
123     if (nxt_java_modules == NULL) {
124         nxt_alert(task, "realpath(\"%s\") failed %E", path, nxt_errno);
125         goto free;
126     }
127 
128     if (rootfs != NULL && strlen(path) > rootfs_len) {
129         nxt_java_modules = path + rootfs_len;
130     }
131 
132     nxt_debug(task, "JAVA MODULES: %s", nxt_java_modules);
133 
134     return NXT_OK;
135 
136 free:
137 
138     nxt_free(path);
139 
140     return NXT_ERROR;
141 }
142 
143 
144 static char **
145 nxt_java_module_jars(const char *jars[], int jar_count)
146 {
147     char        **res, *jurl;
148     uint8_t     pathsep;
149     nxt_int_t   modules_len, jlen, i;
150     const char  **jar;
151 
152     res = nxt_malloc(jar_count * sizeof(char*));
153     if (res == NULL) {
154         return NULL;
155     }
156 
157     modules_len = nxt_strlen(nxt_java_modules);
158 
159     pathsep = nxt_java_modules[modules_len - 1] == '/';
160 
161     for (i = 0, jar = jars; *jar != NULL; jar++) {
162         jlen = nxt_length("file:") + modules_len
163                + (!pathsep ? nxt_length("/") : 0)
164                + nxt_strlen(*jar) + 1;
165 
166         jurl = nxt_malloc(jlen);
167         if (jurl == NULL) {
168             return NULL;
169         }
170 
171         res[i++] = jurl;
172 
173         jurl = nxt_cpymem(jurl, "file:", nxt_length("file:"));
174         jurl = nxt_cpymem(jurl, nxt_java_modules, modules_len);
175 
176         if (!pathsep) {
177             *jurl++ = '/';
178         }
179 
180         jurl = nxt_cpymem(jurl, *jar, nxt_strlen(*jar));
181         *jurl++ = '\0';
182     }
183 
184     return res;
185 }
186 
187 
188 static nxt_int_t
189 nxt_java_start(nxt_task_t *task, nxt_process_data_t *data)
190 {
191     jint                   rc;
192     char                   *opt, *real_path;
193     char                   **classpath_arr, **unit_jars, **system_jars;
194     JavaVM                 *jvm;
195     JNIEnv                 *env;
196     jobject                cl, classpath;
197     nxt_str_t              str;
198     nxt_int_t              opt_len, real_path_len;
199     nxt_uint_t             i, unit_jars_count, classpath_count;
200     nxt_uint_t             system_jars_count;
201     JavaVMOption           *jvm_opt;
202     JavaVMInitArgs         jvm_args;
203     nxt_unit_ctx_t         *ctx;
204     nxt_unit_init_t        java_init;
205     nxt_java_data_t        java_data;
206     nxt_conf_value_t       *value;
207     nxt_java_app_conf_t    *c;
208     nxt_common_app_conf_t  *app_conf;
209 
210     //setenv("ASAN_OPTIONS", "handle_segv=0", 1);
211 
212     jvm_args.version = JNI_VERSION_1_6;
213     jvm_args.nOptions = 0;
214     jvm_args.ignoreUnrecognized = 0;
215 
216     app_conf = data->app;
217     c = &app_conf->u.java;
218 
219     if (c->options != NULL) {
220         jvm_args.nOptions += nxt_conf_array_elements_count(c->options);
221     }
222 
223     jvm_opt = nxt_malloc(jvm_args.nOptions * sizeof(JavaVMOption));
224     if (jvm_opt == NULL) {
225         nxt_alert(task, "failed to allocate jvm_opt");
226         return NXT_ERROR;
227     }
228 
229     jvm_args.options = jvm_opt;
230 
231     unit_jars_count = nxt_nitems(nxt_java_unit_jars) - 1;
232 
233     unit_jars = nxt_java_module_jars(nxt_java_unit_jars, unit_jars_count);
234     if (unit_jars == NULL) {
235         nxt_alert(task, "failed to allocate buffer for unit_jars array");
236 
237         return NXT_ERROR;
238     }
239 
240     system_jars_count = nxt_nitems(nxt_java_system_jars) - 1;
241 
242     system_jars = nxt_java_module_jars(nxt_java_system_jars, system_jars_count);
243     if (system_jars == NULL) {
244         nxt_alert(task, "failed to allocate buffer for system_jars array");
245 
246         return NXT_ERROR;
247     }
248 
249     if (c->options != NULL) {
250 
251         for (i = 0; /* void */ ; i++) {
252             value = nxt_conf_get_array_element(c->options, i);
253             if (value == NULL) {
254                 break;
255             }
256 
257             nxt_conf_get_string(value, &str);
258 
259             opt = nxt_malloc(str.length + 1);
260             if (opt == NULL) {
261                 nxt_alert(task, "failed to allocate jvm_opt");
262                 return NXT_ERROR;
263             }
264 
265             memcpy(opt, str.start, str.length);
266             opt[str.length] = '\0';
267 
268             jvm_opt[i].optionString = opt;
269         }
270     }
271 
272     if (c->classpath != NULL) {
273         classpath_count = nxt_conf_array_elements_count(c->classpath);
274         classpath_arr = nxt_malloc(classpath_count * sizeof(char *));
275 
276         for (i = 0; /* void */ ; i++) {
277             value = nxt_conf_get_array_element(c->classpath, i);
278             if (value == NULL) {
279                 break;
280             }
281 
282             nxt_conf_get_string(value, &str);
283 
284             opt_len = str.length + 1;
285 
286             char *sc = memchr(str.start, ':', str.length);
287             if (sc == NULL && str.start[0] == '/') {
288                 opt_len += nxt_length("file:");
289             }
290 
291             opt = nxt_malloc(opt_len);
292             if (opt == NULL) {
293                 nxt_alert(task, "failed to allocate classpath");
294                 return NXT_ERROR;
295             }
296 
297             if (sc == NULL && str.start[0] != '/') {
298                 nxt_memcpy(opt, str.start, str.length);
299                 opt[str.length] = '\0';
300 
301                 real_path = realpath(opt, NULL);
302                 if (real_path == NULL) {
303                     nxt_alert(task, "realpath(%s) failed: %E", opt, nxt_errno);
304                     return NXT_ERROR;
305                 }
306 
307                 real_path_len = nxt_strlen(real_path);
308 
309                 free(opt);
310 
311                 opt_len = nxt_length("file:") + real_path_len + 1;
312 
313                 opt = nxt_malloc(opt_len);
314                 if (opt == NULL) {
315                     nxt_alert(task, "failed to allocate classpath");
316                     return NXT_ERROR;
317                 }
318 
319             } else {
320                 real_path = (char *) str.start;  /* I love this cast! */
321                 real_path_len = str.length;
322             }
323 
324             classpath_arr[i] = opt;
325 
326             if (sc == NULL) {
327                 opt = nxt_cpymem(opt, "file:", nxt_length("file:"));
328             }
329 
330             opt = nxt_cpymem(opt, real_path, real_path_len);
331             *opt = '\0';
332         }
333 
334     } else {
335         classpath_count = 0;
336         classpath_arr = NULL;
337     }
338 
339     rc = JNI_CreateJavaVM(&jvm, (void **) &env, &jvm_args);
340     if (rc != JNI_OK) {
341         nxt_alert(task, "failed to create Java VM: %d", (int) rc);
342         return NXT_ERROR;
343     }
344 
345     rc = nxt_java_initThread(env);
346     if (rc != NXT_UNIT_OK) {
347         nxt_alert(task, "nxt_java_initThread() failed");
348         goto env_failed;
349     }
350 
351     rc = nxt_java_initURLClassLoader(env);
352     if (rc != NXT_UNIT_OK) {
353         nxt_alert(task, "nxt_java_initURLClassLoader() failed");
354         goto env_failed;
355     }
356 
357     cl = nxt_java_newURLClassLoader(env, system_jars_count, system_jars);
358     if (cl == NULL) {
359         nxt_alert(task, "nxt_java_newURLClassLoader failed");
360         goto env_failed;
361     }
362 
363     nxt_java_setContextClassLoader(env, cl);
364 
365     cl = nxt_java_newURLClassLoader_parent(env, unit_jars_count, unit_jars, cl);
366     if (cl == NULL) {
367         nxt_alert(task, "nxt_java_newURLClassLoader_parent failed");
368         goto env_failed;
369     }
370 
371     nxt_java_setContextClassLoader(env, cl);
372 
373     rc = nxt_java_initContext(env, cl);
374     if (rc != NXT_UNIT_OK) {
375         nxt_alert(task, "nxt_java_initContext() failed");
376         goto env_failed;
377     }
378 
379     rc = nxt_java_initRequest(env, cl);
380     if (rc != NXT_UNIT_OK) {
381         nxt_alert(task, "nxt_java_initRequest() failed");
382         goto env_failed;
383     }
384 
385     rc = nxt_java_initResponse(env, cl);
386     if (rc != NXT_UNIT_OK) {
387         nxt_alert(task, "nxt_java_initResponse() failed");
388         goto env_failed;
389     }
390 
391     rc = nxt_java_initInputStream(env, cl);
392     if (rc != NXT_UNIT_OK) {
393         nxt_alert(task, "nxt_java_initInputStream() failed");
394         goto env_failed;
395     }
396 
397     rc = nxt_java_initOutputStream(env, cl);
398     if (rc != NXT_UNIT_OK) {
399         nxt_alert(task, "nxt_java_initOutputStream() failed");
400         goto env_failed;
401     }
402 
403     nxt_java_jni_init(env);
404     if (rc != NXT_UNIT_OK) {
405         nxt_alert(task, "nxt_java_jni_init() failed");
406         goto env_failed;
407     }
408 
409     classpath = nxt_java_newURLs(env, classpath_count, classpath_arr);
410     if (classpath == NULL) {
411         nxt_alert(task, "nxt_java_newURLs failed");
412         goto env_failed;
413     }
414 
415     java_data.jvm = jvm;
416     java_data.cl = cl;
417     java_data.ctx = nxt_java_startContext(env, c->webapp, classpath);
418     java_data.conf = c;
419 
420     if ((*env)->ExceptionCheck(env)) {
421         nxt_alert(task, "Unhandled exception in application start");
422         (*env)->ExceptionDescribe(env);
423         return NXT_ERROR;
424     }
425 
426     rc = nxt_java_init_threads(c);
427     if (nxt_slow_path(rc == NXT_UNIT_ERROR)) {
428         return NXT_ERROR;
429     }
430 
431     nxt_unit_default_init(task, &java_init);
432 
433     java_init.callbacks.request_handler = nxt_java_request_handler;
434     java_init.callbacks.websocket_handler = nxt_java_websocket_handler;
435     java_init.callbacks.close_handler = nxt_java_close_handler;
436     java_init.callbacks.ready_handler = nxt_java_ready_handler;
437     java_init.request_data_size = sizeof(nxt_java_request_data_t);
438     java_init.data = &java_data;
439     java_init.ctx_data = env;
440     java_init.shm_limit = app_conf->shm_limit;
441 
442     ctx = nxt_unit_init(&java_init);
443     if (nxt_slow_path(ctx == NULL)) {
444         nxt_alert(task, "nxt_unit_init() failed");
445         return NXT_ERROR;
446     }
447 
448     rc = nxt_unit_run(ctx);
449 
450     nxt_java_join_threads(ctx, c);
451 
452     nxt_java_stopContext(env, java_data.ctx);
453 
454     if ((*env)->ExceptionCheck(env)) {
455         (*env)->ExceptionDescribe(env);
456     }
457 
458     nxt_unit_done(ctx);
459 
460     (*jvm)->DestroyJavaVM(jvm);
461 
462     exit(rc);
463 
464     return NXT_OK;
465 
466 env_failed:
467 
468     if ((*env)->ExceptionCheck(env)) {
469         (*env)->ExceptionDescribe(env);
470     }
471 
472     return NXT_ERROR;
473 }
474 
475 
476 static void
477 nxt_java_request_handler(nxt_unit_request_info_t *req)
478 {
479     JNIEnv                   *env;
480     jobject                  jreq, jresp;
481     nxt_java_data_t          *java_data;
482     nxt_java_request_data_t  *data;
483 
484     java_data = req->unit->data;
485     env = req->ctx->data;
486     data = req->data;
487 
488     jreq = nxt_java_newRequest(env, java_data->ctx, req);
489     if (jreq == NULL) {
490         nxt_unit_req_alert(req, "failed to create Request instance");
491 
492         if ((*env)->ExceptionCheck(env)) {
493             (*env)->ExceptionDescribe(env);
494             (*env)->ExceptionClear(env);
495         }
496 
497         nxt_unit_request_done(req, NXT_UNIT_ERROR);
498         return;
499     }
500 
501     jresp = nxt_java_newResponse(env, req);
502     if (jresp == NULL) {
503         nxt_unit_req_alert(req, "failed to create Response instance");
504 
505         if ((*env)->ExceptionCheck(env)) {
506             (*env)->ExceptionDescribe(env);
507             (*env)->ExceptionClear(env);
508         }
509 
510         (*env)->DeleteLocalRef(env, jreq);
511 
512         nxt_unit_request_done(req, NXT_UNIT_ERROR);
513         return;
514     }
515 
516     data->header_size = 10 * 1024;
517     data->buf_size = 32 * 1024; /* from Jetty */
518     data->jreq = jreq;
519     data->jresp = jresp;
520     data->buf = NULL;
521 
522     nxt_unit_request_group_dup_fields(req);
523 
524     nxt_java_service(env, java_data->ctx, jreq, jresp);
525 
526     if ((*env)->ExceptionCheck(env)) {
527         (*env)->ExceptionDescribe(env);
528         (*env)->ExceptionClear(env);
529     }
530 
531     if (!nxt_unit_response_is_init(req)) {
532         nxt_unit_response_init(req, 200, 0, 0);
533     }
534 
535     if (!nxt_unit_response_is_sent(req)) {
536         nxt_unit_response_send(req);
537     }
538 
539     if (data->buf != NULL) {
540         nxt_unit_buf_send(data->buf);
541 
542         data->buf = NULL;
543     }
544 
545     if (nxt_unit_response_is_websocket(req)) {
546         data->jreq = (*env)->NewGlobalRef(env, jreq);
547         data->jresp = (*env)->NewGlobalRef(env, jresp);
548 
549     } else {
550         nxt_unit_request_done(req, NXT_UNIT_OK);
551     }
552 
553     (*env)->DeleteLocalRef(env, jresp);
554     (*env)->DeleteLocalRef(env, jreq);
555 }
556 
557 
558 static void
559 nxt_java_websocket_handler(nxt_unit_websocket_frame_t *ws)
560 {
561     void                     *b;
562     JNIEnv                   *env;
563     jobject                  jbuf;
564     nxt_java_request_data_t  *data;
565 
566     env = ws->req->ctx->data;
567     data = ws->req->data;
568 
569     b = malloc(ws->payload_len);
570     if (b != NULL) {
571         nxt_unit_websocket_read(ws, b, ws->payload_len);
572 
573         jbuf = (*env)->NewDirectByteBuffer(env, b, ws->payload_len);
574         if (jbuf != NULL) {
575             nxt_java_Request_websocket(env, data->jreq, jbuf,
576                                        ws->header->opcode, ws->header->fin);
577 
578             if ((*env)->ExceptionCheck(env)) {
579                 (*env)->ExceptionDescribe(env);
580                 (*env)->ExceptionClear(env);
581             }
582 
583             (*env)->DeleteLocalRef(env, jbuf);
584         }
585 
586         free(b);
587     }
588 
589     nxt_unit_websocket_done(ws);
590 }
591 
592 
593 static void
594 nxt_java_close_handler(nxt_unit_request_info_t *req)
595 {
596     JNIEnv                   *env;
597     nxt_java_request_data_t  *data;
598 
599     env = req->ctx->data;
600     data = req->data;
601 
602     nxt_java_Request_close(env, data->jreq);
603 
604     (*env)->DeleteGlobalRef(env, data->jresp);
605     (*env)->DeleteGlobalRef(env, data->jreq);
606 
607     nxt_unit_request_done(req, NXT_UNIT_OK);
608 }
609 
610 
611 static int
612 nxt_java_ready_handler(nxt_unit_ctx_t *ctx)
613 {
614     int                  res;
615     uint32_t             i;
616     nxt_java_data_t      *java_data;
617     nxt_java_app_conf_t  *c;
618 
619     /* Worker thread context. */
620     if (!nxt_unit_is_main_ctx(ctx)) {
621         return NXT_UNIT_OK;
622     }
623 
624     java_data = ctx->unit->data;
625     c = java_data->conf;
626 
627     if (c->threads <= 1) {
628         return NXT_UNIT_OK;
629     }
630 
631     for (i = 0; i < c->threads - 1; i++) {
632         res = pthread_create(&nxt_java_threads[i], nxt_java_thread_attr,
633                              nxt_java_thread_func, ctx);
634 
635         if (nxt_fast_path(res == 0)) {
636             nxt_unit_debug(ctx, "thread #%d created", (int) (i + 1));
637 
638         } else {
639             nxt_unit_alert(ctx, "thread #%d create failed: %s (%d)",
640                            (int) (i + 1), strerror(res), res);
641 
642             return NXT_UNIT_ERROR;
643         }
644     }
645 
646     return NXT_UNIT_OK;
647 }
648 
649 
650 static void *
651 nxt_java_thread_func(void *data)
652 {
653     int              rc;
654     JavaVM           *jvm;
655     JNIEnv           *env;
656     nxt_unit_ctx_t   *main_ctx, *ctx;
657     nxt_java_data_t  *java_data;
658 
659     main_ctx = data;
660 
661     nxt_unit_debug(main_ctx, "worker thread start");
662 
663     java_data = main_ctx->unit->data;
664     jvm = java_data->jvm;
665 
666     rc = (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL);
667     if (rc != JNI_OK) {
668         nxt_unit_alert(main_ctx, "failed to attach Java VM: %d", (int) rc);
669         return NULL;
670     }
671 
672     nxt_java_setContextClassLoader(env, java_data->cl);
673 
674     ctx = nxt_unit_ctx_alloc(main_ctx, env);
675     if (nxt_slow_path(ctx == NULL)) {
676         goto fail;
677     }
678 
679     (void) nxt_unit_run(ctx);
680 
681     nxt_unit_done(ctx);
682 
683 fail:
684 
685     (*jvm)->DetachCurrentThread(jvm);
686 
687     nxt_unit_debug(NULL, "worker thread end");
688 
689     return NULL;
690 }
691 
692 
693 static int
694 nxt_java_init_threads(nxt_java_app_conf_t *c)
695 {
696     int                    res;
697     static pthread_attr_t  attr;
698 
699     if (c->threads <= 1) {
700         return NXT_UNIT_OK;
701     }
702 
703     if (c->thread_stack_size > 0) {
704         res = pthread_attr_init(&attr);
705         if (nxt_slow_path(res != 0)) {
706             nxt_unit_alert(NULL, "thread attr init failed: %s (%d)",
707                            strerror(res), res);
708 
709             return NXT_UNIT_ERROR;
710         }
711 
712         res = pthread_attr_setstacksize(&attr, c->thread_stack_size);
713         if (nxt_slow_path(res != 0)) {
714             nxt_unit_alert(NULL, "thread attr set stack size failed: %s (%d)",
715                            strerror(res), res);
716 
717             return NXT_UNIT_ERROR;
718         }
719 
720         nxt_java_thread_attr = &attr;
721     }
722 
723     nxt_java_threads = nxt_unit_malloc(NULL,
724                                        sizeof(pthread_t) * (c->threads - 1));
725     if (nxt_slow_path(nxt_java_threads == NULL)) {
726         nxt_unit_alert(NULL, "Failed to allocate thread id array");
727 
728         return NXT_UNIT_ERROR;
729     }
730 
731     memset(nxt_java_threads, 0, sizeof(pthread_t) * (c->threads - 1));
732 
733     return NXT_UNIT_OK;
734 }
735 
736 
737 static void
738 nxt_java_join_threads(nxt_unit_ctx_t *ctx, nxt_java_app_conf_t *c)
739 {
740     int       res;
741     uint32_t  i;
742 
743     if (nxt_java_threads == NULL) {
744         return;
745     }
746 
747     for (i = 0; i < c->threads - 1; i++) {
748         if ((uintptr_t) nxt_java_threads[i] == 0) {
749             continue;
750         }
751 
752         res = pthread_join(nxt_java_threads[i], NULL);
753 
754         if (nxt_fast_path(res == 0)) {
755             nxt_unit_debug(ctx, "thread #%d joined", (int) i);
756 
757         } else {
758             nxt_unit_alert(ctx, "thread #%d join failed: %s (%d)",
759                            (int) i, strerror(res), res);
760         }
761     }
762 
763     nxt_unit_free(ctx, nxt_java_threads);
764 }
765 
766 
767