diff --git a/examples/examples.h b/examples/examples.h index 83670b5f3..e92e65eb9 100644 --- a/examples/examples.h +++ b/examples/examples.h @@ -1,4 +1,4 @@ -// Copyright 2015-2018 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -15,6 +15,7 @@ #define EXAMPLES_H_ #include +#include #include #include diff --git a/examples/micro-arithmetics.c b/examples/micro-arithmetics.c new file mode 100644 index 000000000..9e2eb53f1 --- /dev/null +++ b/examples/micro-arithmetics.c @@ -0,0 +1,147 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "examples.h" + +// Sequence NATS microservice example. +// +// This example illustrates multiple NATS microservices communicating with each +// other. Please see the main microservice, micro-sequence.c for a more detailed +// explanation. +// +// This specific microservice implements add, multiply, and divide operations. + +// arithmeticsOp is a type for a C function that implements am operation: add, +// multiply, divide. +typedef void (*arithmeticsOP)(long double *result, long double a1, long double a2); + +// handle_arithmetics_op is a helper function that wraps an implementation of an +// operation into a request handler. +static microError * +handle_arithmetics_op(microRequest *req, arithmeticsOP op) +{ + microError *err = NULL; + microArgs *args = NULL; + long double a1, a2, result; + char buf[1024]; + int len = 0; + + err = micro_ParseArgs(&args, microRequest_GetData(req), microRequest_GetDataLength(req)); + if ((err == NULL) && (microArgs_Count(args) != 2)) + { + err = micro_Errorf("invalid number of arguments, expected 2 got %d", microArgs_Count(args)); + } + if (err == NULL) + err = microArgs_GetFloat(&a1, args, 0); + if (err == NULL) + err = microArgs_GetFloat(&a2, args, 1); + if (err == NULL) + op(&result, a1, a2); + if (err == NULL) + len = snprintf(buf, sizeof(buf), "%Lf", result); + if (err == NULL) + err = microRequest_Respond(req, buf, len); + + microArgs_Destroy(args); + return microError_Wrapf(err, "failed to handle arithmetics operation"); +} + +static void add(long double *result, long double a1, long double a2) +{ + *result = a1 + a2; +} + +static void divide(long double *result, long double a1, long double a2) +{ + *result = a1 / a2; +} + +static void multiply(long double *result, long double a1, long double a2) +{ + *result = a1 * a2; +} + +// request handlers for each operation. +static microError *handle_add(microRequest *req) { return handle_arithmetics_op(req, add); } +static microError *handle_divide(microRequest *req) { return handle_arithmetics_op(req, divide); } +static microError *handle_multiply(microRequest *req) { return handle_arithmetics_op(req, multiply); } + +// main is the main entry point for the microservice. +int main(int argc, char **argv) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + natsConnection *conn = NULL; + natsOptions *opts = NULL; + microService *m = NULL; + microGroup *g = NULL; + char errorbuf[1024]; + + microServiceConfig cfg = { + .Description = "Arithmetic operations - NATS microservice example in C", + .Name = "c-arithmetics", + .Version = "1.0.0", + }; + microEndpointConfig add_cfg = { + .Name = "add", + .Handler = handle_add, + }; + microEndpointConfig divide_cfg = { + .Name = "divide", + .Handler = handle_divide, + }; + microEndpointConfig multiply_cfg = { + .Name = "multiply", + .Handler = handle_multiply, + }; + + // Connect to NATS server + opts = parseArgs(argc, argv, ""); + s = natsConnection_Connect(&conn, opts); + if (s != NATS_OK) + { + printf("Error: %u - %s\n", s, natsStatus_GetText(s)); + nats_PrintLastErrorStack(stderr); + natsOptions_Destroy(opts); + return 1; + } + + // Create the Microservice that listens on nc. + err = micro_AddService(&m, conn, &cfg); + + // Add the endpoints for the functions. + if (err == NULL) + microService_AddGroup(&g, m, "op"); + if (err == NULL) + err = microGroup_AddEndpoint(g, &add_cfg); + if (err == NULL) + err = microGroup_AddEndpoint(g, &multiply_cfg); + if (err == NULL) + err = microGroup_AddEndpoint(g, ÷_cfg); + + // Run the service, until stopped. + if (err == NULL) + err = microService_Run(m); + + // Cleanup. + microService_Destroy(m); + natsOptions_Destroy(opts); + natsConnection_Destroy(conn); + if (err != NULL) + { + printf("Error: %s\n", microError_String(err, errorbuf, sizeof(errorbuf))); + microError_Destroy(err); + return 1; + } + return 0; +} diff --git a/examples/micro-func.c b/examples/micro-func.c new file mode 100644 index 000000000..0f62cb85a --- /dev/null +++ b/examples/micro-func.c @@ -0,0 +1,229 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "examples.h" + +// Sequence NATS microservice example. +// +// This example illustrates multiple NATS microservices communicating with each +// other. Please see the main microservice, micro-sequence.c for a more detailed +// explanation. +// +// This specific microservice implements factorial, fibonacci, and power2 +// functions. Instead of performing arithmetic operations locally, we call the +// arithmetics microservice to perform the operations. + +// functionHandler is a type for a C function that implements a "function", i.e. +// power2, factorial, etc. +typedef microError *(*functionHandler)(long double *result, natsConnection *conn, int n); + +// callArithmetics is a helper function that calls the arithmetics microservice. +static microError * +call_arithmetics(long double *result, natsConnection *nc, const char *subject, long double a1, long double a2) +{ + microError *err = NULL; + microClient *client = NULL; + natsMsg *response = NULL; + microArgs *args = NULL; + char buf[1024]; + int len; + + err = micro_NewClient(&client, nc, NULL); + if (err == NULL) + len = snprintf(buf, sizeof(buf), "%Lf %Lf", a1, a2); + if (err == NULL) + err = microClient_DoRequest(&response, client, subject, buf, len); + if (err == NULL) + err = micro_ParseArgs(&args, natsMsg_GetData(response), natsMsg_GetDataLength(response)); + if (err == NULL) + err = microArgs_GetFloat(result, args, 0); + + microClient_Destroy(client); + natsMsg_Destroy(response); + return err; +} + +// factorial implements the factorial(N) function. Calls the arithmetics service +// for all multiplications. +static microError * +factorial(long double *result, natsConnection *nc, int n) +{ + microError *err = NULL; + int i; + + if (n < 1) + return micro_Errorf("n=%d. must be greater than 0", n); + + *result = 1; + for (i = 1; i <= n; i++) + { + err = call_arithmetics(result, nc, "op.multiply", *result, i); + if (err != NULL) + return err; + } + return NULL; +} + +// fibonacci implements the fibonacci(N) function. Calls the arithmetics service +// for all additions. +static microError * +fibonacci(long double *result, natsConnection *nc, int n) +{ + microError *err = NULL; + int i; + long double n1, n2; + + if (n < 0) + return micro_Errorf("n=%d. must be non-negative", n); + + if (n < 2) + { + *result = n; + return NULL; + } + + for (i = 1, n1 = 0, n2 = 1; i <= n; i++) + { + err = call_arithmetics(result, nc, "op.add", n1, n2); + if (err != NULL) + return err; + n1 = n2; + n2 = *result; + } + return NULL; +} + +// power2 implements the 2**N function. Calls the arithmetics service +// for all multiplications. +static microError *power2(long double *result, natsConnection *nc, int n) +{ + microError *err = NULL; + int i; + + if (n < 1) + return micro_Errorf("n=%d. must be greater than 0", n); + + *result = 1; + for (i = 1; i <= n; i++) + { + err = call_arithmetics(result, nc, "op.multiply", *result, 2); + if (err != NULL) + return err; + } + return NULL; +} + +// handle_function_op is a helper function that wraps an implementation function +// like factorial, fibonacci, etc. into a request handler. +static microError * +handle_function_op(microRequest *req, functionHandler op) +{ + microError *err = NULL; + microArgs *args = NULL; + int n; + long double result; + char buf[1024]; + int len = 0; + + err = micro_ParseArgs(&args, microRequest_GetData(req), microRequest_GetDataLength(req)); + if ((err == NULL) && (microArgs_Count(args) != 1)) + { + err = micro_Errorf("Invalid number of arguments, expected 1 got %d", microArgs_Count(args)); + } + if (err == NULL) + err = microArgs_GetInt(&n, args, 0); + if (err == NULL) + err = op(&result, microRequest_GetConnection(req), n); + if (err == NULL) + len = snprintf(buf, sizeof(buf), "%Lf", result); + if (err == NULL) + err = microRequest_Respond(req, buf, len); + + microArgs_Destroy(args); + return err; +} + +// handle_... are the request handlers for each function. +static microError *handle_factorial(microRequest *req) { return handle_function_op(req, factorial); } +static microError *handle_fibonacci(microRequest *req) { return handle_function_op(req, fibonacci); } +static microError *handle_power2(microRequest *req) { return handle_function_op(req, power2); } + +// main is the main entry point for the microservice. +int main(int argc, char **argv) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + natsOptions *opts = NULL; + natsConnection *conn = NULL; + microService *m = NULL; + microGroup *g = NULL; + char errorbuf[1024]; + + microServiceConfig cfg = { + .Description = "Functions - NATS microservice example in C", + .Name = "c-functions", + .Version = "1.0.0", + }; + microEndpointConfig factorial_cfg = { + .Name = "factorial", + .Handler = handle_factorial, + }; + microEndpointConfig fibonacci_cfg = { + .Name = "fibonacci", + .Handler = handle_fibonacci, + }; + microEndpointConfig power2_cfg = { + .Name = "power2", + .Handler = handle_power2, + }; + + // Connect to NATS server + opts = parseArgs(argc, argv, ""); + s = natsConnection_Connect(&conn, opts); + if (s != NATS_OK) + { + printf("Error: %u - %s\n", s, natsStatus_GetText(s)); + nats_PrintLastErrorStack(stderr); + natsOptions_Destroy(opts); + return 1; + } + + // Create the Microservice that listens on nc. + err = micro_AddService(&m, conn, &cfg); + + // Add the endpoints for the functions. + if (err == NULL) + err = microService_AddGroup(&g, m, "f"); + if (err == NULL) + err = microGroup_AddEndpoint(g, &factorial_cfg); + if (err == NULL) + err = microGroup_AddEndpoint(g, &fibonacci_cfg); + if (err == NULL) + err = microGroup_AddEndpoint(g, &power2_cfg); + + // Run the service, until stopped. + if (err == NULL) + err = microService_Run(m); + + // Cleanup. + microService_Destroy(m); + natsOptions_Destroy(opts); + natsConnection_Destroy(conn); + if (err != NULL) + { + printf("Error: %s\n", microError_String(err, errorbuf, sizeof(errorbuf))); + microError_Destroy(err); + return 1; + } + return 0; +} diff --git a/examples/micro-hello.c b/examples/micro-hello.c new file mode 100644 index 000000000..6eedbbf70 --- /dev/null +++ b/examples/micro-hello.c @@ -0,0 +1,93 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "examples.h" + +// Hello World! NATS microservice example. +// +// Requires NATS server and CLI, and the nats.c examples fully built. See +// https://github.com/nats-io/nats.c#building +// +// RUN: +// ```sh +// $NATS_SERVER & # NATS_SERVER points to the NATS server binary +// nats_pid=$! +// sleep 2 # wait for server to start +// ./examples/nats-micro-hello & +// hello_pid=$! +// sleep 2 # wait for microservice to start +// nats request 'hello' '' +// kill $hello_pid $nats_pid +// ``` +// +// OUTPUT: +// ``` +// 06:34:57 Sending request on "hello" +// 06:34:57 Received with rtt 1.08ms +// Hello, World! +// ``` + +#define HELLO "Hello, World!" + +static microError * +handle(microRequest *req) +{ + return microRequest_Respond(req, HELLO, sizeof(HELLO)); +} + +int main(int argc, char **argv) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + natsConnection *conn = NULL; + natsOptions *opts = NULL; + microService *m = NULL; + char errorbuf[1024]; + + microEndpointConfig hello_cfg = { + .Name = "hello", + .Handler = handle, + }; + microServiceConfig cfg = { + .Description = "Hello World! - NATS microservice example in C", + .Name = "c-hello", + .Version = "1.0.0", + .Endpoint = &hello_cfg, + }; + + // Connect and start the services + opts = parseArgs(argc, argv, ""); + s = natsConnection_Connect(&conn, opts); + if (s != NATS_OK) + { + printf("Error: %u - %s\n", s, natsStatus_GetText(s)); + nats_PrintLastErrorStack(stderr); + natsOptions_Destroy(opts); + return 1; + } + + err = micro_AddService(&m, conn, &cfg); + if (err == NULL) + err = microService_Run(m); + + microService_Destroy(m); + natsOptions_Destroy(opts); + natsConnection_Destroy(conn); + if (err != NULL) + { + printf("Error: %s\n", microError_String(err, errorbuf, sizeof(errorbuf))); + microError_Destroy(err); + return 1; + } + return 0; +} diff --git a/examples/micro-sequence.c b/examples/micro-sequence.c new file mode 100644 index 000000000..3655f32ea --- /dev/null +++ b/examples/micro-sequence.c @@ -0,0 +1,191 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "examples.h" + +// Sequence NATS microservice example. +// +// This example illustrates multiple NATS microservices communicating with each +// other. +// +// The main service (c-sequence) calculates the sum of 1/f(1) + 1/f(2)... up to +// N (included). It exposes one (default) endpoint, "sequence". The inputs are +// f (the function name) and N. name, can be "factorial", "fibonacci", or +// "power2"). +// +// c-sequence parses the request, then calculates the sequence by calling the +// c-functions microservice to calculate f(1), f(2), etc. The c-functions +// service in turn uses c-arithmetics microservice for all arithmetic +// operations. +// +// Requires NATS server and CLI, and the nats.c examples fully built. See +// https://github.com/nats-io/nats.c#building +// +// RUN: +// ```sh +// $NATS_SERVER & # NATS_SERVER points to the NATS server binary +// nats_pid=$! +// sleep 2 # wait for server to start +// ./examples/nats-micro-sequence & +// sequence_pid=$! +// ./examples/nats-micro-func & +// func_pid=$! +// ./examples/nats-micro-arithmetics & +// arithmetics_pid=$! +// sleep 2 # wait for microservice to start +// nats request -r 'sequence' '"factorial" 10' +// nats request -r 'sequence' '"power2" 10' +// nats request -r 'sequence' '"fibonacci" 10' +// kill $sequence_pid $func_pid $arithmetics_pid $nats_pid +// ``` +// +// OUTPUT: +// ``` +// 2.718282 +// 1.999023 +// 3.341705 +// ``` + +static microError * +call_function(long double *result, natsConnection *nc, const char *subject, int n) +{ + microError *err = NULL; + microClient *client = NULL; + natsMsg *response = NULL; + microArgs *args = NULL; + char buf[256]; + char sbuf[256]; + int len; + + len = snprintf(buf, sizeof(buf), "%d", n); + snprintf(sbuf, sizeof(sbuf), "f.%s", subject); + err = micro_NewClient(&client, nc, NULL); + if (err == NULL) + err = microClient_DoRequest(&response, client, sbuf, buf, len); + if (err == NULL) + err = micro_ParseArgs(&args, natsMsg_GetData(response), natsMsg_GetDataLength(response)); + if (err == NULL) + err = microArgs_GetFloat(result, args, 0); + + microClient_Destroy(client); + natsMsg_Destroy(response); + return err; +} + +// calculates the sum of X/f(1) + X/f(2)... up to N (included). The inputs are X +// (float), f name (string), and N (int). E.g.: '10.0 "power2" 10' will +// calculate 10/2 + 10/4 + 10/8 + 10/16 + 10/32 + 10/64 + 10/128 + 10/256 + +// 10/512 + 10/1024 = 20.998046875 +static microError *handle_sequence(microRequest *req) +{ + microError *err = NULL; + natsConnection *nc = microRequest_GetConnection(req); + microArgs *args = NULL; + int n = 0; + int i; + const char *function; + long double initialValue = 1.0; + long double value = 1.0; + long double denominator = 0; + char result[64]; + int result_len = 0; + + err = micro_ParseArgs(&args, microRequest_GetData(req), microRequest_GetDataLength(req)); + if ((err == NULL) && (microArgs_Count(args) != 2)) + { + err = micro_Errorf("Invalid number of arguments, expected 2 got %d", microArgs_Count(args)); + } + + if (err == NULL) + err = microArgs_GetString(&function, args, 0); + if ((err == NULL) && + (strcmp(function, "factorial") != 0) && + (strcmp(function, "power2") != 0) && + (strcmp(function, "fibonacci") != 0)) + { + err = micro_Errorf("Invalid function name '%s', must be 'factorial', 'power2', or 'fibonacci'", function); + } + if (err == NULL) + err = microArgs_GetInt(&n, args, 1); + if ((err == NULL) && (n < 1)) + { + err = micro_Errorf("Invalid number of iterations %d, must be at least 1", n); + } + + for (i = 1; (err == NULL) && (i <= n); i++) + { + if (err == NULL) + err = call_function(&denominator, nc, function, i); + if ((err == NULL) && (denominator == 0)) + { + err = micro_Errorf("division by zero at step %d", i); + } + if (err == NULL) + value = value + initialValue / denominator; + } + + if (err == NULL) + result_len = snprintf(result, sizeof(result), "%Lf", value); + if (err == NULL) + err = microRequest_Respond(req, result, result_len); + + microArgs_Destroy(args); + return err; +} + +int main(int argc, char **argv) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + natsConnection *conn = NULL; + natsOptions *opts = NULL; + microService *m = NULL; + char errorbuf[1024]; + + microEndpointConfig sequence_cfg = { + .Subject = "sequence", + .Name = "sequence-service", + .Handler = handle_sequence, + }; + microServiceConfig cfg = { + .Description = "Sequence adder - NATS microservice example in C", + .Name = "c-sequence", + .Version = "1.0.0", + .Endpoint = &sequence_cfg, + }; + + opts = parseArgs(argc, argv, ""); + s = natsConnection_Connect(&conn, opts); + if (s != NATS_OK) + { + printf("Error: %u - %s\n", s, natsStatus_GetText(s)); + nats_PrintLastErrorStack(stderr); + natsOptions_Destroy(opts); + return 1; + } + + err = micro_AddService(&m, conn, &cfg); + if (err == NULL) + err = microService_Run(m); + + microService_Destroy(m); + natsOptions_Destroy(opts); + natsConnection_Destroy(conn); + if (err != NULL) + { + printf("Error: %s\n", microError_String(err, errorbuf, sizeof(errorbuf))); + microError_Destroy(err); + return 1; + } + return 0; +} diff --git a/examples/micro-stats.c b/examples/micro-stats.c new file mode 100644 index 000000000..8a5b27203 --- /dev/null +++ b/examples/micro-stats.c @@ -0,0 +1,152 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "examples.h" + +typedef struct service_state_s +{ + // in a real application this should be protected by a mutex. In this + // example, the main control flow provides synchronization. + int odd_count; +} service_state_t; + +static microError * +handle_default(microRequest *req) +{ + char buf[64]; + const char *response = "odd"; + int n; + service_state_t *state = microRequest_GetServiceState(req); + + snprintf(buf, sizeof(buf), "%.*s", microRequest_GetDataLength(req), microRequest_GetData(req)); + n = atoi(buf); + if (n % 2 != 0) + { + // this should be protected by a mutex in a real application. + state->odd_count++; + + response = "even"; + } + return microRequest_Respond(req, response, strlen(response)); +} + +static microError * +handle_stats(microRequest *req) +{ + microError *err = NULL; + microServiceStats *stats = NULL; + char buf[2048]; + service_state_t *service_state = microRequest_GetServiceState(req); + int total, custom, len; + + err = microService_GetStats(&stats, microRequest_GetService(req)); + if (err != NULL) + return err; + + total = (int) stats->Endpoints[0].NumRequests; + custom = service_state->odd_count; + len = snprintf(buf, sizeof(buf), + "{\"total\":%d,\"odd\":%d}", total, custom); + return microRequest_Respond(req, buf, len); +} + +static microError * +run_example(natsConnection *conn, microRequestHandler stats_handler, char *buf, int buf_cap) +{ + microError *err = NULL; + microService *m = NULL; + microClient *c = NULL; + service_state_t service_state = { + .odd_count = 0, + }; + microEndpointConfig default_cfg = { + .Name = "default", + .Handler = handle_default, + }; + microServiceConfig cfg = { + .Name = "c-stats", + .Description = "NATS microservice in C with a custom stats handler", + .Version = "1.0.0", + .Endpoint = &default_cfg, + .StatsHandler = stats_handler, + .State = &service_state, + }; + int i; + int len; + natsMsg *resp = NULL; + natsMsg *stats_resp = NULL; + + err = micro_AddService(&m, conn, &cfg); + if (err == NULL) + err = micro_NewClient(&c, conn, NULL); + for (i = 0; (err == NULL) && (i < 10); i++) + { + len = snprintf(buf, buf_cap, "%d", i); + if (err == NULL) + err = microClient_DoRequest(&resp, c, "default", buf, len); + if (err == NULL) + natsMsg_Destroy(resp); + } + + if (err == NULL) + err = microClient_DoRequest(&stats_resp, c, "$SRV.STATS.c-stats", "", 0); + if (err == NULL) + { + len = natsMsg_GetDataLength(stats_resp); + if (len > buf_cap - 1) + { + len = buf_cap - 1; + } + memcpy(buf, natsMsg_GetData(stats_resp), len); + buf[len] = '\0'; + + natsMsg_Destroy(stats_resp); + } + + microService_Destroy(m); + microClient_Destroy(c); + return err; +} + +int main(int argc, char **argv) +{ + microError *err = NULL; + natsOptions *opts = parseArgs(argc, argv, ""); + natsConnection *conn = NULL; + char buf[2048]; + + err = micro_ErrorFromStatus( + natsConnection_Connect(&conn, opts)); + if (err == NULL) + err = run_example(conn, NULL, buf, sizeof(buf)); + if (err == NULL) + printf("Default stats response:\n----\n%s\n----\n\n", buf); + + if (err == NULL) + err = run_example(conn, handle_stats, buf, sizeof(buf)); + if (err == NULL) + printf("Custom stats response:\n----\n%s\n----\n\n", buf); + + if (err != NULL) + { + fprintf(stderr, "Error: %s\n", microError_String(err, buf, sizeof(buf))); + } + + natsOptions_Destroy(opts); + natsConnection_Destroy(conn); + microError_Destroy(err); + return err == NULL ? 0 : 1; +} diff --git a/src/conn.c b/src/conn.c index 726e2d962..fbd67f3a1 100644 --- a/src/conn.c +++ b/src/conn.c @@ -1,4 +1,4 @@ -// Copyright 2015-2021 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -526,10 +526,17 @@ _processInfo(natsConnection *nc, char *info, int len) { natsStatus s = NATS_OK; nats_JSON *json = NULL; + bool postDiscoveredServersCb = false; + bool postLameDuckCb = false; if (info == NULL) return NATS_OK; + natsOptions_lock(nc->opts); + postDiscoveredServersCb = (nc->opts->discoveredServersCb != NULL); + postLameDuckCb = (nc->opts->lameDuckCb != NULL); + natsOptions_unlock(nc->opts); + _clearServerInfo(&(nc->info)); s = nats_JSONParse(&json, info, len); @@ -574,13 +581,13 @@ _processInfo(natsConnection *nc, char *info, int len) nc->info.connectURLsCount, tlsName, &added); - if ((s == NATS_OK) && added && !nc->initc && (nc->opts->discoveredServersCb != NULL)) + if ((s == NATS_OK) && added && !nc->initc && postDiscoveredServersCb) natsAsyncCb_PostConnHandler(nc, ASYNC_DISCOVERED_SERVERS); } // Process the LDM callback after the above. It will cover cases where // we have connect URLs and invoke discovered server callback, and case // where we don't. - if ((s == NATS_OK) && nc->info.lameDuckMode && (nc->opts->lameDuckCb != NULL)) + if ((s == NATS_OK) && nc->info.lameDuckMode && postLameDuckCb) natsAsyncCb_PostConnHandler(nc, ASYNC_LAME_DUCK_MODE); if (s != NATS_OK) @@ -1523,6 +1530,15 @@ _doReconnect(void *arg) int i = 0; natsCustomReconnectDelayHandler crd = NULL; void *crdClosure = NULL; + bool postDisconnectedCb = false; + bool postReconnectedCb = false; + bool postConnectedCb = false; + + natsOptions_lock(nc->opts); + postDisconnectedCb = (nc->opts->disconnectedCb != NULL); + postReconnectedCb = (nc->opts->reconnectedCb != NULL); + postConnectedCb = (nc->opts->connectedCb != NULL); + natsOptions_unlock(nc->opts); natsConn_Lock(nc); @@ -1542,7 +1558,7 @@ _doReconnect(void *arg) // Perform appropriate callback if needed for a disconnect. // (do not do this if we are here on initial connect failure) - if (!nc->initc && (nc->opts->disconnectedCb != NULL)) + if (!nc->initc && postDisconnectedCb) natsAsyncCb_PostConnHandler(nc, ASYNC_DISCONNECTED); crd = nc->opts->customReconnectDelayCB; @@ -1705,7 +1721,7 @@ _doReconnect(void *arg) // This was the initial connect. Set this to false. nc->initc = false; // Invoke the callback. - if (nc->opts->connectedCb != NULL) + if (postConnectedCb) natsAsyncCb_PostConnHandler(nc, ASYNC_CONNECTED); } else @@ -1713,7 +1729,7 @@ _doReconnect(void *arg) // Call reconnectedCB if appropriate. Since we are in a separate // thread, we could invoke the callback directly, however, we // still post it so all callbacks from a connection are serialized. - if (nc->opts->reconnectedCb != NULL) + if (postReconnectedCb) natsAsyncCb_PostConnHandler(nc, ASYNC_RECONNECTED); } @@ -1997,13 +2013,20 @@ _connect(natsConnection *nc) int max = 0; int64_t wtime = 0; bool retry = false; + bool retryOnFailedConnect = false; + bool hasConnectedCb = false; + + natsOptions_lock(nc->opts); + hasConnectedCb = (nc->opts->connectedCb != NULL); + retryOnFailedConnect = nc->opts->retryOnFailedConnect; + natsOptions_unlock(nc->opts); natsConn_Lock(nc); nc->initc = true; pool = nc->srvPool; - if ((nc->opts->retryOnFailedConnect) && (nc->opts->connectedCb == NULL)) + if ((retryOnFailedConnect) && !hasConnectedCb) { retry = true; max = nc->opts->maxReconnect; @@ -2056,6 +2079,7 @@ _connect(natsConnection *nc) retSts = NATS_OK; } } + if (!retry) break; @@ -2069,8 +2093,8 @@ _connect(natsConnection *nc) // If not connected and retry asynchronously on failed connect if ((nc->status != NATS_CONN_STATUS_CONNECTED) - && nc->opts->retryOnFailedConnect - && (nc->opts->connectedCb != NULL)) + && retryOnFailedConnect + && hasConnectedCb) { natsConn_Unlock(nc); @@ -2471,8 +2495,15 @@ _close(natsConnection *nc, natsConnStatus status, bool fromPublicClose, bool doC struct threadsToJoin ttj; bool sockWasActive = false; bool detach = false; + bool postClosedCb = false; + bool postDisconnectedCb = false; natsSubscription *sub = NULL; + natsOptions_lock(nc->opts); + postClosedCb = (nc->opts->closedCb != NULL); + postDisconnectedCb = (nc->opts->disconnectedCb != NULL); + natsOptions_unlock(nc->opts); + natsConn_lockAndRetain(nc); // If invoked from the public Close() call, attempt to flush @@ -2546,7 +2577,7 @@ _close(natsConnection *nc, natsConnStatus status, bool fromPublicClose, bool doC // Perform appropriate callback if needed for a disconnect. // Do not invoke if we were disconnected and failed to reconnect (since // it has already been invoked in doReconnect). - if (doCBs && !nc->rle && (nc->opts->disconnectedCb != NULL) && sockWasActive) + if (doCBs && !nc->rle && postDisconnectedCb && sockWasActive) natsAsyncCb_PostConnHandler(nc, ASYNC_DISCONNECTED); sub = nc->respMux; @@ -2562,7 +2593,7 @@ _close(natsConnection *nc, natsConnStatus status, bool fromPublicClose, bool doC natsConn_Lock(nc); // Perform appropriate callback if needed for a connection closed. - if (doCBs && (nc->opts->closedCb != NULL)) + if (doCBs && postClosedCb) natsAsyncCb_PostConnHandler(nc, ASYNC_CLOSED); nc->status = status; diff --git a/src/crypto.c b/src/crypto.c index 23c205c35..fdebf07e9 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -1,4 +1,4 @@ -// Copyright 2019 The NATS Authors +// Copyright 2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/src/include/n-unix.h b/src/include/n-unix.h index 3b775cd42..ac4919a25 100644 --- a/src/include/n-unix.h +++ b/src/include/n-unix.h @@ -64,4 +64,6 @@ typedef size_t natsRecvLen; #define nats_vsnprintf vsnprintf #define nats_strtok strtok_r +#define nats_vscprintf(f, a) vsnprintf(NULL, 0, (f), (a)) + #endif /* N_UNIX_H_ */ diff --git a/src/include/n-win.h b/src/include/n-win.h index 328756e79..e84d65170 100644 --- a/src/include/n-win.h +++ b/src/include/n-win.h @@ -63,6 +63,8 @@ typedef int natsRecvLen; #define nats_vsnprintf(b, sb, f, a) vsnprintf_s((b), (sb), (_TRUNCATE), (f), (a)) +#define nats_vscprintf _vscprintf + int nats_asprintf(char **newStr, const char *fmt, ...); diff --git a/src/micro.c b/src/micro.c new file mode 100644 index 000000000..0ee378536 --- /dev/null +++ b/src/micro.c @@ -0,0 +1,953 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "microp.h" +#include "conn.h" +#include "opts.h" + +static inline void _lock_service(microService *m) { natsMutex_Lock(m->service_mu); } +static inline void _unlock_service(microService *m) { natsMutex_Unlock(m->service_mu); } + +static microError *_clone_service_config(microServiceConfig **out, microServiceConfig *cfg); +static microError *_new_service(microService **ptr, natsConnection *nc); +static microError *_wrap_connection_event_callbacks(microService *m); + +static void _free_cloned_service_config(microServiceConfig *cfg); +static void _free_service(microService *m); +static void _release_service(microService *m); +static void _retain_service(microService *m); + +microError * +micro_AddService(microService **new_m, natsConnection *nc, microServiceConfig *cfg) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + microService *m = NULL; + + if ((new_m == NULL) || (nc == NULL) || (cfg == NULL) || !micro_is_valid_name(cfg->Name) || nats_IsStringEmpty(cfg->Version)) + return micro_ErrorInvalidArg; + + // Make a microservice object, with a reference to a natsConnection. + err = _new_service(&m, nc); + if (err != NULL) + return err; + + IFOK(s, natsMutex_Create(&m->service_mu)); + IFOK(s, natsNUID_Next(m->id, sizeof(m->id))); + err = micro_ErrorFromStatus(s); + + MICRO_CALL(err, _clone_service_config(&m->cfg, cfg)); + + // Wrap the connection callbacks before we subscribe to anything. + MICRO_CALL(err, _wrap_connection_event_callbacks(m)); + + MICRO_CALL(err, micro_init_monitoring(m)); + MICRO_CALL(err, microService_AddEndpoint(m, cfg->Endpoint)); + + if (err != NULL) + { + microError_Ignore(microService_Destroy(m)); + return microError_Wrapf(err, "failed to add microservice %s", cfg->Name); + } + + *new_m = m; + return NULL; +} + +microError * +micro_add_endpoint(microEndpoint **new_ep, microService *m, const char *prefix, microEndpointConfig *cfg, bool is_internal) +{ + microError *err = NULL; + microEndpoint *ptr = NULL; + microEndpoint *prev_ptr = NULL; + microEndpoint *ep = NULL; + microEndpoint *prev_ep = NULL; + + if (m == NULL) + return micro_ErrorInvalidArg; + if (cfg == NULL) + return NULL; + + err = micro_new_endpoint(&ep, m, prefix, cfg, is_internal); + if (err != NULL) + return microError_Wrapf(err, "failed to create endpoint %s", cfg->Name); + + _lock_service(m); + + if (m->stopped) + { + _unlock_service(m); + return micro_Errorf("can't add an endpoint %s to service %s: the service is stopped", cfg->Name, m->cfg->Name); + } + + if (m->first_ep != NULL) + { + if (strcmp(m->first_ep->name, ep->name) == 0) + { + ep->next = m->first_ep->next; + prev_ep = m->first_ep; + m->first_ep = ep; + } + else + { + prev_ptr = m->first_ep; + for (ptr = m->first_ep->next; ptr != NULL; prev_ptr = ptr, ptr = ptr->next) + { + if (strcmp(ptr->name, ep->name) == 0) + { + ep->next = ptr->next; + prev_ptr->next = ep; + prev_ep = ptr; + break; + } + } + if (prev_ep == NULL) + { + prev_ptr->next = ep; + } + } + } + else + { + m->first_ep = ep; + } + + _unlock_service(m); + + if (prev_ep != NULL) + { + // Rid of the previous endpoint with the same name, if any. If this + // fails we can return the error, leave the newly added endpoint in the + // list, not started. A retry with the same name will clean it up. + if (err = micro_stop_endpoint(prev_ep), err != NULL) + return err; + micro_release_endpoint(prev_ep); + } + + // retain `m` before the endpoint uses it for its on_complete callback. + _retain_service(m); + + if (err = micro_start_endpoint(ep), err != NULL) + { + // Best effort, leave the new endpoint in the list, as is. A retry with + // the same name will clean it up. + _release_service(m); + return microError_Wrapf(err, "failed to start endpoint %s", ep->name); + } + + if (new_ep != NULL) + *new_ep = ep; + return NULL; +} + +microError * +microService_AddEndpoint(microService *m, microEndpointConfig *cfg) +{ + return micro_add_endpoint(NULL, m, NULL, cfg, false); +} + +microError * +microGroup_AddEndpoint(microGroup *g, microEndpointConfig *cfg) +{ + if (g == NULL) + return micro_ErrorInvalidArg; + + return micro_add_endpoint(NULL, g->m, g->prefix, cfg, false); +} + +microError * +microService_Stop(microService *m) +{ + microError *err = NULL; + microEndpoint *ep = NULL; + bool finalize = false; + microDoneHandler doneHandler = NULL; + + if (m == NULL) + return micro_ErrorInvalidArg; + + _lock_service(m); + + if (m->stopped) + { + _unlock_service(m); + return NULL; + } + ep = m->first_ep; + + for (; ep != NULL; ep = ep->next) + { + if (err = micro_stop_endpoint(ep), err != NULL) + { + _unlock_service(m); + return microError_Wrapf(err, "failed to stop service '%s', stopping endpoint '%s'", m->cfg->Name, ep->name); + } + } + + finalize = (m->first_ep == NULL); + if (finalize) + { + natsLib_stopServiceCallbacks(m); + m->stopped = true; + doneHandler = m->cfg->DoneHandler; + } + + _unlock_service(m); + + if (finalize) + { + if (doneHandler != NULL) + doneHandler(m); + + // Relase the endpoint's server reference from `micro_add_endpoint`. + _release_service(m); + } + + return NULL; +} + +static bool +_find_endpoint(microEndpoint **prevp, microService *m, microEndpoint *to_find) +{ + microEndpoint *ep = NULL; + microEndpoint *prev_ep = NULL; + + if ((m == NULL) || (to_find == NULL)) + return false; + + for (ep = m->first_ep; ep != NULL; ep = ep->next) + { + if (ep == to_find) + { + *prevp = prev_ep; + return true; + } + prev_ep = ep; + } + + return false; +} + +void micro_release_on_endpoint_complete(void *closure) +{ + microEndpoint *ep = (microEndpoint *)closure; + microEndpoint *prev_ep = NULL; + microService *m = NULL; + natsSubscription *sub = NULL; + microDoneHandler doneHandler = NULL; + bool free_ep = false; + bool finalize = false; + + if (ep == NULL) + return; + + m = ep->m; + if ((m == NULL) || (m->service_mu == NULL)) + return; + + micro_lock_endpoint(ep); + ep->is_draining = false; + sub = ep->sub; + ep->sub = NULL; + ep->refs--; + free_ep = (ep->refs == 0); + micro_unlock_endpoint(ep); + + // Force the subscription to be destroyed now. + natsSubscription_Destroy(sub); + + _lock_service(m); + + // Release the service reference for the completed endpoint. It can not be + // the last reference, so no need to free m. + m->refs--; + + // Unlink the endpoint from the service. + if (_find_endpoint(&prev_ep, m, ep)) + { + if (prev_ep != NULL) + { + prev_ep->next = ep->next; + } + else + { + m->first_ep = ep->next; + } + } + + finalize = (!m->stopped) && (m->first_ep == NULL); + if (finalize) + { + natsLib_stopServiceCallbacks(m); + m->stopped = true; + doneHandler = m->cfg->DoneHandler; + } + + _unlock_service(m); + + if (free_ep) + micro_free_endpoint(ep); + + if (finalize) + { + if (doneHandler != NULL) + doneHandler(m); + + // Relase the endpoint's server reference from `micro_add_endpoint`. + _release_service(m); + } +} + +bool microService_IsStopped(microService *m) +{ + bool stopped; + + if ((m == NULL) || (m->service_mu == NULL)) + return true; + + _lock_service(m); + stopped = m->stopped; + _unlock_service(m); + + return stopped; +} + +microError * +microService_Destroy(microService *m) +{ + microError *err = NULL; + + err = microService_Stop(m); + if (err != NULL) + return err; + + _release_service(m); + return NULL; +} + +microError * +microService_Run(microService *m) +{ + if ((m == NULL) || (m->service_mu == NULL)) + return micro_ErrorInvalidArg; + + while (!microService_IsStopped(m)) + { + nats_Sleep(50); + } + + return NULL; +} + +void * +microService_GetState(microService *m) +{ + if (m == NULL) + return NULL; + + return m->cfg->State; +} + +static microError * +_new_service(microService **ptr, natsConnection *nc) +{ + *ptr = NATS_CALLOC(1, sizeof(microService)); + if (*ptr == NULL) + return micro_ErrorOutOfMemory; + + natsConn_retain(nc); + (*ptr)->refs = 1; + (*ptr)->nc = nc; + (*ptr)->started = nats_Now() * 1000000; + return NULL; +} + +static void +_retain_service(microService *m) +{ + if (m == NULL) + return; + + _lock_service(m); + + ++(m->refs); + + _unlock_service(m); +} + +static void +_release_service(microService *m) +{ + int refs = 0; + + if (m == NULL) + return; + + _lock_service(m); + + refs = --(m->refs); + + _unlock_service(m); + + if (refs == 0) + _free_service(m); +} + +static void +_free_service(microService *m) +{ + microGroup *next = NULL; + + if (m == NULL) + return; + + // destroy all groups. + if (m->groups != NULL) + { + microGroup *g = m->groups; + while (g != NULL) + { + next = g->next; + NATS_FREE(g); + g = next; + } + } + + _free_cloned_service_config(m->cfg); + natsConn_release(m->nc); + natsMutex_Destroy(m->service_mu); + NATS_FREE(m); +} + +static inline microError * +_new_service_config(microServiceConfig **ptr) +{ + *ptr = NATS_CALLOC(1, sizeof(microServiceConfig)); + return (*ptr == NULL) ? micro_ErrorOutOfMemory : NULL; +} + +static microError * +_clone_service_config(microServiceConfig **out, microServiceConfig *cfg) +{ + microError *err = NULL; + microServiceConfig *new_cfg = NULL; + + if (out == NULL || cfg == NULL) + return micro_ErrorInvalidArg; + + err = _new_service_config(&new_cfg); + if (err == NULL) + { + memcpy(new_cfg, cfg, sizeof(microServiceConfig)); + } + // the strings are declared const for the public, but in a clone these need + // to be duplicated. + MICRO_CALL(err, micro_strdup((char **)&new_cfg->Name, cfg->Name)); + MICRO_CALL(err, micro_strdup((char **)&new_cfg->Version, cfg->Version)); + MICRO_CALL(err, micro_strdup((char **)&new_cfg->Description, cfg->Description)); + MICRO_CALL(err, micro_clone_metadata(&new_cfg->Metadata, &new_cfg->MetadataLen, cfg->Metadata, cfg->MetadataLen)); + MICRO_CALL(err, micro_clone_endpoint_config(&new_cfg->Endpoint, cfg->Endpoint)); + if (err != NULL) + { + _free_cloned_service_config(new_cfg); + return err; + } + + *out = new_cfg; + return NULL; +} + +static void +_free_cloned_service_config(microServiceConfig *cfg) +{ + if (cfg == NULL) + return; + + // the strings are declared const for the public, but in a clone these need + // to be freed. + NATS_FREE((char *)cfg->Name); + NATS_FREE((char *)cfg->Version); + NATS_FREE((char *)cfg->Description); + micro_free_cloned_metadata(cfg->Metadata, cfg->MetadataLen); + micro_free_cloned_endpoint_config(cfg->Endpoint); + NATS_FREE(cfg); +} + +static microError * +_start_service_callbacks(microService *m) +{ + natsStatus s = NATS_OK; + + if (m == NULL) + return micro_ErrorInvalidArg; + + // Extra reference to the service as long as its callbacks are registered. + _retain_service(m); + + s = natsLib_startServiceCallbacks(m); + if (s != NATS_OK) + { + _release_service(m); + } + + return micro_ErrorFromStatus(s); +} + +static microError * +_services_for_connection(microService ***to_call, int *num_microservices, natsConnection *nc) +{ + natsMutex *mu = natsLib_getServiceCallbackMutex(); + natsHash *h = natsLib_getAllServicesToCallback(); + microService *m = NULL; + microService **p = NULL; + natsHashIter iter; + int n = 0; + int i; + + natsMutex_Lock(mu); + + natsHashIter_Init(&iter, h); + while (natsHashIter_Next(&iter, NULL, (void **)&m)) + if (m->nc == nc) + n++; + natsHashIter_Done(&iter); + if (n > 0) + { + p = NATS_CALLOC(n, sizeof(microService *)); + if (p == NULL) + { + natsMutex_Unlock(mu); + return micro_ErrorOutOfMemory; + } + + natsHashIter_Init(&iter, h); + i = 0; + while (natsHashIter_Next(&iter, NULL, (void **)&m)) + { + if (m->nc == nc) + { + _retain_service(m); // for the callback + p[i++] = m; + } + } + natsHashIter_Done(&iter); + } + + natsMutex_Unlock(mu); + + *to_call = p; + *num_microservices = n; + return NULL; +} + +static void +_on_connection_closed(natsConnection *nc, void *ignored) +{ + microService *m = NULL; + microService **to_call = NULL; + microError *err = NULL; + int n = 0; + int i; + + err = _services_for_connection(&to_call, &n, nc); + if (err != NULL) + { + microError_Ignore(err); + return; + } + + for (i = 0; i < n; i++) + { + m = to_call[i]; + microError_Ignore(microService_Stop(m)); + + _release_service(m); + } + + NATS_FREE(to_call); +} + +static void +_on_service_error(microService *m, const char *subject, natsStatus s) +{ + microEndpoint *ep = NULL; + microError *err = NULL; + + if (m == NULL) + return; + + _lock_service(m); + for (ep = m->first_ep; + (ep != NULL) && !micro_match_endpoint_subject(ep->subject, subject); + ep = ep->next) + ; + micro_retain_endpoint(ep); // for the callback + _unlock_service(m); + + if (ep != NULL) + { + if (m->cfg->ErrHandler != NULL) + (*m->cfg->ErrHandler)(m, ep, s); + + err = microError_Wrapf(micro_ErrorFromStatus(s), "NATS error on endpoint %s", ep->subject); + micro_update_last_error(ep, err); + microError_Destroy(err); + } + micro_release_endpoint(ep); // after the callback + + // TODO: Should we stop the service? The Go client does. + microError_Ignore(microService_Stop(m)); +} + +static void +_on_error(natsConnection *nc, natsSubscription *sub, natsStatus s, void *not_used) +{ + microService *m = NULL; + microService **to_call = NULL; + microError *err = NULL; + const char *subject = NULL; + int n = 0; + int i; + + if (sub == NULL) + { + return; + } + subject = natsSubscription_GetSubject(sub); + + // `to_call` will have a list of retained service pointers. + err = _services_for_connection(&to_call, &n, nc); + if (err != NULL) + { + microError_Ignore(err); + return; + } + + for (i = 0; i < n; i++) + { + m = to_call[i]; + _on_service_error(m, subject, s); + _release_service(m); // release the extra ref in `to_call`. + } + + NATS_FREE(to_call); +} + +static microError * +_wrap_connection_event_callbacks(microService *m) +{ + microError *err = NULL; + + if ((m == NULL) || (m->nc == NULL) || (m->nc->opts == NULL)) + return micro_ErrorInvalidArg; + + // The new service must be in the list for this to work. + MICRO_CALL(err, _start_service_callbacks(m)); + MICRO_CALL(err, micro_ErrorFromStatus( + natsOptions_setMicroCallbacks(m->nc->opts, _on_connection_closed, _on_error))); + + return microError_Wrapf(err, "failed to wrap connection event callbacks"); +} + +microError * +microService_AddGroup(microGroup **new_group, microService *m, const char *prefix) +{ + if ((m == NULL) || (new_group == NULL) || (prefix == NULL)) + return micro_ErrorInvalidArg; + + *new_group = NATS_CALLOC(1, sizeof(microGroup) + + strlen(prefix) + 1); // "prefix\0" + if (new_group == NULL) + { + return micro_ErrorOutOfMemory; + } + + memcpy((*new_group)->prefix, prefix, strlen(prefix) + 1); + (*new_group)->m = m; + (*new_group)->next = m->groups; + m->groups = *new_group; + + return NULL; +} + +microError * +microGroup_AddGroup(microGroup **new_group, microGroup *parent, const char *prefix) +{ + char *p; + size_t len; + + if ((parent == NULL) || (new_group == NULL) || (prefix == NULL)) + return micro_ErrorInvalidArg; + + *new_group = NATS_CALLOC(1, sizeof(microGroup) + + strlen(parent->prefix) + 1 + // "parent_prefix." + strlen(prefix) + 1); // "prefix\0" + if (new_group == NULL) + { + return micro_ErrorOutOfMemory; + } + + p = (*new_group)->prefix; + len = strlen(parent->prefix); + memcpy(p, parent->prefix, len); + p[len] = '.'; + p += len + 1; + memcpy(p, prefix, strlen(prefix) + 1); + (*new_group)->m = parent->m; + (*new_group)->next = parent->m->groups; + parent->m->groups = *new_group; + + return NULL; +} + +natsConnection * +microService_GetConnection(microService *m) +{ + if (m == NULL) + return NULL; + return m->nc; +} + +microError * +microService_GetInfo(microServiceInfo **new_info, microService *m) +{ + microError *err = NULL; + microServiceInfo *info = NULL; + microEndpoint *ep = NULL; + int len; + + if ((new_info == NULL) || (m == NULL) || (m->service_mu == NULL)) + return micro_ErrorInvalidArg; + + info = NATS_CALLOC(1, sizeof(microServiceInfo)); + if (info == NULL) + return micro_ErrorOutOfMemory; + + MICRO_CALL(err, micro_strdup((char **)&info->Name, m->cfg->Name)); + MICRO_CALL(err, micro_strdup((char **)&info->Version, m->cfg->Version)); + MICRO_CALL(err, micro_strdup((char **)&info->Description, m->cfg->Description)); + MICRO_CALL(err, micro_strdup((char **)&info->Id, m->id)); + MICRO_CALL(err, micro_clone_metadata(&info->Metadata, &info->MetadataLen, m->cfg->Metadata, m->cfg->MetadataLen)); + + if (err == NULL) + { + info->Type = MICRO_INFO_RESPONSE_TYPE; + + _lock_service(m); + + len = 0; + for (ep = m->first_ep; ep != NULL; ep = ep->next) + { + if ((!ep->is_monitoring_endpoint) && (ep->subject != NULL)) + len++; + } + + // Overallocate subjects, will filter out internal ones. + info->Endpoints = NATS_CALLOC(len, sizeof(microEndpointInfo)); + if (info->Endpoints == NULL) + { + err = micro_ErrorOutOfMemory; + } + + len = 0; + for (ep = m->first_ep; (err == NULL) && (ep != NULL); ep = ep->next) + { + if ((!ep->is_monitoring_endpoint) && (ep->subject != NULL)) + { + MICRO_CALL(err, micro_strdup((char **)&info->Endpoints[len].Name, ep->name)); + MICRO_CALL(err, micro_strdup((char **)&info->Endpoints[len].Subject, ep->subject)); + MICRO_CALL(err, micro_clone_metadata(&(info->Endpoints[len].Metadata), &info->Endpoints[len].MetadataLen, ep->config->Metadata, ep->config->MetadataLen)); + if (err == NULL) + { + len++; + info->EndpointsLen = len; + } + } + } + _unlock_service(m); + } + + if (err != NULL) + { + microServiceInfo_Destroy(info); + return err; + } + + *new_info = info; + return NULL; +} + +void microServiceInfo_Destroy(microServiceInfo *info) +{ + int i; + + if (info == NULL) + return; + + // casts to quiet the compiler. + for (i = 0; i < info->EndpointsLen; i++) + { + NATS_FREE((char *)info->Endpoints[i].Name); + NATS_FREE((char *)info->Endpoints[i].Subject); + micro_free_cloned_metadata(info->Endpoints[i].Metadata, info->Endpoints[i].MetadataLen); + } + NATS_FREE((char *)info->Endpoints); + NATS_FREE((char *)info->Name); + NATS_FREE((char *)info->Version); + NATS_FREE((char *)info->Description); + NATS_FREE((char *)info->Id); + micro_free_cloned_metadata(info->Metadata, info->MetadataLen); + NATS_FREE(info); +} + +microError * +microService_GetStats(microServiceStats **new_stats, microService *m) +{ + microError *err = NULL; + microServiceStats *stats = NULL; + microEndpoint *ep = NULL; + int len; + long double avg = 0.0; + + if ((new_stats == NULL) || (m == NULL) || (m->service_mu == NULL)) + return micro_ErrorInvalidArg; + + stats = NATS_CALLOC(1, sizeof(microServiceStats)); + if (stats == NULL) + return micro_ErrorOutOfMemory; + + MICRO_CALL(err, micro_strdup((char **)&stats->Name, m->cfg->Name)); + MICRO_CALL(err, micro_strdup((char **)&stats->Version, m->cfg->Version)); + MICRO_CALL(err, micro_strdup((char **)&stats->Id, m->id)); + + if (err == NULL) + { + stats->Started = m->started; + stats->Type = MICRO_STATS_RESPONSE_TYPE; + + _lock_service(m); + + len = 0; + for (ep = m->first_ep; ep != NULL; ep = ep->next) + { + if ((ep != NULL) && (!ep->is_monitoring_endpoint)) + len++; + } + + // Allocate the actual structs, not pointers. + stats->Endpoints = NATS_CALLOC(len, sizeof(microEndpointStats)); + if (stats->Endpoints == NULL) + { + err = micro_ErrorOutOfMemory; + } + + len = 0; + for (ep = m->first_ep; ((err == NULL) && (ep != NULL)); ep = ep->next) + { + if ((ep != NULL) && (!ep->is_monitoring_endpoint) && (ep->endpoint_mu != NULL)) + { + micro_lock_endpoint(ep); + // copy the entire struct, including the last error buffer. + stats->Endpoints[len] = ep->stats; + + MICRO_CALL(err, micro_strdup((char **)&stats->Endpoints[len].Name, ep->name)); + MICRO_CALL(err, micro_strdup((char **)&stats->Endpoints[len].Subject, ep->subject)); + if (err == NULL) + { + avg = (long double)ep->stats.ProcessingTimeSeconds * 1000000000.0 + (long double)ep->stats.ProcessingTimeNanoseconds; + avg = avg / (long double)ep->stats.NumRequests; + stats->Endpoints[len].AverageProcessingTimeNanoseconds = (int64_t)avg; + len++; + stats->EndpointsLen = len; + } + micro_unlock_endpoint(ep); + } + } + + _unlock_service(m); + } + + if (err != NULL) + { + microServiceStats_Destroy(stats); + return err; + } + *new_stats = stats; + return NULL; +} + +void microServiceStats_Destroy(microServiceStats *stats) +{ + int i; + + if (stats == NULL) + return; + + for (i = 0; i < stats->EndpointsLen; i++) + { + NATS_FREE((char *)stats->Endpoints[i].Name); + NATS_FREE((char *)stats->Endpoints[i].Subject); + } + NATS_FREE(stats->Endpoints); + NATS_FREE((char *)stats->Name); + NATS_FREE((char *)stats->Version); + NATS_FREE((char *)stats->Id); + NATS_FREE(stats); +} + +void micro_free_cloned_metadata(const char **metadata, int len) +{ + int i; + + if (metadata == NULL) + return; + + for (i = 0; i < len*2; i++) + { + NATS_FREE((char *)metadata[i]); + } + NATS_FREE((char **)metadata); +} + +microError *micro_clone_metadata(const char ***new_metadata, int *new_len, const char **metadata, int len) +{ + char **dup = NULL; + int i; + + if (new_metadata == NULL) + return micro_ErrorInvalidArg; + *new_metadata = NULL; + + if (len == 0) + return NULL; + + dup = NATS_CALLOC(len * 2, sizeof(char *)); + if (dup == NULL) + return micro_ErrorOutOfMemory; + + for (i = 0; i < len*2; i++) + { + micro_strdup(&dup[i], metadata[i]); + if (dup[i] == NULL) + { + micro_free_cloned_metadata((const char **)dup, i); + return micro_ErrorOutOfMemory; + } + } + + *new_metadata = (const char **)dup; + *new_len = len; + return NULL; +} diff --git a/src/micro_args.c b/src/micro_args.c new file mode 100644 index 000000000..7db7c7a77 --- /dev/null +++ b/src/micro_args.c @@ -0,0 +1,355 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "microp.h" +#include "micro_args.h" + +struct args_s +{ + void **args; + int count; +}; + +static microError *parse(void **args, int *args_len, const char *data, int data_len); + +static inline microError *new_args(microArgs **ptr, int n) +{ + *ptr = NATS_CALLOC(1, sizeof(microArgs)); + if (*ptr == NULL) + return micro_ErrorOutOfMemory; + + (*ptr)->args = NATS_CALLOC(n, sizeof(void *)); + if ((*ptr)->args == NULL) + { + NATS_FREE(*ptr); + return micro_ErrorOutOfMemory; + } + + (*ptr)->count = n; + return NULL; +} + +microError * +micro_ParseArgs(microArgs **ptr, const char *data, int data_len) +{ + microError *err = NULL; + microArgs *args = NULL; + int n = 0; + + if ((ptr == NULL) || (data == NULL) || (data_len < 0)) + return microError_Wrapf(micro_ErrorInvalidArg, "failed to parse args"); + + MICRO_CALL(err, parse(NULL, &n, data, data_len)); + MICRO_CALL(err, new_args(&args, n)); + MICRO_CALL(err, parse(args->args, &n, data, data_len)); + + if (err != NULL) + { + microArgs_Destroy(args); + return microError_Wrapf(err, "failed to parse args"); + } + *ptr = args; + return NULL; +} + +void microArgs_Destroy(microArgs *args) +{ + int i; + + if (args == NULL) + return; + + for (i = 0; i < args->count; i++) + { + NATS_FREE(args->args[i]); + } + NATS_FREE(args->args); + NATS_FREE(args); +} + +int microArgs_Count(microArgs *args) +{ + if (args == NULL) + return 0; + + return args->count; +} + +microError * +microArgs_GetInt(int *val, microArgs *args, int index) +{ + if ((args == NULL) || (index < 0) || (index >= args->count) || (val == NULL)) + return micro_ErrorInvalidArg; + + *val = *((int *)args->args[index]); + return NULL; +} + +microError * +microArgs_GetFloat(long double *val, microArgs *args, int index) +{ + if ((args == NULL) || (index < 0) || (index >= args->count) || (val == NULL)) + return micro_ErrorInvalidArg; + + *val = *((long double *)args->args[index]); + return NULL; +} + +microError * +microArgs_GetString(const char **val, microArgs *args, int index) +{ + if ((args == NULL) || (index < 0) || (index >= args->count) || (val == NULL)) + return micro_ErrorInvalidArg; + + *val = (const char *)args->args[index]; + return NULL; +} + +// decodes the rest of a string into a pre-allocated buffer of sufficient +// length, or just calculates the needed buffer size. The opening quote must +// have been already processed by the caller (parse). +static microError * +decode_rest_of_string(char *dup, int *decoded_len, int *i, const char *data, int data_len) +{ + char c; + int len = 0; + bool terminated = false; + bool escape = false; + + for (; !terminated && *i < data_len; (*i)++) + { + c = data[*i]; + switch (c) + { + case '"': + if (escape) + { + // include the escaped quote. + if (dup != NULL) + { + dup[len] = c; + } + len++; + escape = false; + } + else + { + // end of quoted string. + terminated = true; + } + break; + + case '\\': + if (!escape) + { + escape = true; + } + else + { + // include the escaped backslash. + if (dup != NULL) + { + dup[len] = c; + } + len++; + escape = false; + } + break; + + default: + if (dup != NULL) + { + dup[len] = c; + } + len++; + escape = false; + break; + } + } + if (!terminated) + { + return micro_Errorf("a quoted string is not properly terminated"); + } + + *decoded_len = len; + return NULL; +} + +static microError * +decode_and_dupe_rest_of_string(char **dup, int *i, const char *data, int data_len) +{ + microError *err = NULL; + int start = *i; + int decoded_len = 0; + + err = decode_rest_of_string(NULL, &decoded_len, i, data, data_len); + if (err != NULL) + { + return err; + } + if (dup == NULL) + { + // nothing else to do - the string has been scanned and validated. + return NULL; + } + + *i = start; + + *dup = NATS_CALLOC(decoded_len + 1, sizeof(char)); + if (*dup == NULL) + { + return micro_ErrorOutOfMemory; + } + + // no need to check for error the 2nd time, we already know the string is + // valid. + decode_rest_of_string(*dup, &decoded_len, i, data, data_len); + (*dup)[decoded_len] = 0; + return NULL; +} + +typedef enum parserState +{ + NewArg = 0, + NumberArg, +} parserState; + +static microError * +parse(void **args, int *args_len, const char *data, int data_len) +{ + int i = 0; + microError *err = NULL; + char c; + int n = 0; + parserState state = NewArg; + char numbuf[64]; + int num_len = 0; + bool is_float = false; + +#define EOS 0 + for (; i < data_len + 1;) + { + c = (i < data_len) ? data[i] : EOS; + + switch (state) + { + case NewArg: + switch (c) + { + case EOS: + case ' ': + i++; + break; + + case '"': + i++; // consume the opening quote. + err = decode_and_dupe_rest_of_string((char **)(&args[n]), &i, data, data_len); + if (err != NULL) + { + return err; + } + n++; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + case '+': + case '.': + state = NumberArg; + num_len = 0; + numbuf[num_len++] = c; + is_float = (c == '.'); + i++; + break; + + default: + return micro_Errorf("unexpected '%c', an argument must be a number or a quoted string", c); + } + break; + + case NumberArg: + switch (c) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + case '+': + case '.': + case 'e': + case 'E': + case ',': + numbuf[num_len] = c; + num_len++; + is_float = is_float || (c == '.') || (c == 'e') || (c == 'E'); + i++; + break; + + case EOS: + case ' ': + if (args != NULL) + { + numbuf[num_len] = 0; + if (is_float) + { + args[n] = NATS_CALLOC(1, sizeof(long double)); + if (args[n] == NULL) + { + return micro_ErrorOutOfMemory; + } + *(long double *)args[n] = strtold(numbuf, NULL); + } + else + { + args[n] = NATS_CALLOC(1, sizeof(int)); + if (args[n] == NULL) + { + return micro_ErrorOutOfMemory; + } + *(int *)args[n] = atoi(numbuf); + } + } + n++; + i++; + state = NewArg; + break; + + default: + return micro_Errorf("unexpected '%c', a number must be followed by a space", c); + } + break; + + default: + return micro_Errorf("unreachable: wrong state for a ' ', expected NewArg or NumberArg, got %d", state); + } + } + + *args_len = n; + return NULL; +} diff --git a/src/micro_args.h b/src/micro_args.h new file mode 100644 index 000000000..fa3efa40a --- /dev/null +++ b/src/micro_args.h @@ -0,0 +1,43 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MICRO_ARGS_H_ +#define MICRO_ARGS_H_ + +/** + * Request unmarshaled as "arguments", a space-separated list of numbers and strings. + * TODO document the interface. + */ +typedef struct args_s microArgs; + +NATS_EXTERN microError * +micro_ParseArgs(microArgs **args, const char *data, int data_len); + +NATS_EXTERN int +microArgs_Count(microArgs *args); + +NATS_EXTERN microError * +microArgs_GetInt(int *val, microArgs *args, int index); + +NATS_EXTERN microError * +microArgs_GetFloat(long double *val, microArgs *args, int index); + +NATS_EXTERN microError * +microArgs_GetString(const char **val, microArgs *args, int index); + +NATS_EXTERN void +microArgs_Destroy(microArgs *args); + +/** @} */ // end of microserviceGroup + +#endif /* MICRO_H_ */ diff --git a/src/micro_client.c b/src/micro_client.c new file mode 100644 index 000000000..efb658571 --- /dev/null +++ b/src/micro_client.c @@ -0,0 +1,66 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "microp.h" +#include "conn.h" + +microError * +micro_NewClient(microClient **new_client, natsConnection *nc, microClientConfig *cfg) +{ + microClient *client = NULL; + + if (new_client == NULL) + return micro_ErrorInvalidArg; + + client = NATS_CALLOC(1, sizeof(microClient)); + if (client == NULL) + return micro_ErrorOutOfMemory; + + natsConn_retain(nc); + client->nc = nc; + *new_client = client; + return NULL; +} + +void microClient_Destroy(microClient *client) +{ + if (client == NULL) + return; + + natsConn_release(client->nc); + NATS_FREE(client); +} + +microError * +microClient_DoRequest(natsMsg **reply, microClient *client, const char *subject, const char *data, int data_len) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + natsMsg *msg = NULL; + + if ((client == NULL) || (reply == NULL)) + return micro_ErrorInvalidArg; + + s = natsConnection_Request(&msg, client->nc, subject, data, data_len, 5000); + if (s != NATS_OK) + { + return microError_Wrapf(micro_ErrorFromStatus(s), "request failed"); + } + + err = micro_is_error_message(s, msg); + if (err == NULL) + { + *reply = msg; + } + return err; +} diff --git a/src/micro_endpoint.c b/src/micro_endpoint.c new file mode 100644 index 000000000..95240989e --- /dev/null +++ b/src/micro_endpoint.c @@ -0,0 +1,430 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "microp.h" + +static microError *_dup_with_prefix(char **dst, const char *prefix, const char *src); + +static void _handle_request(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure); + +static void _retain_endpoint(microEndpoint *ep, bool lock); +static void _release_endpoint(microEndpoint *ep); + +microError * +micro_new_endpoint(microEndpoint **new_ep, microService *m, const char *prefix, microEndpointConfig *cfg, bool is_internal) +{ + microError *err = NULL; + microEndpoint *ep = NULL; + const char *subj; + + if (cfg == NULL) + return microError_Wrapf(micro_ErrorInvalidArg, "NULL endpoint config"); + if (!micro_is_valid_name(cfg->Name)) + return microError_Wrapf(micro_ErrorInvalidArg, "invalid endpoint name %s", cfg->Name); + if (cfg->Handler == NULL) + return microError_Wrapf(micro_ErrorInvalidArg, "NULL endpoint request handler for %s", cfg->Name); + + if ((cfg->Subject != NULL) && !micro_is_valid_subject(cfg->Subject)) + return micro_ErrorInvalidArg; + + subj = nats_IsStringEmpty(cfg->Subject) ? cfg->Name : cfg->Subject; + + ep = NATS_CALLOC(1, sizeof(microEndpoint)); + if (ep == NULL) + return micro_ErrorOutOfMemory; + ep->is_monitoring_endpoint = is_internal; + ep->m = m; + + MICRO_CALL(err, micro_ErrorFromStatus(natsMutex_Create(&ep->endpoint_mu))); + MICRO_CALL(err, micro_clone_endpoint_config(&ep->config, cfg)); + MICRO_CALL(err, _dup_with_prefix(&ep->name, prefix, cfg->Name)); + MICRO_CALL(err, _dup_with_prefix(&ep->subject, prefix, subj)); + if (err != NULL) + { + micro_free_endpoint(ep); + return err; + } + + *new_ep = ep; + return NULL; +} + +microError * +micro_start_endpoint(microEndpoint *ep) +{ + natsStatus s = NATS_OK; + natsSubscription *sub = NULL; + + if ((ep->subject == NULL) || (ep->config == NULL) || (ep->config->Handler == NULL) || (ep->m == NULL)) + // nothing to do + return NULL; + + // reset the stats. + memset(&ep->stats, 0, sizeof(ep->stats)); + + if (ep->is_monitoring_endpoint) + s = natsConnection_Subscribe(&sub, ep->m->nc, ep->subject, _handle_request, ep); + else + s = natsConnection_QueueSubscribe(&sub, ep->m->nc, ep->subject, MICRO_QUEUE_GROUP, _handle_request, ep); + + if (s == NATS_OK) + { + // extra retain for the subscription since we'll need to hold it until + // on_complete. + micro_lock_endpoint(ep); + ep->refs++; + ep->sub = sub; + ep->is_draining = false; + micro_unlock_endpoint(ep); + + natsSubscription_SetOnCompleteCB(sub, micro_release_on_endpoint_complete, ep); + } + else + { + natsSubscription_Destroy(sub); // likely always a no-op. + } + + return micro_ErrorFromStatus(s); +} + +microError * +micro_stop_endpoint(microEndpoint *ep) +{ + natsStatus s = NATS_OK; + natsSubscription *sub = NULL; + + if ((ep == NULL) || (ep->m == NULL)) + return NULL; + + micro_lock_endpoint(ep); + sub = ep->sub; + + if (ep->is_draining || natsConnection_IsClosed(ep->m->nc) || !natsSubscription_IsValid(sub)) + { + // If stopping, _release_on_endpoint_complete will take care of + // finalizing, nothing else to do. In other cases + // _release_on_endpoint_complete has already been called. + micro_unlock_endpoint(ep); + return NULL; + } + + ep->is_draining = true; + micro_unlock_endpoint(ep); + + // When the drain is complete, will release the final ref on ep. + s = natsSubscription_Drain(sub); + if (s != NATS_OK) + { + return microError_Wrapf(micro_ErrorFromStatus(s), + "failed to stop endpoint %s: failed to drain subscription", ep->name); + } + + return NULL; +} + +void micro_retain_endpoint(microEndpoint *ep) +{ + if (ep == NULL) + return; + + micro_lock_endpoint(ep); + + ep->refs++; + + micro_unlock_endpoint(ep); +} + +void micro_release_endpoint(microEndpoint *ep) +{ + int refs; + + if (ep == NULL) + return; + + micro_lock_endpoint(ep); + + refs = --(ep->refs); + + micro_unlock_endpoint(ep); + + if (refs == 0) + micro_free_endpoint(ep); +} + +void micro_free_endpoint(microEndpoint *ep) +{ + if (ep == NULL) + return; + + NATS_FREE(ep->name); + NATS_FREE(ep->subject); + natsSubscription_Destroy(ep->sub); + natsMutex_Destroy(ep->endpoint_mu); + micro_free_cloned_endpoint_config(ep->config); + NATS_FREE(ep); +} + +static void +_update_last_error(microEndpoint *ep, microError *err) +{ + ep->stats.NumErrors++; + microError_String(err, ep->stats.LastErrorString, sizeof(ep->stats.LastErrorString)); +} + +static void +_handle_request(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure) +{ + microError *err = NULL; + microError *service_err = NULL; + microEndpoint *ep = (microEndpoint *)closure; + microService *m; + microEndpointStats *stats = NULL; + microRequestHandler handler; + microRequest *req = NULL; + int64_t start, elapsed_ns = 0, full_s; + + if ((ep == NULL) || (ep->endpoint_mu == NULL) || (ep->config == NULL) || (ep->config->Handler == NULL)) + { + // This would be a bug, we should not have received a message on this + // subscription. + return; + } + + stats = &ep->stats; + m = ep->m; + handler = ep->config->Handler; + + err = micro_new_request(&req, m, ep, msg); + if (err == NULL) + { + // handle the request. + start = nats_NowInNanoSeconds(); + service_err = handler(req); + if (service_err != NULL) + { + // if the handler returned an error, we attempt to respond with it. + // Note that if the handler chose to do its own RespondError which + // fails, and then the handler returns its error - we'll try to + // RespondError again, double-counting the error. + err = microRequest_RespondError(req, service_err); + } + + elapsed_ns = nats_NowInNanoSeconds() - start; + } + + // Update stats. + micro_lock_endpoint(ep); + stats->NumRequests++; + stats->ProcessingTimeNanoseconds += elapsed_ns; + full_s = stats->ProcessingTimeNanoseconds / 1000000000; + stats->ProcessingTimeSeconds += full_s; + stats->ProcessingTimeNanoseconds -= full_s * 1000000000; + _update_last_error(ep, err); + micro_unlock_endpoint(ep); + + microError_Destroy(err); + micro_free_request(req); + natsMsg_Destroy(msg); +} + +void micro_update_last_error(microEndpoint *ep, microError *err) +{ + if (err == NULL || ep == NULL) + return; + + micro_lock_endpoint(ep); + _update_last_error(ep, err); + micro_unlock_endpoint(ep); +} + +bool micro_is_valid_name(const char *name) +{ + int i; + int len; + + if (name == NULL) + return false; + + len = (int)strlen(name); + if (len == 0) + return false; + + for (i = 0; i < len; i++) + { + if (!isalnum(name[i]) && (name[i] != '_') && (name[i] != '-')) + return false; + } + return true; +} + +bool micro_is_valid_subject(const char *subject) +{ + int i; + int len; + + if (subject == NULL) + return false; + + len = (int)strlen(subject); + if (len == 0) + return false; + + for (i = 0; i < len - 1; i++) + { + if ((subject[i] == ' ') || (subject[i] == '>')) + return false; + } + + if ((subject[i] == ' ')) + return false; + + return true; +} + +static inline microError * +_new_endpoint_config(microEndpointConfig **ptr) +{ + *ptr = NATS_CALLOC(1, sizeof(microEndpointConfig)); + return (*ptr == NULL) ? micro_ErrorOutOfMemory : NULL; +} + +microError * +micro_clone_endpoint_config(microEndpointConfig **out, microEndpointConfig *cfg) +{ + microError *err = NULL; + microEndpointConfig *new_cfg = NULL; + + if (out == NULL) + return micro_ErrorInvalidArg; + + if (cfg == NULL) + { + *out = NULL; + return NULL; + } + + err = _new_endpoint_config(&new_cfg); + if (err == NULL) + { + memcpy(new_cfg, cfg, sizeof(microEndpointConfig)); + } + + MICRO_CALL(err, micro_strdup((char **)&new_cfg->Name, cfg->Name)); + MICRO_CALL(err, micro_strdup((char **)&new_cfg->Subject, cfg->Subject)); + MICRO_CALL(err, micro_clone_metadata(&new_cfg->Metadata, &new_cfg->MetadataLen, cfg->Metadata, cfg->MetadataLen)); + + if (err != NULL) + { + micro_free_cloned_endpoint_config(new_cfg); + return err; + } + + *out = new_cfg; + return NULL; +} + +void micro_free_cloned_endpoint_config(microEndpointConfig *cfg) +{ + if (cfg == NULL) + return; + + // the strings are declared const for the public, but in a clone these need + // to be freed. + NATS_FREE((char *)cfg->Name); + NATS_FREE((char *)cfg->Subject); + micro_free_cloned_metadata(cfg->Metadata, cfg->MetadataLen); + + NATS_FREE(cfg); +} + +bool micro_match_endpoint_subject(const char *ep_subject, const char *actual_subject) +{ + const char *e = ep_subject; + const char *a = actual_subject; + const char *etok, *enext; + int etok_len; + bool last_etok = false; + const char *atok, *anext; + int atok_len; + bool last_atok = false; + + if (e == NULL || a == NULL) + return false; + + while (true) + { + enext = strchr(e, '.'); + if (enext == NULL) + { + enext = e + strlen(e); + last_etok = true; + } + etok = e; + etok_len = (int)(enext - e); + e = enext + 1; + + anext = strchr(a, '.'); + if (anext == NULL) + { + anext = a + strlen(a); + last_atok = true; + } + atok = a; + atok_len = (int)(anext - a); + a = anext + 1; + + if (last_etok) + { + if (etok_len == 1 && etok[0] == '>') + return true; + + if (!last_atok) + return false; + } + if (!(etok_len == 1 && etok[0] == '*') && + !(etok_len == atok_len && strncmp(etok, atok, etok_len) == 0)) + { + return false; + } + if (last_atok) + { + return last_etok; + } + } +} + +static microError *_dup_with_prefix(char **dst, const char *prefix, const char *src) +{ + size_t len = strlen(src) + 1; + char *p; + + if (!nats_IsStringEmpty(prefix)) + len += strlen(prefix) + 1; + + *dst = NATS_CALLOC(1, len); + if (*dst == NULL) + return micro_ErrorOutOfMemory; + + p = *dst; + if (!nats_IsStringEmpty(prefix)) + { + len = strlen(prefix); + memcpy(p, prefix, len); + p[len] = '.'; + p += len + 1; + } + memcpy(p, src, strlen(src) + 1); + return NULL; +} diff --git a/src/micro_error.c b/src/micro_error.c new file mode 100644 index 000000000..4b64a4088 --- /dev/null +++ b/src/micro_error.c @@ -0,0 +1,233 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "microp.h" + +static microError _errorOutOfMemory = { + .is_internal = true, + .status = NATS_NO_MEMORY, + .message = "out of memory", +}; + +static microError _errorInvalidArg = { + .is_internal = true, + .status = NATS_INVALID_ARG, + .message = "invalid function argument", +}; + +static microError _errorInvalidFormat = { + .is_internal = true, + .status = NATS_INVALID_ARG, + .message = "invalid format string", +}; + +microError *micro_ErrorOutOfMemory = &_errorOutOfMemory; +microError *micro_ErrorInvalidArg = &_errorInvalidArg; + +static microError * +verrorf(natsStatus s, int code, const char *format, va_list args) +{ + microError *err = NULL; + char *ptr; + int message_len = 0; + + va_list args2; + va_copy(args2, args); + + if (format == NULL) + format = ""; + + // Do not use nats_vsnprintf here since we want to calculate the size of + // the resulting formatted string. On Windows, that would fail. Use + // that instead. + message_len = nats_vscprintf(format, args); + if (message_len < 0) + { + va_end(args2); + return &_errorInvalidFormat; + } + + err = NATS_CALLOC(1, sizeof(microError) + message_len + 1); + if (err == NULL) + { + va_end(args2); + return &_errorOutOfMemory; + } + + ptr = (char *)(err) + sizeof(microError); + nats_vsnprintf(ptr, message_len + 1, format, args2); + va_end(args2); + err->message = (const char *)ptr; + + err->code = code; + err->status = s; + + return err; +} + +microError * +micro_Errorf(const char *format, ...) +{ + microError *err = NULL; + va_list args; + + va_start(args, format); + err = verrorf(NATS_OK, 0, format, args); + va_end(args); + return err; +} + +microError * +micro_ErrorfCode(int code, const char *format, ...) +{ + microError *err = NULL; + va_list args; + + va_start(args, format); + err = verrorf(NATS_OK, code, format, args); + va_end(args); + return err; +} + +microError * +micro_ErrorFromStatus(natsStatus s) +{ + microError *err = NULL; + const char *message = natsStatus_GetText(s); + size_t message_len = strlen(message); + char *ptr; + + if (s == NATS_OK) + return NULL; + + err = NATS_CALLOC(1, sizeof(microError) + message_len + 1); + if (err == NULL) + return &_errorOutOfMemory; + + ptr = (char *)(err) + sizeof(microError); + memcpy(ptr, message, message_len + 1); + err->message = ptr; + err->status = s; + return err; +} + +microError * +micro_is_error_message(natsStatus status, natsMsg *msg) +{ + microError *err = NULL; + const char *c = NULL, *d = NULL; + bool is_service_error; + bool is_nats_error = (status != NATS_OK); + int code = 0; + + if (msg != NULL) + { + natsMsgHeader_Get(msg, MICRO_ERROR_CODE_HDR, &c); + natsMsgHeader_Get(msg, MICRO_ERROR_HDR, &d); + } + if (!nats_IsStringEmpty(c)) + { + code = atoi(c); + } + is_service_error = (code != 0) || !nats_IsStringEmpty(d); + + if (is_service_error && !is_nats_error) + { + return micro_ErrorfCode(code, d); + } + else if (!is_service_error && is_nats_error) + { + return micro_ErrorFromStatus(status); + } + else if (is_service_error && is_nats_error) + { + err = microError_Wrapf(micro_ErrorFromStatus(status), d); + err->code = code; + return err; + } + + return NULL; +} + +microError * +microError_Wrapf(microError *err, const char *format, ...) +{ + va_list args; + microError *new_err = NULL; + + if (err == NULL) + return NULL; + + va_start(args, format); + new_err = verrorf(NATS_OK, 0, format, args); + va_end(args); + + new_err->cause = err; + return new_err; +} + +const char * +microError_String(microError *err, char *buf, size_t size) +{ + size_t used = 0; + const char *caused; + + if (buf == NULL) + return ""; + if (err == NULL) + { + snprintf(buf, size, "null"); + return buf; + } + + if (err->status != NATS_OK) + { + used += snprintf(buf + used, size - used, "status %u: ", err->status); + } + if (err->code != 0) + { + used += snprintf(buf + used, size - used, "code %d: ", err->code); + } + used += snprintf(buf + used, size - used, "%s", err->message); + + if (err->cause != NULL) + { + used += snprintf(buf + used, size - used, ": "); + caused = microError_String(err->cause, buf + used, size - used); + used += strlen(caused); + } + return buf; +} + +natsStatus +microError_Status(microError *err) +{ + if (err == NULL) + return NATS_OK; + + if (err->status != NATS_OK) + return err->status; + + return microError_Status(err->cause); +} + +void microError_Destroy(microError *err) +{ + if ((err == NULL) || err->is_internal) + return; + + microError_Destroy(err->cause); + NATS_FREE(err); +} diff --git a/src/micro_monitoring.c b/src/micro_monitoring.c new file mode 100644 index 000000000..82608d13c --- /dev/null +++ b/src/micro_monitoring.c @@ -0,0 +1,374 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "microp.h" +#include "util.h" + +static microError *marshal_ping(natsBuffer **new_buf, microService *m); +static microError *handle_ping(microRequest *req); +static microError *marshal_info(natsBuffer **new_buf, microServiceInfo *info); +static microError *handle_info(microRequest *req); +static microError *marshal_stats(natsBuffer **new_buf, microServiceStats *stats); +static microError *handle_stats(microRequest *req); + +static microError * +add_internal_handler(microService *m, const char *verb, const char *kind, const char *id, const char *name, microRequestHandler handler); +static microError * +add_verb_handlers(microService *m, const char *verb, microRequestHandler handler); +static microError * +new_dotted_subject(char **new_subject, int count, ...); + +microError * +micro_init_monitoring(microService *m) +{ + microError *err = NULL; + MICRO_CALL(err, add_verb_handlers(m, MICRO_PING_VERB, handle_ping)); + MICRO_CALL(err, add_verb_handlers(m, MICRO_STATS_VERB, handle_stats)); + MICRO_CALL(err, add_verb_handlers(m, MICRO_INFO_VERB, handle_info)); + return err; +} + +static microError * +handle_ping(microRequest *req) +{ + microError *err = NULL; + microService *m = microRequest_GetService(req); + natsBuffer *buf = NULL; + + if ((m == NULL) || (m->cfg == NULL)) + return micro_ErrorInvalidArg; // Should not happen + + MICRO_CALL(err, marshal_ping(&buf, m)); + MICRO_CALL(err, microRequest_Respond(req, natsBuf_Data(buf), natsBuf_Len(buf))); + + natsBuf_Destroy(buf); + return err; +} + +static microError * +handle_info(microRequest *req) +{ + microError *err = NULL; + microService *m = microRequest_GetService(req); + microServiceInfo *info = NULL; + natsBuffer *buf = NULL; + + if ((m == NULL) || (m->cfg == NULL)) + return micro_ErrorInvalidArg; // Should not happen + + MICRO_CALL(err, microService_GetInfo(&info, m)); + MICRO_CALL(err, marshal_info(&buf, info)); + MICRO_CALL(err, microRequest_Respond(req, natsBuf_Data(buf), natsBuf_Len(buf))); + + natsBuf_Destroy(buf); + microServiceInfo_Destroy(info); + return err; +} + +static microError * +handle_stats_internal(microRequest *req) +{ + microError *err = NULL; + microService *m = microRequest_GetService(req); + microServiceStats *stats = NULL; + natsBuffer *buf = NULL; + + if ((m == NULL) || (m == NULL)) + return micro_ErrorInvalidArg; // Should not happen + + MICRO_CALL(err, microService_GetStats(&stats, req->Service)); + MICRO_CALL(err, marshal_stats(&buf, stats)); + MICRO_CALL(err, microRequest_Respond(req, natsBuf_Data(buf), natsBuf_Len(buf))); + + natsBuf_Destroy(buf); + microServiceStats_Destroy(stats); + return err; +} + +static microError * +handle_stats(microRequest *req) +{ + microService *m = microRequest_GetService(req); + + if ((m == NULL) || (m->cfg == NULL)) + return micro_ErrorInvalidArg; // Should not happen + + if (m->cfg->StatsHandler != NULL) + return m->cfg->StatsHandler(req); + else + return handle_stats_internal(req); +} + +static microError * +new_dotted_subject(char **new_subject, int count, ...) +{ + va_list args; + int i; + size_t len, n; + char *result, *p; + + va_start(args, count); + len = 0; + for (i = 0; i < count; i++) + { + if (i > 0) + { + len++; /* for the dot */ + } + len += strlen(va_arg(args, char *)); + } + va_end(args); + + result = NATS_CALLOC(len + 1, 1); + if (result == NULL) + { + return micro_ErrorInvalidArg; + } + + len = 0; + va_start(args, count); + for (i = 0; i < count; i++) + { + if (i > 0) + { + result[len++] = '.'; + } + p = va_arg(args, char *); + n = strlen(p); + memcpy(result + len, p, n); + len += n; + } + va_end(args); + + *new_subject = result; + return NULL; +} + +microError * +micro_new_control_subject(char **newSubject, const char *verb, const char *name, const char *id) +{ + if (nats_IsStringEmpty(name) && !nats_IsStringEmpty(id)) + { + return micro_Errorf("service name is required when id is provided: %s", id); + } + + else if (nats_IsStringEmpty(name) && nats_IsStringEmpty(id)) + return new_dotted_subject(newSubject, 2, MICRO_API_PREFIX, verb); + else if (nats_IsStringEmpty(id)) + return new_dotted_subject(newSubject, 3, MICRO_API_PREFIX, verb, name); + else + return new_dotted_subject(newSubject, 4, MICRO_API_PREFIX, verb, name, id); +} + +static microError * +add_internal_handler(microService *m, const char *verb, const char *kind, + const char *id, const char *name, microRequestHandler handler) +{ + microError *err = NULL; + char *subj = NULL; + + err = micro_new_control_subject(&subj, verb, kind, id); + if (err != NULL) + return err; + + microEndpointConfig cfg = { + .Subject = subj, + .Name = name, + .Handler = handler, + }; + err = micro_add_endpoint(NULL, m, "", &cfg, true); + NATS_FREE(subj); + return err; +} + +// __verbHandlers generates control handlers for a specific verb. Each request +// generates 3 subscriptions, one for the general verb affecting all services +// written with the framework, one that handles all services of a particular +// kind, and finally a specific service instance. +static microError * +add_verb_handlers(microService *m, const char *verb, microRequestHandler handler) +{ + microError *err = NULL; + char name[1024]; + + snprintf(name, sizeof(name), "%s-all", verb); + err = add_internal_handler(m, verb, "", "", name, handler); + if (err == NULL) + { + snprintf(name, sizeof(name), "%s-kind", verb); + err = add_internal_handler(m, verb, m->cfg->Name, "", name, handler); + } + if (err == NULL) + { + err = add_internal_handler(m, verb, m->cfg->Name, m->id, verb, handler); + } + return err; +} + +// name and sep must be a string literal +#define IFOK_attr(_name, _value, _sep) \ + IFOK(s, natsBuf_Append(buf, "\"" _name "\":\"", -1)); \ + IFOK(s, natsBuf_Append(buf, (_value) != NULL ? (_value) : "", -1)); \ + IFOK(s, natsBuf_Append(buf, "\"" _sep, -1)); + +static microError * +marshal_ping(natsBuffer **new_buf, microService *m) +{ + natsBuffer *buf = NULL; + natsStatus s; + + s = natsBuf_Create(&buf, 1024); + if (s == NATS_OK) + { + s = natsBuf_AppendByte(buf, '{'); + IFOK_attr("name", m->cfg->Name, ","); + IFOK_attr("version", m->cfg->Version, ","); + IFOK_attr("id", m->id, ","); + IFOK_attr("type", MICRO_PING_RESPONSE_TYPE, ""); + IFOK(s, natsBuf_AppendByte(buf, '}')); + } + + if (s != NATS_OK) + { + natsBuf_Destroy(buf); + return micro_ErrorFromStatus(s); + } + + *new_buf = buf; + return NULL; +} + +natsStatus +_marshal_metadata(natsBuffer *buf, const char **metadata, int len) +{ + natsStatus s = NATS_OK; + int i; + + if (len > 0) + { + IFOK(s, natsBuf_Append(buf, "\"metadata\":{", -1)); + for (i = 0; ((s == NATS_OK) && (i < len)); i++) + { + IFOK(s, natsBuf_AppendByte(buf, '"')); + IFOK(s, natsBuf_Append(buf, metadata[i * 2], -1)); + IFOK(s, natsBuf_Append(buf, "\":\"", 3)); + IFOK(s, natsBuf_Append(buf, metadata[i * 2 + 1], -1)); + IFOK(s, natsBuf_AppendByte(buf, '"')); + if (i != len - 1) + IFOK(s, natsBuf_AppendByte(buf, ',')); + } + IFOK(s, natsBuf_Append(buf, "},", 2)); + } + return NATS_OK; +} + +static microError * +marshal_info(natsBuffer **new_buf, microServiceInfo *info) +{ + natsBuffer *buf = NULL; + natsStatus s; + int i; + + s = natsBuf_Create(&buf, 4096); + IFOK(s, natsBuf_AppendByte(buf, '{')); + + IFOK_attr("description", info->Description, ","); + + // "endpoints":{...} + if ((s == NATS_OK) && (info->EndpointsLen > 0)) + { + IFOK(s, natsBuf_Append(buf, "\"endpoints\":[", -1)); + for (i = 0; ((s == NATS_OK) && (i < info->EndpointsLen)); i++) + { + IFOK(s, natsBuf_AppendByte(buf, '{')); + IFOK_attr("name", info->Endpoints[i].Name, ","); + IFOK(s, _marshal_metadata(buf, info->Endpoints[i].Metadata, info->Endpoints[i].MetadataLen)); + IFOK_attr("subject", info->Endpoints[i].Subject, ""); + IFOK(s, natsBuf_AppendByte(buf, '}')); // end endpoint + if (i != info->EndpointsLen - 1) + IFOK(s, natsBuf_AppendByte(buf, ',')); + } + IFOK(s, natsBuf_Append(buf, "],", 2)); + } + + IFOK_attr("id", info->Id, ","); + IFOK(s, _marshal_metadata(buf, info->Metadata, info->MetadataLen)); + IFOK_attr("name", info->Name, ","); + IFOK_attr("type", info->Type, ","); + IFOK_attr("version", info->Version, ""); + IFOK(s, natsBuf_AppendByte(buf, '}')); + + if (s != NATS_OK) + { + natsBuf_Destroy(buf); + return microError_Wrapf(micro_ErrorFromStatus(s), "failed to marshal service info"); + } + *new_buf = buf; + return NULL; +} + +static microError * +marshal_stats(natsBuffer **new_buf, microServiceStats *stats) +{ + natsBuffer *buf = NULL; + natsStatus s; + int i; + char timebuf[128]; + microEndpointStats *ep; + + s = natsBuf_Create(&buf, 8 * 1024); + IFOK(s, natsBuf_AppendByte(buf, '{')); + IFOK_attr("id", stats->Id, ","); + IFOK_attr("name", stats->Name, ","); + IFOK_attr("type", stats->Type, ","); + IFOK(s, nats_EncodeTimeUTC(timebuf, sizeof(timebuf), stats->Started)); + IFOK_attr("started", timebuf, ","); + + if ((s == NATS_OK) && (stats->EndpointsLen > 0)) + { + IFOK(s, natsBuf_Append(buf, "\"endpoints\":[", -1)); + for (i = 0; i < stats->EndpointsLen; i++) + { + ep = &stats->Endpoints[i]; + IFOK(s, natsBuf_AppendByte(buf, '{')); + IFOK_attr("name", ep->Name, ","); + IFOK_attr("subject", ep->Subject, ","); + IFOK(s, nats_marshalLong(buf, false, "num_requests", ep->NumRequests)); + IFOK(s, nats_marshalLong(buf, true, "num_errors", ep->NumErrors)); + IFOK(s, nats_marshalDuration(buf, true, "average_processing_time", ep->AverageProcessingTimeNanoseconds)); + IFOK(s, natsBuf_AppendByte(buf, ',')); + IFOK_attr("last_error", ep->LastErrorString, ""); + IFOK(s, natsBuf_AppendByte(buf, '}')); + + if (i < (stats->EndpointsLen - 1)) + IFOK(s, natsBuf_AppendByte(buf, ',')); + } + IFOK(s, natsBuf_Append(buf, "],", 2)); + } + + IFOK_attr("version", stats->Version, ""); + IFOK(s, natsBuf_AppendByte(buf, '}')); + + if (s == NATS_OK) + { + *new_buf = buf; + return NULL; + } + else + { + natsBuf_Destroy(buf); + return microError_Wrapf(micro_ErrorFromStatus(s), "failed to marshal service info"); + } +} diff --git a/src/micro_request.c b/src/micro_request.c new file mode 100644 index 000000000..72159b7e3 --- /dev/null +++ b/src/micro_request.c @@ -0,0 +1,201 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "microp.h" + +microError * +microRequest_Respond(microRequest *req, const char *data, size_t len) +{ + return microRequest_RespondCustom(req, NULL, data, len); +} + +microError * +microRequest_RespondError(microRequest *req, microError *err) +{ + return microRequest_RespondCustom(req, err, NULL, 0); +} + +microError * +microRequest_RespondCustom(microRequest *req, microError *service_error, const char *data, size_t len) +{ + natsMsg *msg = NULL; + natsStatus s = NATS_OK; + char buf[64]; + + if ((req == NULL) || (req->Message == NULL) || (req->Message->sub == NULL) || (req->Message->sub->conn == NULL)) + { + s = NATS_INVALID_ARG; + } + if (s == NATS_OK) + { + s = natsMsg_Create(&msg, natsMsg_GetReply(req->Message), NULL, data, (int)len); + } + if ((s == NATS_OK) && (service_error != NULL)) + { + micro_update_last_error(req->Endpoint, service_error); + if (service_error->status != NATS_OK) + { + s = natsMsgHeader_Set(msg, MICRO_STATUS_HDR, natsStatus_GetText(service_error->status)); + } + if (s == NATS_OK) + { + s = natsMsgHeader_Set(msg, MICRO_ERROR_HDR, service_error->message); + } + if (s == NATS_OK) + { + snprintf(buf, sizeof(buf), "%u", service_error->code); + s = natsMsgHeader_Set(msg, MICRO_ERROR_CODE_HDR, buf); + } + } + if (s == NATS_OK) + { + s = natsConnection_PublishMsg(req->Message->sub->conn, msg); + } + + microError_Destroy(service_error); + natsMsg_Destroy(msg); + return microError_Wrapf( + micro_ErrorFromStatus(s), + "microRequest_RespondErrorWithData failed"); +} + +microError * +microRequest_AddHeader(microRequest *req, const char *key, const char *value) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Add(microRequest_GetMsg(req), key, value)); +} + +microError * +microRequest_DeleteHeader(microRequest *req, const char *key) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Delete(microRequest_GetMsg(req), key)); +} + +natsConnection * +microRequest_GetConnection(microRequest *req) +{ + return ((req != NULL) && (req->Service != NULL)) ? req->Service->nc : NULL; +} + +const char * +microRequest_GetData(microRequest *req) +{ + return natsMsg_GetData(microRequest_GetMsg(req)); +} + +int microRequest_GetDataLength(microRequest *req) +{ + return natsMsg_GetDataLength(microRequest_GetMsg(req)); +} + +microEndpoint * +microRequest_GetEndpoint(microRequest *req) +{ + return (req != NULL) ? req->Endpoint : NULL; +} + +microError * +microRequest_GetHeaderKeys(microRequest *req, const char ***keys, int *count) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Keys(microRequest_GetMsg(req), keys, count)); +} + +microError * +microRequest_GetHeaderValue(microRequest *req, const char *key, const char **value) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Get(microRequest_GetMsg(req), key, value)); +} + +microError * +microRequest_GetHeaderValues(microRequest *req, const char *key, const char ***values, int *count) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Values(microRequest_GetMsg(req), key, values, count)); +} + +natsMsg * +microRequest_GetMsg(microRequest *req) +{ + return (req != NULL) ? req->Message : NULL; +} + +const char * +microRequest_GetReply(microRequest *req) +{ + return natsMsg_GetReply(microRequest_GetMsg(req)); +} + +const char *microRequest_GetSubject(microRequest *req) +{ + return natsMsg_GetSubject(microRequest_GetMsg(req)); +} + +void *microRequest_GetServiceState(microRequest *req) +{ + if ((req == NULL) || (req->Service == NULL) || (req->Service->cfg == NULL)) + { + return NULL; + } + return req->Service->cfg->State; +} + +void *microRequest_GetEndpointState(microRequest *req) +{ + if ((req == NULL) || (req->Endpoint == NULL) || (req->Endpoint->config == NULL)) + { + return NULL; + } + return req->Endpoint->config->State; +} + +microError * +microRequest_SetHeader(microRequest *req, const char *key, const char *value) +{ + return micro_ErrorFromStatus( + natsMsgHeader_Set(microRequest_GetMsg(req), key, value)); +} + +microService * +microRequest_GetService(microRequest *req) +{ + return (req != NULL) ? req->Service : NULL; +} + +void micro_free_request(microRequest *req) +{ + NATS_FREE(req); +} + +microError * +micro_new_request(microRequest **new_request, microService *m, microEndpoint *ep, natsMsg *msg) +{ + microRequest *req = NULL; + + // endpoint is optional, service and message references are required. + if ((new_request == NULL) || (m == NULL) || (msg == NULL)) + return micro_ErrorInvalidArg; + + req = (microRequest *)NATS_CALLOC(1, sizeof(microRequest)); + if (req == NULL) + return micro_ErrorOutOfMemory; + + req->Message = msg; + req->Service = m; + req->Endpoint = ep; + *new_request = req; + return NULL; +} diff --git a/src/microp.h b/src/microp.h new file mode 100644 index 000000000..75b90646a --- /dev/null +++ b/src/microp.h @@ -0,0 +1,187 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MICROP_H_ +#define MICROP_H_ + +#include "natsp.h" +#include "mem.h" + +#define MICRO_CALL(__err, __call) \ + if ((__err) == NULL) \ + { \ + __err = (__call); \ + } + +#define MICRO_DO(__err, __block) \ + if ((__err) == NULL) \ + __block; + +#define MICRO_QUEUE_GROUP "q" + +#define MICRO_DEFAULT_ENDPOINT_NAME "default" + +struct micro_error_s +{ + bool is_internal; + struct micro_error_s *cause; + natsStatus status; + int code; + const char *message; +}; + +struct micro_client_s +{ + natsConnection *nc; +}; + +struct micro_endpoint_s +{ + // The name and subject that the endpoint is listening on (may be different + // from one specified in config). + char *name; + char *subject; + + // A copy of the config provided to add_endpoint. + microEndpointConfig *config; + + // Retained/released by the service that owns the endpoint to avoid race + // conditions. + microService *m; + + // Monitoring endpoints are different in a few ways. For now, express it as + // a single flag but consider unbundling: + // - use_queue_group: Service endpoints use a queue group, monitoring + // endpoints don't. + // - forward_response_errors_to_async_handler: Service endpoints handle + // respond errors themselves, standard monitoring endpoints don't, so + // send the errors to the service async handler, if exists. + // - gather_stats: Monitoring endpoints don't need stats. + // - include_in_info: Monitoring endpoints are not listed in INFO + // responses. + bool is_monitoring_endpoint; + + // Mutex for starting/stopping the endpoint, and for updating the stats. + natsMutex *endpoint_mu; + int refs; + bool is_draining; + + // The subscription for the endpoint. If NULL, the endpoint is stopped. + natsSubscription *sub; + + // Endpoint stats. These are initialized only for running endpoints, and are + // cleared if the endpoint is stopped. + microEndpointStats stats; + + microEndpoint *next; +}; + +struct micro_group_s +{ + struct micro_service_s *m; + struct micro_group_s *next; + char prefix[]; +}; + +struct micro_service_s +{ + // these are set at initialization time time and do not change. + natsConnection *nc; + microServiceConfig *cfg; + char id[NUID_BUFFER_LEN + 1]; + + // groups are just convenient wrappers to make "prefixed" endpoints with + // AddEndpoint. They are added at initializaton time, so no need to lock. + struct micro_group_s *groups; + + // these are are updated concurrently with access as the service runs, so + // need to be protected by mutex. + natsMutex *service_mu; + int refs; + + struct micro_endpoint_s *first_ep; + + int64_t started; // UTC time expressed as number of nanoseconds since epoch. + bool stopped; +}; + +/** + * A microservice request. + * + * microRequest represents a request received by a microservice endpoint. + */ +struct micro_request_s +{ + /** + * @brief The NATS message underlying the request. + */ + natsMsg *Message; + + /** + * @brief A reference to the service that received the request. + */ + microService *Service; + + /** + * @brief A reference to the service that received the request. + */ + microEndpoint *Endpoint; +}; + +extern microError *micro_ErrorOutOfMemory; +extern microError *micro_ErrorInvalidArg; + +microError *micro_add_endpoint(microEndpoint **new_ep, microService *m, const char *prefix, microEndpointConfig *cfg, bool is_internal); +microError *micro_clone_endpoint_config(microEndpointConfig **out, microEndpointConfig *cfg); +microError *micro_clone_metadata(const char ***new_metadata, int *new_len, const char **metadata, int len); +microError *micro_init_monitoring(microService *m); +microError *micro_is_error_message(natsStatus s, natsMsg *msg); +microError *micro_new_control_subject(char **newSubject, const char *verb, const char *name, const char *id); +microError *micro_new_endpoint(microEndpoint **new_ep, microService *m, const char *prefix, microEndpointConfig *cfg, bool is_internal); +microError *micro_new_request(microRequest **new_request, microService *m, microEndpoint *ep, natsMsg *msg); +microError *micro_start_endpoint(microEndpoint *ep); +microError *micro_stop_endpoint(microEndpoint *ep); + +void micro_free_cloned_endpoint_config(microEndpointConfig *cfg); +void micro_free_cloned_metadata(const char **metadata, int len); +void micro_free_endpoint(microEndpoint *ep); +void micro_free_request(microRequest *req); +void micro_release_endpoint(microEndpoint *ep); +void micro_release_on_endpoint_complete(void *closure); +void micro_retain_endpoint(microEndpoint *ep); +void micro_update_last_error(microEndpoint *ep, microError *err); + +bool micro_is_valid_name(const char *name); +bool micro_is_valid_subject(const char *subject); +bool micro_match_endpoint_subject(const char *ep_subject, const char *actual_subject); + +static inline void micro_lock_endpoint(microEndpoint *ep) { natsMutex_Lock(ep->endpoint_mu); } +static inline void micro_unlock_endpoint(microEndpoint *ep) { natsMutex_Unlock(ep->endpoint_mu); } + +static inline microError * +micro_strdup(char **ptr, const char *str) +{ + // Make a strdup(NULL) be a no-op, so we don't have to check for NULL + // everywhere. + if (str == NULL) + { + *ptr = NULL; + return NULL; + } + *ptr = NATS_STRDUP(str); + if (*ptr == NULL) + return micro_ErrorOutOfMemory; + return NULL; +} + +#endif /* MICROP_H_ */ diff --git a/src/nats.c b/src/nats.c index d1b2c7852..465491d77 100644 --- a/src/nats.c +++ b/src/nats.c @@ -1,4 +1,4 @@ -// Copyright 2015-2021 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -123,6 +123,11 @@ typedef struct __natsLib natsGCList gc; + // For micro services code + natsMutex *service_callback_mu; + // uses `microService*` as the key and the value. + natsHash *all_services_to_callback; + } natsLib; int64_t gLockSpinCount = 2000; @@ -332,6 +337,8 @@ _freeLib(void) _freeGC(); _freeDlvWorkers(); natsNUID_free(); + natsMutex_Destroy(gLib.service_callback_mu); + natsHash_Destroy(gLib.all_services_to_callback); natsCondition_Destroy(gLib.cond); @@ -763,8 +770,13 @@ _asyncCbsThread(void *arg) switch (cb->type) { case ASYNC_CLOSED: - (*(nc->opts->closedCb))(nc, nc->opts->closedCbClosure); + { + (*(nc->opts->closedCb))(nc, nc->opts->closedCbClosure); + if (nc->opts->microClosedCb != NULL) + (*(nc->opts->microClosedCb))(nc, NULL); break; + } + case ASYNC_DISCONNECTED: (*(nc->opts->disconnectedCb))(nc, nc->opts->disconnectedCbClosure); break; @@ -784,7 +796,10 @@ _asyncCbsThread(void *arg) { if (cb->errTxt != NULL) nats_setErrStatusAndTxt(cb->err, cb->errTxt); + (*(nc->opts->asyncErrCb))(nc, cb->sub, cb->err, nc->opts->asyncErrCbClosure); + if (nc->opts->microAsyncErrCb != NULL) + (*(nc->opts->microAsyncErrCb))(nc, cb->sub, cb->err, NULL); break; } #if defined(NATS_HAS_STREAMING) @@ -1048,6 +1063,10 @@ nats_Open(int64_t lockSpinCount) if (gLib.dlvWorkers.workers == NULL) s = NATS_NO_MEMORY; } + if (s == NATS_OK) + s = natsMutex_Create(&gLib.service_callback_mu); + if (s == NATS_OK) + s = natsHash_Create(&gLib.all_services_to_callback, 8); if (s == NATS_OK) gLib.initialized = true; @@ -1992,3 +2011,38 @@ natsLib_getMsgDeliveryPoolInfo(int *maxSize, int *size, int *idx, natsMsgDlvWork *workersArray = workers->workers; natsMutex_Unlock(workers->lock); } + +natsStatus +natsLib_startServiceCallbacks(microService *m) +{ + natsStatus s; + + natsMutex_Lock(gLib.service_callback_mu); + s = natsHash_Set(gLib.all_services_to_callback, (int64_t)m, (void *)m, NULL); + natsMutex_Unlock(gLib.service_callback_mu); + + return NATS_UPDATE_ERR_STACK(s); +} + +void +natsLib_stopServiceCallbacks(microService *m) +{ + if (m == NULL) + return; + + natsMutex_Lock(gLib.service_callback_mu); + natsHash_Remove(gLib.all_services_to_callback, (int64_t)m); + natsMutex_Unlock(gLib.service_callback_mu); +} + +natsMutex* +natsLib_getServiceCallbackMutex(void) +{ + return gLib.service_callback_mu; +} + +natsHash* +natsLib_getAllServicesToCallback(void) +{ + return gLib.all_services_to_callback; +} diff --git a/src/nats.h b/src/nats.h index 5b53a29a5..d4b7c2265 100644 --- a/src/nats.h +++ b/src/nats.h @@ -1,4 +1,4 @@ -// Copyright 2015-2022 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -7104,6 +7104,1258 @@ kvStatus_Destroy(kvStatus *sts); /** @} */ // end of funcGroup +// +// Microservices. +// + +/** \defgroup microGroup EXPERIMENTAL - Microservices + * + * \warning EXPERIMENTAL FEATURE! We reserve the right to change the API without + * necessarily bumping the major version of the library. + * + * ### NATS Microservices. + * + * Microservices can expose one or more request-response endpoints that process + * incoming NATS messages. + * + * Microservices are created by calling micro_AddService, and configured by + * passing a microServiceConfig to it. Many microservices can share a single + * connection to a NATS server. + * + * Once created, a microservice will subscribe to all endpoints' subjects and + * associate them with the configured handlers. It will also subscribe to and + * service monitoring subjects for service-specific pings, metadata, and + * statistics requests. The endpoint subscriptions are created with a queue + * group, so that incoming requests are automatically load-balanced across all + * running instances of a microservice. The monitoring subscriptions are not + * groupped, each service instance receives and responds to all monitoring + * requests. + * + * Once created, the microservice is asyncronous, message handlers and other + * callbacks will be invoked in separate threads. No further action is needed. + * Your program can use microService_Stop, microService_IsStopped to control the + * execution of the service. + * + * @{ + */ + +/** \defgroup microTypes Types + * + * Microservice types. + * + * @{ + */ + +/** + * @brief The Microservice client. + * + * Initialize with #micro_NewClient and destroy with #microClient_Destroy. + * + * @see micro_NewClient, microClient_Destroy + */ +typedef struct micro_client_s microClient; + +/** + * @brief The Microservice configuration object. For forward compatibility only. + */ +typedef struct __for_forward_compatibility_only microClientConfig; + +/** + * @brief `microEndpoint` represents a microservice endpoint. + * + * The only place where this struct is used by the user is in callbacks, to + * identify which endpoint was called, or caused an error. + * + * @see microRequestHandler, microErrorHandler, microServiceConfig, + * microEndpointConfig + */ +typedef struct micro_endpoint_s microEndpoint; + +/** + * @brief The Microservice endpoint configuration object. + * + * @see micro_endpoint_config_s for descriptions of the fields, + * micro_service_config_s, microServiceConfig, microService_AddEndpoint, + * microGroup_AddEndpoint + */ +typedef struct micro_endpoint_config_s microEndpointConfig; + +/** + * @brief static information about an endpoint. + * + * microEndpointInfo is returned by microService_GetInfo function, as part of microServiceInfo. It + * is also accessible by sending a `$SRV.INFO.[.]` request to + * the service. See micro_endpoint_info_s for descriptions of the fields. + * + * @see micro_endpoint_info_s, micro_service_info_s, microService_GetInfo + */ +typedef struct micro_endpoint_info_s microEndpointInfo; + +/** + * @brief The Microservice endpoint-level stats struct. + * + * Returned as part of microEndpointStats. See micro_endpoint_stats_s for + * descriptions of the fields. + * + * @see micro_endpoint_stats_s, microServiceStats, microService_GetStats + */ +typedef struct micro_endpoint_stats_s microEndpointStats; + +/** + * @brief the Microservice error object. + * + * This error type is returned by most microservice functions. You can create + * your own custom errors by using #micro_Errorf and wrap existing errors using + * #microError_Wrapf. Errors are heap-allocated and must be freed with either + * #microError_Destroy or by passing it into #microRequest_Respond. + * + * There are no public fields in this struct, use #microError_Code, + * #microError_Status, and #microError_String to get more information about the + * error. + */ +typedef struct micro_error_s microError; + +/** + * @brief a collection of endpoints and other groups, with a + * common prefix to their subjects and names. + * + * It has no other purpose than + * convenience, for organizing endpoint subject space. + */ +typedef struct micro_group_s microGroup; + +/** + * @brief a request received by a microservice endpoint. + * + * @see micro_request_s for descriptions of the fields. + */ +typedef struct micro_request_s microRequest; + +/** + * @brief the main object for a configured microservice. + * + * It can be created with #micro_AddService, and configured by passing a + * microServiceConfig to it. Once no longer needed, a microservice should be + * destroyed with microService_Destroy. + * + * @see micro_AddService, microServiceConfig, microEndpointConfig, + * microService_Destroy, microService_Stop, microService_IsStopped, + * microService_Run + */ +typedef struct micro_service_s microService; + +/** + * @brief The microservice configuration object. + * + * The service is created with a clone of the config and all of its values, so + * the original can be freed or modified after calling #micro_AddService. See + * micro_service_config_s for descriptions of the fields. + * + * @see micro_service_config_s + */ +typedef struct micro_service_config_s microServiceConfig; + +/** + * @brief Information about a running microservice. + * + * microServiceInfo is the struct returned by microService_GetInfo function. It + * is also accessible by sending a `$SRV.INFO.[.]` request to + * the service. See micro_service_info_s for descriptions of the fields. + * + * @see micro_service_info_s, microService_GetInfo + */ +typedef struct micro_service_info_s microServiceInfo; + +/** + * @brief The Microservice service-level stats struct. + * + * @see micro_service_stats_s for descriptions of the fields, + * microService_GetStats + */ +typedef struct micro_service_stats_s microServiceStats; + +/** @} */ // end of microTypes + +/** \defgroup microCallbacks Callbacks + * + * Microservice callbacks. + * @{ + */ + +/** + * @brief Callback type for request processing. + * + * This is the callback that one provides when creating a microservice endpoint. + * The library will invoke this callback for each message arriving to the + * specified subject. + * + * @param req The request object, containing the message and other relevant + * references. + * + * @see microEndpointConfig, micro_endpoint_config_s. + */ +typedef microError *(*microRequestHandler)(microRequest *req); + +/** + * @brief Callback type for async error notifications. + * + * If specified in microServiceConfig, this callback is invoked for internal + * errors (e.g. message delivery failures) related to a microservice. If the + * error is associated with an endpoint, the ep parameter points at the + * endpoint. However, this handler may be invoked for errors happening in + * monitoring subjects, in which case ep is NULL. + * + * The error handler is invoked asynchronously, in a separate theread. + * + * The error handler is not invoked for microservice-level errors that are sent + * back to the client as responses. Note that the error counts in + * microEndpointStats include both internal and service-level errors. + * + * @param m The microservice object. + * @param ep The endpoint object, or NULL if the error is not associated with an + * endpoint. + * @param s The NATS status for the error. + * + * @see microServiceConfig, micro_service_config_s. + */ +typedef void (*microErrorHandler)(microService *m, microEndpoint *ep, natsStatus s); + +/** + * @brief Callback type for `Done` (service stopped) notifications. + * + * If specified in microServiceConfig, this callback is invoked right after the + * service stops. In the C client, this callback is invoked directly from the + * microService_Stop function, in whatever thread is executing it. + * + * @param m The microservice object. + * + * @see microServiceConfig, micro_service_config_s. + */ +typedef void (*microDoneHandler)(microService *m); + +/** @} */ // end of microCallbacks + +/** \defgroup microStructs Public structs + * + * Microservice public structs. + * + * @{ + */ + +/** + * The Microservice endpoint configuration object. + */ +struct micro_endpoint_config_s +{ + /** + * @brief The name of the endpoint. + * + * Used in the service stats to list endpoints by name. Must not be empty. + */ + const char *Name; + + /** + * @brief The NATS subject the endpoint will listen on. + * + * Wildcards are allowed. If `Subject` is empty, it attempts to default to + * `Name`, provided it is a valid subject. + * + * For endpoints added to a group, the subject is automatically prefixed + * with the group's prefix. + */ + const char *Subject; + + /** + * @brief Metadata for the endpoint in the form of a string array [n1, v1, + * n2, v2, ...] representing key/value pairs {n1:v1, n2:v2, ...}. + * MetadataLen contains the number of **pairs** in Metadata. + */ + const char **Metadata; + int MetadataLen; + + /** + * @brief The request handler for the endpoint. + */ + microRequestHandler Handler; + + /** + * @brief A user-provided pointer to store with the endpoint + * (state/closure). + */ + void *State; +}; + +/** + * microEndpointInfo is the struct for the endpoint's static metadata. + */ +struct micro_endpoint_info_s +{ + /** + * @brief The name of the service. + */ + const char *Name; + + /** + * @brief The semantic version of the service. + */ + const char *Subject; + + /** + * @brief The metadata for the endpoint in the form of a string array [n1, + * v1, n2, v2, ...] representing key/value pairs {n1:v1, n2:v2, ...}. + * MetadataLen contains the number of **pairs** in Metadata. + */ + const char **Metadata; + int MetadataLen; +}; + +/** + * The Microservice endpoint stats struct. + */ +struct micro_endpoint_stats_s +{ + const char *Name; + const char *Subject; + + /** + * @brief The number of requests received by the endpoint. + */ + int64_t NumRequests; + + /** + * @brief The number of errors, service-level and internal, associated with + * the endpoint. + */ + int64_t NumErrors; + + /** + * @brief total request processing time (the seconds part). + */ + int64_t ProcessingTimeSeconds; + + /** + * @brief total request processing time (the nanoseconds part). + */ + int64_t ProcessingTimeNanoseconds; + + /** + * @brief average request processing time, in ns. + */ + int64_t AverageProcessingTimeNanoseconds; + + /** + * @brief a copy of the last error message. + */ + char LastErrorString[2048]; +}; + +/** + * @brief The Microservice top-level configuration object. + * + * The service is created with a clone of the config and all of its values, so + * the original can be freed or modified after calling micro_AddService. + */ +struct micro_service_config_s +{ + /** + * @brief The name of the service. + * + * It can be used to compose monitoring messages specific to this service. + */ + const char *Name; + + /** + * @brief The (semantic) version of the service. + */ + const char *Version; + + /** + * @brief The description of the service. + */ + const char *Description; + + /** + * @brief Metadata for the service in the form of a string array [n1, v1, + * n2, v2, ...] representing key/value pairs {n1:v1, n2:v2, ...}. + * MetadataLen contains the number of **pairs** in Metadata. + */ + const char **Metadata; + int MetadataLen; + + /** + * @brief The "main" (aka default) endpoint configuration. + * + * It is the default in that it does not require calling + * microService_AddEndpoint, it is added automatically when creating the + * service. + */ + microEndpointConfig *Endpoint; + + /** + * @brief A custom stats handler. + * + * It will be called to output the service's stats. It replaces the default + * stats handler but can pull the service stats using microService_GetStats + * function, then marshal them itself, as appropriate. + */ + microRequestHandler StatsHandler; + + /** + * @brief An error notification handler. + * + * It will be called asynchonously upon internal errors. It does not get + * called for application-level errors, successfully sent out by the + * microservice. + */ + microErrorHandler ErrHandler; + + /** + * @brief A callback handler for handling the final cleanup `Done` event, + * right before the service is destroyed. + * + * It will be called directly from #microService_Stop method, so it may be + * executed in any of the user threads or in the async callback thread if + * the service stops itself on connection closed or an error event. + */ + microDoneHandler DoneHandler; + + /** + * @brief A user-provided pointer to state data. + * + * A closure that is accessible from the request, stats, and internal event + * handlers. Please note that handlers are invoked on separate threads, + * consider thread-safe mechanisms of accessing the data. + */ + void *State; +}; + +/** + * microServiceInfo is the struct returned by microService_GetInfo function. It + * is also accessible by sending a `$SRV.INFO.[.]` request to + * the service. + */ +struct micro_service_info_s +{ + /** + * @brief Response type. Always `"io.nats.micro.v1.info_response"`. + */ + const char *Type; + + /** + * @brief The name of the service. + */ + const char *Name; + + /** + * @brief The semantic version of the service. + */ + const char *Version; + + /** + * @brief The description of the service. + */ + const char *Description; + + /** + * @brief The ID of the service instance responding to the request. + */ + const char *Id; + + /** + * @brief The service metadata in the form of a string array [n1, v1, n2, + * v2, ...] representing key/value pairs {n1:v1, n2:v2, ...}. MetadataLen + * contains the number of **pairs** in Metadata. + */ + const char **Metadata; + int MetadataLen; + + /** + * @brief Endpoints. + */ + microEndpointInfo *Endpoints; + + /** + * @brief The number of endpoints in the `Endpoints` array. + */ + int EndpointsLen; +}; + +/** + * The Microservice stats struct. + */ +struct micro_service_stats_s +{ + /** + * @brief Response type. Always `"io.nats.micro.v1.stats_response"`. + */ + const char *Type; + + /** + * @brief The name of the service. + */ + const char *Name; + + /** + * @brief The semantic version of the service. + */ + const char *Version; + + /** + * @brief The ID of the service instance responding to the request. + */ + const char *Id; + + /** + * @brief The timestamp of when the service was started. + */ + int64_t Started; + + /** + * @brief The stats for each endpoint of the service. + */ + microEndpointStats *Endpoints; + + /** + * @brief The number of endpoints in the `endpoints` array. + */ + int EndpointsLen; +}; + +/** @} */ // end of microStructs + +/** \defgroup microConstants Public constants + * + * Microservice public constants. + * @{ + */ + +/** + * @brief The prefix for all microservice monitoring subjects. + * + * For example, `"$SRV.PING"`. + */ +#define MICRO_API_PREFIX "$SRV" + +/** + * @brief The `type` set in the `$SRV.INFO` responses. + */ +#define MICRO_INFO_RESPONSE_TYPE "io.nats.micro.v1.info_response" + +/** + * @brief For `$SRV.INFO.*` subjects. + */ +#define MICRO_INFO_VERB "INFO" + +/** + * @brief The `type` set in the `$SRV.PING` response. + */ +#define MICRO_PING_RESPONSE_TYPE "io.nats.micro.v1.ping_response" + +/** + * @brief For `$SRV.PING` subjects. + */ +#define MICRO_PING_VERB "PING" + +/** + * @brief The `type` set in the `STATS` response. + */ +#define MICRO_STATS_RESPONSE_TYPE "io.nats.micro.v1.stats_response" + +/** + * @brief The "verb" used in `$SRV.STATS` subjects. + */ +#define MICRO_STATS_VERB "STATS" + +/** + * @brief The response message header used to communicate an erroneous NATS + * status back to the requestor. + */ +#define MICRO_STATUS_HDR "Nats-Status" + +/** + * @brief The response message header used to communicate an error message back + * to the requestor. + */ +#define MICRO_ERROR_HDR "Nats-Service-Error" + +/** + * @brief The response message header used to communicate an integer error code + * back to the requestor. + */ +#define MICRO_ERROR_CODE_HDR "Nats-Service-Error-Code" + +/** @} */ // end of microConstants + +/** \defgroup microFunctions Functions + * + * Microservice functions. + * @{ + */ + +/** \defgroup microServiceFunctions microService + * + * Functions that operate with #microService. + * @{ + */ + +/** @brief Creates and starts a new microservice. + * + * @note The microservice should be destroyed to clean up using + * #microService_Destroy. + * + * @param new_microservice the location where to store the pointer to the new + * #microService object. + * @param nc the #natsConnection the service will use to receive and respond to + * requests. + * @param config a #microServiceConfig object with the configuration of the + * service. See #micro_service_config_s for descriptions of the fields. + * + * @return a #microError if an error occurred. + * + * @see #microService_Destroy, #microService_AddEndpoint, #microServiceConfig, + * #microEndpointConfig + */ +NATS_EXTERN microError * +micro_AddService(microService **new_microservice, natsConnection *nc, microServiceConfig *config); + +/** @brief Adds an endpoint to a microservice and starts listening for messages. + * + * Endpoints are currently destroyed when the service is stopped, there is no + * present way to remove or stop individual endpoints. + * + * @param m the #microService that the endpoint will be added to. + * @param config a #microEndpointConfig object with the configuration of the + * endpoint. See #micro_endpoint_config_s for descriptions of the fields. + * + * @return a #microError if an error occurred. + * + * @see #microService_Destroy, #microEndpointConfig + */ +NATS_EXTERN microError * +microService_AddEndpoint(microService *m, microEndpointConfig *config); + +/** @brief Adds an group (prefix) to a microservice. + * + * Groups are associated with a service, and are destroyed when the service is + * destroyed. + * + * @param new_group the location where to store the pointer to the new + * #microGroup object. + * @param m the #microService that the group will be added to. + * @param prefix a prefix to use on names and subjects of all endpoints in the + * group. + * + * @return a #microError if an error occurred. + * + * @see #microGroup_AddGroup, #microGroup_AddEndpoint + */ +NATS_EXTERN microError * +microService_AddGroup(microGroup **new_group, microService *m, const char *prefix); + +/** @brief Destroys a microservice, stopping it first if needed. + * + * @note This function may fail while stopping the service, do not assume + * unconditional success if clean up is important. + * + * @param m the #microService to stop and destroy. + * + * @return a #microError if an error occurred. + * + * @see #microService_Stop, #microService_Run + */ +NATS_EXTERN microError * +microService_Destroy(microService *m); + +/** @brief Returns the connection associated with the service. If the service + * was successfully started, it is safe to assume it's not NULL, however it may + * already have been disconnected or closed. + * + * @param m the #microService. + * + * @return a #natsConnection associated with the service. + */ +NATS_EXTERN natsConnection * +microService_GetConnection(microService *m); + +/** @brief Returns a #microServiceInfo for a microservice. + * + * @note the #microServiceInfo struct returned by this call should be freed with + * #microServiceInfo_Destroy. + * + * @param new_info receives the pointer to the #microServiceInfo. + * @param m the #microService to query. + * + * @return a #microError if an error occurred. + * + * @see #microServiceInfo, #micro_service_info_s + */ +NATS_EXTERN microError * +microService_GetInfo(microServiceInfo **new_info, microService *m); + +/** @brief Returns the pointer to state data (closure). It is originally + * provided in #microServiceConfig.State. + * + * @param m the #microService. + * + * @return the state pointer. + * + * @see #microServiceConfig + */ +NATS_EXTERN void * +microService_GetState(microService *m); + +/** @brief Returns run-time statistics for a microservice. + * + * @note the #microServiceStats struct returned by this call should be freed + * with #microServiceStats_Destroy. + * + * @param new_stats receives the pointer to the #microServiceStats. + * @param m the #microService to query. + * + * @return a #microError if an error occurred. + * + * @see #microServiceStats, #micro_service_stats_s + */ +NATS_EXTERN microError * +microService_GetStats(microServiceStats **new_stats, microService *m); + +/** @brief Checks if the service is stopped. + * + * @param m the #microService. + * + * @return `true` if stopped, otherwise `false`. + * + * @see #micro_AddService, #microService_Stop, #microService_Run + */ +NATS_EXTERN bool +microService_IsStopped(microService *m); + +/** @brief Waits for a microservice to stop. + * + * #micro_AddService starts the service with async subscriptions. + * #microService_Run waits for the service to stop. + * + * @param m the #microService. + * + * @return a #microError for invalid arguments, otherwise always succeeds. + * + * @see #micro_AddService, #microService_Stop + */ +NATS_EXTERN microError * +microService_Run(microService *m); + +/** @brief Stops a running microservice. + * + * Drains and closes the all subscriptions (endpoints and monitoring), resets + * the stats, and calls the `Done` callback for the service, so it can do its + * own clean up if needed. + * + * It is possible that this call encounters an error while stopping the service, + * in which case it aborts and returns the error. The service then may be in a + * partially stopped state, and the `Done` callback will not have been called. + * + * @param m the #microService. + * + * @return a #microError if an error occurred. + * + * @see #micro_AddService, #microService_Run + */ +NATS_EXTERN microError *microService_Stop(microService *m); + +/** @} */ // end of microServiceFunctions + +/** \defgroup microGroupFunctions microGroup + * + * Functions that operate with #microGroup. + * @{ + */ + +/** @brief Adds a sub-group to #microGroup. + * + * The new subgroup will be prefixed as "parent_prefix.prefix.". Groups are + * associated with a service, and are destroyed when the service is destroyed. + * + * @param new_group the location where to store the pointer to the new + * #microGroup object. + * @param parent the #microGroup that the new group will be added to. + * @param prefix a prefix to use on names and subjects of all endpoints in the + * group. + * + * @return a #microError if an error occurred. + * + * @see #microGroup_AddGroup, #microGroup_AddEndpoint + */ +NATS_EXTERN microError * +microGroup_AddGroup(microGroup **new_group, microGroup *parent, const char *prefix); + +/** @brief Adds an endpoint to a #microGroup and starts listening for messages. + * + * This is a convenience method to add an endpoint with its `Subject` and `Name` + * prefixed with the group's prefix. + * + * @param g the #microGroup that the endpoint will be added to. + * @param config a #microEndpointConfig object with the configuration of the + * endpoint. See #micro_endpoint_config_s for descriptions of the fields. + * + * @return a #microError if an error occurred. + * + * @see #microService_Destroy, #microService_AddEndpoint, #microEndpointConfig + */ +NATS_EXTERN microError * +microGroup_AddEndpoint(microGroup *g, microEndpointConfig *config); + +/** @} */ // end of microGroupFunctions + +/** \defgroup microRequestFunctions microRequest + * + * Functions that operate with #microRequest. + * @{ + */ + +/** @brief Adds a header to the underlying NATS request message. + * + * @param req the request. + * @param key the key under which the `value` will be stored. It can't ne `NULL` + * or empty. + * @param value the string to add to the values associated with the given `key`. + * The value can be `NULL` or empty string. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Add, #microRequest_DeleteHeader, #microRequest_GetHeaderKeys, + * #microRequest_GetHeaderValue, #microRequest_GetHeaderValues + */ +NATS_EXTERN microError * +microRequest_AddHeader(microRequest *req, const char *key, const char *value); + +/** @brief Deletes a header from the underlying NATS request message. + * + * @param req the request. + * @param key the key to delete from the headers map. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Delete, #microRequest_AddHeader + */ +NATS_EXTERN microError * +microRequest_DeleteHeader(microRequest *req, const char *key); + +/** @brief Returns the connection associated with the request. + * + * @param req the request. + * + * @return a #natsConnection associated with the request. In fact, it's the + * connection associated with the service, so by the time the request handler is + * invoked the connection state may be different from when it was received. + */ +NATS_EXTERN natsConnection * +microRequest_GetConnection(microRequest *req); + +/** @brief Returns the data in the the request, as a byte array. + * + * @note The request owns the data, so it should not be freed other than with + * `microRequest_Destroy`. + * @note The data is not `NULL` terminated. Use `microRequest_GetDataLength` to + * obtain the number of bytes. + * + * @param req the request. + * + * @return a pointer to the request's data. + * + * @see #natsMsg_GetData, #microRequest_GetDataLength + */ +NATS_EXTERN const char * +microRequest_GetData(microRequest *req); + +/** @brief Returns the number of data bytes in the the request. + * + * @param req the request. + * + * @return the number of data bytes in the request. + * + * @see #natsMsg_GetDataLength, #microRequest_GetData + */ +NATS_EXTERN int +microRequest_GetDataLength(microRequest *req); + +/** \brief Returns the pointer to the user-provided endpoint state, if + * the request is associated with an endpoint. + * + * @param req the request. + * + * @return the `state` pointer provided in microEndpointConfig. + * + * @see #microEndpointConfig, #micro_endpoint_config_s + */ +NATS_EXTERN void * +microRequest_GetEndpointState(microRequest *req); + +/** @brief Gets the list of all header keys in the NATS message underlying the + * request. + * + * The returned strings are own by the library and MUST not be freed or altered. + * However, the returned array `keys` MUST be freed by the user. + * + * @param req the request. + * @param keys the memory location where the library will store the pointer to + * the array of keys. + * @param count the memory location where the library will store the number of + * keys returned. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Keys, #microRequest_GetHeaderValue, #microRequest_GetHeaderValues + */ +NATS_EXTERN microError * +microRequest_GetHeaderKeys(microRequest *req, const char ***keys, int *count); + +/** @brief Get the header entry associated with `key` from the NATS message underlying the request. + * + * @param req the request. + * @param key the key for which the value is requested. + * @param value the memory location where the library will store the pointer to the first + * value (if more than one is found) associated with the `key`. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Get, #microRequest_GetHeaderValue, #microRequest_GetHeaderValues + */ +NATS_EXTERN microError * +microRequest_GetHeaderValue(microRequest *req, const char *key, const char **value); + +/** @brief Get all header values associated with `key` from the NATS message + * underlying the request. + * + * @param req the request. + * @param key the key for which the values are requested. + * @param values the memory location where the library will store the pointer to + * the array value (if more than one is found) associated with the `key`. + * @param count the memory location where the library will store the number of + * values returned. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Values, #microRequest_GetHeaderValue, + * #microRequest_GetHeaderKeys + */ +NATS_EXTERN microError * +microRequest_GetHeaderValues(microRequest *req, const char *key, const char ***values, int *count); + +/** @brief Get the NATS message underlying the request. + * + * @param req the request. + * + * @return the pointer to #natsMsg. + */ +NATS_EXTERN natsMsg * +microRequest_GetMsg(microRequest *req); + +/** @brief Returns the reply subject set in this message. + * + * Returns the reply, possibly `NULL`. + * + * @warning The string belongs to the message and must not be freed. + * Copy it if needed. + * + * @param req the request. + */ +NATS_EXTERN const char * +microRequest_GetReply(microRequest *req); + +/** @brief Returns the pointer to the microservice associated with the request. + * + * @param req the request. + * + * @return the microservice pointer. + */ +NATS_EXTERN microService * +microRequest_GetService(microRequest *req); + +/** @brief Returns the pointer to the user-provided service state. + * + * @param req the request. + * + * @return the `state` pointer provided in microServiceConfig. + * + * @see #microServiceConfig, #micro_service_config_s + */ +NATS_EXTERN void * +microRequest_GetServiceState(microRequest *req); + +/** @brief Returns the subject of the request message. + * + * @warning The string belongs to the message and must not be freed. + * Copy it if needed. + * + * @param req the request. + */ +NATS_EXTERN const char * +microRequest_GetSubject(microRequest *req); + +/** + * @brief Respond to a request, on the same NATS connection. + * + * @param req the request. + * @param data the response data. + * @param len the length of the response data. + * + * @return an error, if any. + */ +NATS_EXTERN microError * +microRequest_Respond(microRequest *req, const char *data, size_t len); + +/** + * @brief Respond to a request with a simple error. + * + * If err is NULL, `RespondError` does nothing. + * + * @note microRequest_RespondError is called automatially if the handler returns + * an error. Usually, there is no need for a handler to use this function + * directly. If the request + * + * @param req the request. + * @param err the error to include in the response header. If `NULL`, no error. + * + * @return an error, if any. + */ +NATS_EXTERN microError * +microRequest_RespondError(microRequest *req, microError *err); + +/** + * @brief Respond to a message, with an OK or an error. + * + * If err is NULL, `RespondErrorWithData` is equivalent to `Respond`. If err is + * not NULL, the response will include the error in the response header, and err + * will be freed. + * + * The following example illustrates idiomatic usage in a request handler. Since + * this handler handles its own error responses, the only error it might return + * would be a failure to send the response. + * + * \code{.c} + * err = somefunc(); + * if (err != NULL) { + * return microRequest_RespondCustom(req, err, error_data, data_len); + * } + * ... + * \endcode + * + * Or, if the request handler has its own cleanup logic: + * + * \code{.c} + * if (err = somefunc(), err != NULL) + * goto CLEANUP; + * ... + * + * CLEANUP: + * if (err != NULL) { + * return microRequest_RespondCustom(req, err, error_data, data_len); + * } + * return NULL; + * \endcode + * + * @param req the request. + * @param err the error to include in the response header. If `NULL`, no error. + * @param data the response data. + * @param len the length of the response data. + * + * @note + * + * + * @return an error, if any. + */ +NATS_EXTERN microError * +microRequest_RespondCustom(microRequest *req, microError *err, const char *data, size_t len); + +/** @brief Add `value` to the header associated with `key` in the NATS message + * underlying the request. + * + * @param req the request. + * @param key the key under which the `value` will be stored. It can't ne `NULL` + * or empty. + * @param value the string to store under the given `key`. The value can be + * `NULL` or empty string. + * + * @return a #microError if an error occurred. + * + * @see #natsMsgHeader_Set + */ +NATS_EXTERN microError * +microRequest_SetHeader(microRequest *req, const char *key, const char *value); + +/** @} */ // end of microRequestFunctions + +/** \defgroup microErrorFunctions microError + * + * Functions that create and manipulate #microError. + * @{ + */ + +/** @brief creates a new #microError, with a printf-like formatted message. + * + * @note Errors must be freed with #microError_Destroy, but often they are + * simply returned up the call stack. + * + * @param format printf-like format. + * + * @return a new #microError or micro_ErrorOutOfMemory. + */ +NATS_EXTERN microError * +micro_Errorf(const char *format, ...); + +/** @brief creates a new #microError, with a code and a printf-like formatted + * message. + * + * @note Errors must be freed with #microError_Destroy, but often they are + * simply returned up the call stack. + * + * @param code an `int` code, loosely modeled after the HTTP status code. + * @param format printf-like format. + * + * @return a new #microError or micro_ErrorOutOfMemory. + */ +NATS_EXTERN microError * +micro_ErrorfCode(int code, const char *format, ...); + +/** @brief Wraps a NATS status into a #microError, if not a NATS_OK. + * + * @param s NATS status of receiving the response. + * + * @return a new #microError or `NULL` if no error if found. + */ +NATS_EXTERN microError * +micro_ErrorFromStatus(natsStatus s); + +/** @brief returns the int code of the error. + * + * @param err the error. + * + * @return the int code. + */ +NATS_EXTERN int +microError_Code(microError *err); + +/** @brief destroys a #microError. + * + * @param err the error. + */ +NATS_EXTERN void +microError_Destroy(microError *err); + +#define microError_Ignore(__err) microError_Destroy(__err) + +/** + * @brief Returns the NATS status associated with the error. + * + * @param err + * + * @return the status + */ +NATS_EXTERN natsStatus +microError_Status(microError *err); + +/** + * @brief Returns a printable string with the error message. + * + * This function outputs into a user-provided buffer, and returns a "`%s`-able" + * pointer to it. + * + * @param err the error. + * @param buf the output buffer. + * @param len the capacity of the output buffer. + * @return `buf` + */ +NATS_EXTERN const char * +microError_String(microError *err, char *buf, size_t len); + +/** + * @brief Wraps an exising #microError with a higher printf-like formatted + * message. + * + * @warning The original error may be freed and should not be refered to after + * having been wrapped. + * + * @param err the original error + * @param format the new message to prepend to the original error message. + * @param ... + * + * @return a new, wrapped, error. + */ +NATS_EXTERN microError * +microError_Wrapf(microError *err, const char *format, ...); + +/** @} */ // end of microErrorFunctions + +/** \defgroup microClientFunctions microClient + * + * @{ + */ + +/** + * @brief Creates a new microservice client. + * + * @param new_client received the pointer to the new client. + * @param nc a NATS connection. + * @param cfg for future use, use NULL for now. + * + * @return a #microError if an error occurred. + */ +NATS_EXTERN microError * +micro_NewClient(microClient **new_client, natsConnection *nc, microClientConfig *cfg); + +/** + * @brief Destroys a microservice client. + * + * @param client the client to destroy. + */ +NATS_EXTERN void +microClient_Destroy(microClient *client); + +/** + * @brief Sends a request to a microservice and receives the response. + * + * @param reply receives the pointer to the response NATS message. `reply` must + * be freed with #natsMsg_Destroy. + * @param client the client to use. + * @param subject the subject to send the request on. + * @param data the request data. + * @param data_len the request data length. + * + * @return a #microError if an error occurred. + */ +NATS_EXTERN microError * +microClient_DoRequest(natsMsg **reply, microClient *client, const char *subject, const char *data, int data_len); + +/** @} */ // end of microClientFunctions + +/** \defgroup microCleanupFunctions Miscellaneous + * + * Functions to destroy miscellaneous objects. + * @{ + */ + +/** + * @brief Destroys a #microServiceInfo object. + * + * @param info the object to destroy. + */ +NATS_EXTERN void +microServiceInfo_Destroy(microServiceInfo *info); + +/** + * @brief Destroys a #microServiceStats object. + * + * @param stats the object to destroy. + */ +NATS_EXTERN void +microServiceStats_Destroy(microServiceStats *stats); + +/** @} */ // end of microCleanupFunctions + +/** @} */ // end of microFunctions + +/** @} */ // end of microGroup + /** \defgroup wildcardsGroup Wildcards * @{ * Use of wildcards. There are two type of wildcards: `*` for partial, diff --git a/src/natsp.h b/src/natsp.h index c98bbb283..a3dac236b 100644 --- a/src/natsp.h +++ b/src/natsp.h @@ -247,6 +247,9 @@ struct __natsOptions natsErrHandler asyncErrCb; void *asyncErrCbClosure; + natsConnectionHandler microClosedCb; + natsErrHandler microAsyncErrCb; + int64_t pingInterval; int maxPingsOut; int maxPendingMsgs; @@ -793,6 +796,18 @@ natsLib_getMsgDeliveryPoolInfo(int *maxSize, int *size, int *idx, natsMsgDlvWork void nats_setNATSThreadKey(void); +natsStatus +natsLib_startServiceCallbacks(microService *m); + +void +natsLib_stopServiceCallbacks(microService *m); + +natsMutex* +natsLib_getServiceCallbackMutex(void); + +natsHash* +natsLib_getAllServicesToCallback(void); + // // Threads // diff --git a/src/opts.c b/src/opts.c index ec730fbb0..8955d8d7d 100644 --- a/src/opts.c +++ b/src/opts.c @@ -914,6 +914,19 @@ natsOptions_SetClosedCB(natsOptions *opts, natsConnectionHandler closedCb, return NATS_OK; } +natsStatus +natsOptions_setMicroCallbacks(natsOptions *opts, natsConnectionHandler closed, natsErrHandler errHandler) +{ + LOCK_AND_CHECK_OPTIONS(opts, 0); + + opts->microClosedCb = closed; + opts->microAsyncErrCb = errHandler; + + UNLOCK_OPTS(opts); + + return NATS_OK; +} + natsStatus natsOptions_SetDisconnectedCB(natsOptions *opts, natsConnectionHandler disconnectedCb, diff --git a/src/opts.h b/src/opts.h index 3ee87e3e2..3955634e1 100644 --- a/src/opts.h +++ b/src/opts.h @@ -16,6 +16,16 @@ #include "natsp.h" +static inline void natsOptions_lock(natsOptions *opts) +{ + natsMutex_Lock(opts->mu); +} + +static inline void natsOptions_unlock(natsOptions *opts) +{ + natsMutex_Unlock(opts->mu); +} + #define LOCK_AND_CHECK_OPTIONS(o, c) \ if (((o) == NULL) || ((c))) \ return nats_setDefaultError(NATS_INVALID_ARG); \ @@ -38,4 +48,7 @@ natsOptions* natsOptions_clone(natsOptions *opts); +natsStatus +natsOptions_setMicroCallbacks(natsOptions *opts, natsConnectionHandler closedCb, natsErrHandler errCb); + #endif /* OPTS_H_ */ diff --git a/src/util.c b/src/util.c index 350774c38..7064f2d18 100644 --- a/src/util.c +++ b/src/util.c @@ -1,4 +1,4 @@ -// Copyright 2015-2021 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -2228,9 +2228,155 @@ nats_marshalULong(natsBuffer *buf, bool comma, const char *fieldName, uint64_t u return NATS_UPDATE_ERR_STACK(s); } +// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the +// tail of buf, omitting trailing zeros. It omits the decimal +// point too when the fraction is 0. It returns the index where the +// output bytes begin and the value v/10**prec. +static void +fmt_frac(char *buf, int w, uint64_t v, int prec, int *nw, uint64_t *nv) +{ + // Omit trailing zeros up to and including decimal point. + bool print = false; + int i; + int digit; -bool -nats_IsSubjectValid(const char *subject, bool wcAllowed) + for (i = 0; i < prec; i++) + { + digit = v % 10; + print = print || digit != 0; + if (print) + { + w--; + buf[w] = digit + '0'; + } + v /= 10; + } + if (print) + { + w--; + buf[w] = '.'; + } + *nw = w; + *nv = v; +} + +// fmtInt formats v into the tail of buf. +// It returns the index where the output begins. +static int +fmt_int(char *buf, int w, uint64_t v) +{ + if (v == 0) + { + w--; + buf[w] = '0'; + } + else + { + while (v > 0) + { + w--; + buf[w] = v % 10 + '0'; + v /= 10; + } + } + return w; +} + +natsStatus +nats_marshalDuration(natsBuffer *out_buf, bool comma, const char *field_name, int64_t d) +{ + // Largest time is 2540400h10m10.000000000s + char buf[32]; + int w = 32; + bool neg = d < 0; + uint64_t u = (uint64_t) (neg ? -d : d); + int prec; + natsStatus s = NATS_OK; + const char *start = (comma ? ",\"" : "\""); + + if (u < 1000000000) + { + // Special case: if duration is smaller than a second, + // use smaller units, like 1.2ms + w--; + buf[w] = 's'; + w--; + if (u == 0) + { + s = natsBuf_Append(out_buf, start, -1); + IFOK(s, natsBuf_Append(out_buf, field_name, -1)); + IFOK(s, natsBuf_Append(out_buf, "\":\"0s\"", -1)); + return NATS_UPDATE_ERR_STACK(s); + } + else if (u < 1000) + { + // print nanoseconds + prec = 0; + buf[w] = 'n'; + } + else if (u < 1000000) + { + // print microseconds + prec = 3; + // U+00B5 'ยต' micro sign == 0xC2 0xB5 (in reverse?) + buf[w] = '\xB5'; + w--; // Need room for two bytes. + buf[w] = '\xC2'; + } + else + { + // print milliseconds + prec = 6; + buf[w] = 'm'; + } + fmt_frac(buf, w, u, prec, &w, &u); + w = fmt_int(buf, w, u); + } + else + { + w--; + buf[w] = 's'; + + fmt_frac(buf, w, u, 9, &w, &u); + + // u is now integer seconds + w = fmt_int(buf, w, u % 60); + u /= 60; + + // u is now integer minutes + if (u > 0) + { + w--; + buf[w] = 'm'; + w = fmt_int(buf, w, u % 60); + u /= 60; + + // u is now integer hours + // Stop at hours because days can be different lengths. + if (u > 0) + { + w--; + buf[w] = 'h'; + w = fmt_int(buf, w, u); + } + } + } + + if (neg) + { + w--; + buf[w] = '-'; + } + + s = natsBuf_Append(out_buf, start, -1); + IFOK(s, natsBuf_Append(out_buf, field_name, -1)); + IFOK(s, natsBuf_Append(out_buf, "\":\"", -1)); + IFOK(s, natsBuf_Append(out_buf, buf + w, sizeof(buf) - w)); + IFOK(s, natsBuf_Append(out_buf, "\"", -1)); + return NATS_UPDATE_ERR_STACK(s); +} + +bool nats_IsSubjectValid(const char *subject, bool wcAllowed) { int i = 0; int len = 0; diff --git a/src/util.h b/src/util.h index d8a3f3b23..b7c156da6 100644 --- a/src/util.h +++ b/src/util.h @@ -1,4 +1,4 @@ -// Copyright 2015-2021 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -240,6 +240,9 @@ nats_marshalLong(natsBuffer *buf, bool comma, const char *fieldName, int64_t lva natsStatus nats_marshalULong(natsBuffer *buf, bool comma, const char *fieldName, uint64_t uval); +natsStatus +nats_marshalDuration(natsBuffer *out_buf, bool comma, const char *field_name, int64_t d); + bool nats_IsSubjectValid(const char *subject, bool wcAllowed); diff --git a/test/list.txt b/test/list.txt index 3fda01ac8..4c78d876d 100644 --- a/test/list.txt +++ b/test/list.txt @@ -258,6 +258,14 @@ KeyValueDiscardOldToNew KeyValueRePublish KeyValueMirrorDirectGet KeyValueMirrorCrossDomains +MicroMatchEndpointSubject +MicroAddService +MicroGroups +MicroBasics +MicroStartStop +MicroServiceStopsOnClosedConn +MicroServiceStopsWhenServerStops +MicroAsyncErrorHandler StanPBufAllocator StanConnOptions StanSubOptions diff --git a/test/test.c b/test/test.c index bf5659626..44d55a261 100644 --- a/test/test.c +++ b/test/test.c @@ -1,4 +1,4 @@ -// Copyright 2015-2022 The NATS Authors +// Copyright 2015-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -38,6 +38,7 @@ #include "parser.h" #include "js.h" #include "kv.h" +#include "microp.h" #if defined(NATS_HAS_STREAMING) #include "stan/conn.h" #include "stan/pub.h" @@ -118,6 +119,8 @@ struct threadArg int64_t reconnectedAt[4]; int reconnects; bool msgReceived; + int microRunningServiceCount; + bool microAllDone; bool done; int results[10]; const char *tokens[3]; @@ -14397,6 +14400,7 @@ test_AsyncErrHandler(void) _stopServer(serverPid); } + static void _asyncErrBlockingCb(natsConnection *nc, natsSubscription *sub, natsStatus err, void* closure) { @@ -14497,6 +14501,7 @@ test_AsyncErrHandlerSubDestroyed(void) test("Destroy subscription: "); natsSubscription_Destroy(sub); + testCond(true); test("Wait for async error callback to return: "); natsMutex_Lock(arg.m); @@ -16505,10 +16510,10 @@ test_ReconnectJitter(void) FAIL("Unable to setup test"); test("Default jitter values: "); - natsMutex_Lock(opts->mu); + natsOptions_lock(opts); s = (((opts->reconnectJitter == NATS_OPTS_DEFAULT_RECONNECT_JITTER) && (opts->reconnectJitterTLS == NATS_OPTS_DEFAULT_RECONNECT_JITTER_TLS)) ? NATS_OK : NATS_ERR); - natsMutex_Unlock(opts->mu); + natsOptions_unlock(opts); testCond(s == NATS_OK); s = natsOptions_SetURL(opts, "nats://127.0.0.1:4222"); @@ -20552,7 +20557,7 @@ test_EventLoop(void) test("Close and wait for close cb: "); natsConnection_Close(nc); - _waitForConnClosed(&arg); + s = _waitForConnClosed(&arg); testCond(s == NATS_OK); natsMutex_Lock(arg.m); @@ -32206,6 +32211,1129 @@ test_KeyValueMirrorCrossDomains(void) remove(lconfFile); } +static void +test_MicroMatchEndpointSubject(void) +{ + // endpoint, actual, match + const char *test_cases[] = { + "foo", "foo", "true", + "foo.bar.meh", "foo.bar.meh", "true", + "foo.bar.meh", "foo.bar", NULL, + "foo.bar", "foo.bar.meh", NULL, + "foo.*.meh", "foo.bar.meh", "true", + "*.bar.meh", "foo.bar.meh", "true", + "foo.bar.*", "foo.bar.meh", "true", + "foo.bar.>", "foo.bar.meh", "true", + "foo.>", "foo.bar.meh", "true", + "foo.b*ar.meh", "foo.bar.meh", NULL, + }; + char buf[1024]; + int i; + const char *ep_subject; + const char *actual_subject; + bool expected_match; + + for (i = 0; i < (int)(sizeof(test_cases) / sizeof(const char *)); i = i + 3) + { + ep_subject = test_cases[i]; + actual_subject = test_cases[i + 1]; + expected_match = (test_cases[i + 2] != NULL); + + snprintf(buf, sizeof(buf), "endpoint subject '%s', actual subject: '%s': ", ep_subject, actual_subject); + test(buf); + testCond(micro_match_endpoint_subject(ep_subject, actual_subject) == expected_match); + } +} + +static microError * +_microHandleRequest42(microRequest *req) +{ + return microRequest_Respond(req, "42", 2); +} + +static microError * +_microHandleRequestNoisy42(microRequest *req) +{ + if ((rand() % 10) == 0) + return micro_Errorf("Unexpected error!"); + + // Happy Path. + // Random delay between 5-10ms + nats_Sleep(5 + (rand() % 5)); + + return microRequest_Respond(req, "42", 2); +} + +static void +_microServiceDoneHandler(microService *m) +{ + struct threadArg *arg = (struct threadArg*) microService_GetState(m); + + natsMutex_Lock(arg->m); + arg->microRunningServiceCount--; + if (arg->microRunningServiceCount == 0) + { + arg->microAllDone = true; + natsCondition_Broadcast(arg->c); + } + natsMutex_Unlock(arg->m); +} + +static microError * +_startMicroservice(microService** new_m, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg) +{ + microError *err = NULL; + bool prev_done; + int i; + + cfg->DoneHandler = _microServiceDoneHandler; + cfg->State = arg; + + natsMutex_Lock(arg->m); + + arg->microRunningServiceCount++; + prev_done = arg->microAllDone; + arg->microAllDone = false; + + err = micro_AddService(new_m, nc, cfg); + if (err != NULL) + { + arg->microRunningServiceCount--; + arg->microAllDone = prev_done; + } + + natsMutex_Unlock(arg->m); + + if (err != NULL) + return err; + + for (i=0; i < num_eps; i++) + { + err = microService_AddEndpoint(*new_m, eps[i]); + if (err != NULL) + { + microService_Destroy(*new_m); + *new_m = NULL; + return err; + } + } + + return NULL; +} + +static void +_startMicroserviceOK(microService** new_m, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg) +{ + char buf[64]; + + snprintf(buf, sizeof(buf), "Start microservice %s: ", cfg->Name); + test(buf); + + testCond (NULL == _startMicroservice(new_m, nc, cfg, eps, num_eps, arg)); +} + +static void +_startManyMicroservices(microService** svcs, int n, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg) +{ + int i; + + for (i = 0; i < n; i++) + { + _startMicroserviceOK(&(svcs[i]), nc, cfg, eps, num_eps, arg); + } + + testCond(true); +} + +static void +_waitForMicroservicesAllDone(struct threadArg *arg) +{ + natsStatus s = NATS_OK; + + test("Wait for all microservices to stop: "); + natsMutex_Lock(arg->m); + while ((s != NATS_TIMEOUT) && !arg->microAllDone) + s = natsCondition_TimedWait(arg->c, arg->m, 1000); + natsMutex_Unlock(arg->m); + testCond((NATS_OK == s) && arg->microAllDone); + + // `Done` may be immediately followed by freeing the service, so wait a bit + // to make sure it happens before the test exits. + nats_Sleep(20); +} + +static void +_destroyMicroservicesAndWaitForAllDone(microService** svcs, int n, struct threadArg *arg) +{ + char buf[64]; + + snprintf(buf, sizeof(buf), "Wait for all %d microservices to stop: ", n); + test(buf); + + for (int i = 0; i < n; i++) + { + if (NULL != microService_Destroy(svcs[i])) + FAIL("Unable to destroy microservice!"); + } + + _waitForMicroservicesAllDone(arg); +} + +typedef struct +{ + const char *name; + microServiceConfig *cfg; + microEndpointConfig **endpoints; + int num_endpoints; + bool null_nc; + bool null_receiver; + const char *expected_err; + int expected_num_subjects; +} add_service_test_case_t; + +static void +test_MicroAddService(void) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + struct threadArg arg; + natsOptions *opts = NULL; + natsConnection *nc = NULL; + natsPid serverPid = NATS_INVALID_PID; + microService *m = NULL; + microServiceInfo *info = NULL; + natsMsg *reply = NULL; + int i, j, n; + char buf[1024]; + char *subject = NULL; + nats_JSON *js = NULL; + nats_JSON **array = NULL; + int array_len; + const char *str; + + microEndpointConfig default_ep_cfg = { + .Name = "default", + .Handler = _microHandleRequestNoisy42, + }; + microEndpointConfig ep1_cfg = { + .Name = "ep1", + .Handler = _microHandleRequestNoisy42, + }; + microEndpointConfig ep2_cfg = { + .Name = "ep2", + .Subject = "different-from-name", + .Handler = _microHandleRequestNoisy42, + }; + microEndpointConfig ep3_cfg = { + .Name = "ep3", + .Handler = _microHandleRequestNoisy42, + }; + microEndpointConfig *all_ep_cfgs[] = {&ep1_cfg, &ep2_cfg, &ep3_cfg}; + + microServiceConfig minimal_cfg = { + .Version = "1.0.0", + .Name = "minimal", + + }; + microServiceConfig full_cfg = { + .Version = "1.0.0", + .Name = "full", + .Endpoint = &default_ep_cfg, + .Description = "fully declared microservice", + }; + microServiceConfig err_no_name_cfg = { + .Version = "1.0.0", + }; + microServiceConfig err_no_version_cfg = { + .Name = "no-version", + }; + microServiceConfig err_invalid_version_cfg = { + .Version = "BLAH42", + .Name = "invalid-version", + }; + + add_service_test_case_t tcs[] = { + { + .name = "Minimal", + .cfg = &minimal_cfg, + }, + { + .name = "Full", + .cfg = &full_cfg, + .expected_num_subjects = 1, + }, + { + .name = "Full-with-endpoints", + .cfg = &full_cfg, + .endpoints = all_ep_cfgs, + .num_endpoints = sizeof(all_ep_cfgs) / sizeof(all_ep_cfgs[0]), + .expected_num_subjects = 4, + }, + { + .name = "Err-null-connection", + .cfg = &minimal_cfg, + .null_nc = true, + .expected_err = "status 16: invalid function argument", + }, + { + .name = "Err-null-receiver", + .cfg = &minimal_cfg, + .null_receiver = true, + .expected_err = "status 16: invalid function argument", + }, + { + .name = "Err-no-name", + .cfg = &err_no_name_cfg, + .expected_err = "status 16: invalid function argument", + }, + { + .name = "Err-no-version", + .cfg = &err_no_version_cfg, + .expected_err = "status 16: invalid function argument", + }, + { + .name = "Err-invalid-version", + .cfg = &err_invalid_version_cfg, + // TODO: validate the version format. + }, + }; + add_service_test_case_t tc; + + srand((unsigned int)nats_NowInNanoSeconds()); + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + if ((opts == NULL) + || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK) + || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect to server: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + for (n = 0; n < (int)(sizeof(tcs) / sizeof(tcs[0])); n++) + { + tc = tcs[n]; + + snprintf(buf, sizeof(buf), "%s: AddService: ", tc.name); + test(buf); + m = NULL; + err = _startMicroservice( + tc.null_receiver ? NULL : &m, + tc.null_nc ? NULL : nc, + tc.cfg, NULL, 0, &arg); + if (nats_IsStringEmpty(tc.expected_err)) + { + testCond(err == NULL); + } + else + { + if (strcmp(tc.expected_err, microError_String(err, buf, sizeof(buf))) != 0) + { + char buf2[2*1024]; + snprintf(buf2, sizeof(buf2), "Expected error '%s', got '%s'", tc.expected_err, buf); + FAIL(buf2); + } + testCond(true); + microError_Destroy(err); + continue; + } + + for (i = 0; i < tc.num_endpoints; i++) + { + snprintf(buf, sizeof(buf), "%s: AddEndpoint '%s': ", tc.name, tc.endpoints[i]->Name); + test(buf); + testCond(NULL == microService_AddEndpoint(m, tc.endpoints[i])); + } + + err = microService_GetInfo(&info, m); + if (err != NULL) + FAIL("failed to get service info!") + + // run through PING and INFO subject variations: 0: all, 1: .name, 2: .name.id + for (j = 0; j < 3; j++) + { + err = micro_new_control_subject(&subject, MICRO_PING_VERB, + (j > 0 ? tc.cfg->Name : NULL), + (j > 1 ? info->Id : NULL)); + if (err != NULL) + FAIL("failed to generate PING subject!"); + + snprintf(buf, sizeof(buf), "%s: Verify PING subject %s: ", tc.name, subject); + test(buf); + s = natsConnection_Request(&reply, nc, subject, NULL, 0, 1000); + IFOK(s, nats_JSONParse(&js, natsMsg_GetData(reply), natsMsg_GetDataLength(reply))); + IFOK(s, nats_JSONGetStrPtr(js, "id", &str)); + IFOK(s, (strcmp(str, info->Id) == 0) ? NATS_OK : NATS_ERR); + IFOK(s, nats_JSONGetStrPtr(js, "name", &str)); + IFOK(s, (strcmp(str, tc.cfg->Name) == 0) ? NATS_OK : NATS_ERR); + IFOK(s, nats_JSONGetStrPtr(js, "version", &str)); + IFOK(s, (strcmp(str, tc.cfg->Version) == 0) ? NATS_OK : NATS_ERR); + IFOK(s, nats_JSONGetStrPtr(js, "type", &str)); + IFOK(s, (strcmp(str, MICRO_PING_RESPONSE_TYPE) == 0) ? NATS_OK : NATS_ERR); + testCond(s == NATS_OK); + nats_JSONDestroy(js); + natsMsg_Destroy(reply); + NATS_FREE(subject); + + err = micro_new_control_subject(&subject, MICRO_INFO_VERB, + (j > 0 ? tc.cfg->Name : NULL), + (j > 1 ? info->Id : NULL)); + if (err != NULL) + FAIL("failed to generate INFO subject!"); + + snprintf(buf, sizeof(buf), "%s: Verify INFO subject %s: ", tc.name, subject); + test(buf); + s = natsConnection_Request(&reply, nc, subject, NULL, 0, 1000); + array = NULL; + array_len = 0; + testCond((NATS_OK == s) + && (NATS_OK == nats_JSONParse(&js, natsMsg_GetData(reply), natsMsg_GetDataLength(reply))) + && (NATS_OK == nats_JSONGetStrPtr(js, "id", &str)) && (strcmp(str, info->Id) == 0) + && (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) && (strcmp(str, tc.cfg->Name) == 0) + && (NATS_OK == nats_JSONGetStrPtr(js, "type", &str)) && (strcmp(str, MICRO_INFO_RESPONSE_TYPE) == 0) + && (NATS_OK == nats_JSONGetArrayObject(js, "endpoints", &array, &array_len)) && (array_len == tc.expected_num_subjects) + ); + + NATS_FREE(array); + nats_JSONDestroy(js); + natsMsg_Destroy(reply); + NATS_FREE(subject); + } + + microServiceInfo_Destroy(info); + + if (m != NULL) + { + snprintf(buf, sizeof(buf), "%s: Destroy service: %d", m->cfg->Name, m->refs); + test(buf); + testCond(NULL == microService_Destroy(m)); + _waitForMicroservicesAllDone(&arg); + } + } + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + +static void +test_MicroGroups(void) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + struct threadArg arg; + natsOptions *opts = NULL; + natsConnection *nc = NULL; + natsPid serverPid = NATS_INVALID_PID; + microService *m = NULL; + microGroup *g1 = NULL; + microGroup *g2 = NULL; + microServiceInfo *info = NULL; + int i; + + microEndpointConfig ep1_cfg = { + .Name = "ep1", + .Handler = _microHandleRequest42, + }; + microEndpointConfig ep2_cfg = { + .Name = "ep2", + .Handler = _microHandleRequest42, + }; + microServiceConfig cfg = { + .Version = "1.0.0", + .Name = "with-groups", + }; + + const char* expected_subjects[] = { + "ep1", + "g1.ep1", + "g1.g2.ep1", + "g1.g2.ep2", + "g1.ep2", + }; + int expected_num_endpoints = sizeof(expected_subjects) / sizeof(expected_subjects[0]); + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + if ((opts == NULL) + || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK) + || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect to server: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startMicroservice(&m, nc, &cfg, NULL, 0, &arg); + + test("AddEndpoint 1 to service: "); + testCond(NULL == microService_AddEndpoint(m, &ep1_cfg)); + + test("AddGroup g1: "); + testCond(NULL == microService_AddGroup(&g1, m, "g1")); + + test("AddEndpoint 1 to g1: "); + testCond(NULL == microGroup_AddEndpoint(g1, &ep1_cfg)); + + test("Add sub-Group g2: "); + testCond(NULL == microGroup_AddGroup(&g2, g1, "g2")); + + test("AddEndpoint 1 to g2: "); + testCond(NULL == microGroup_AddEndpoint(g2, &ep1_cfg)); + + test("AddEndpoint 2 to g2: "); + testCond(NULL == microGroup_AddEndpoint(g2, &ep2_cfg)); + + test("AddEndpoint 2 to g1: "); + testCond(NULL == microGroup_AddEndpoint(g1, &ep2_cfg)); + + err = microService_GetInfo(&info, m); + if (err != NULL) + FAIL("failed to get service info!") + + test("Verify number of endpoints: "); + testCond(info->EndpointsLen == expected_num_endpoints); + + test("Verify endpoint subjects: "); + for (i = 0; i < info->EndpointsLen; i++) + { + if (strcmp(info->Endpoints[i].Subject, expected_subjects[i]) != 0) { + char buf[1024]; + snprintf(buf, sizeof(buf), "expected %s, got %s", expected_subjects[i], info->Endpoints[i].Subject); + FAIL(buf); + } + } + testCond(true); + + microServiceInfo_Destroy(info); + + microService_Destroy(m); + _waitForMicroservicesAllDone(&arg); + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + +#define NUM_MICRO_SERVICES 5 + +static void +test_MicroBasics(void) +{ + natsStatus s = NATS_OK; + microError *err = NULL; + struct threadArg arg; + natsOptions *opts = NULL; + natsConnection *nc = NULL; + natsPid serverPid = NATS_INVALID_PID; + microService *svcs[NUM_MICRO_SERVICES]; + microEndpointConfig ep1_cfg = { + .Name = "do", + .Subject = "svc.do", + .Handler = _microHandleRequestNoisy42, + }; + const char *ep_md[] = { + "key1", "value1", + "key2", "value2", + "key3", "value3", + }; + const char *service_md[] = { + "skey1", "svalue1", + "skey2", "svalue2", + }; + microEndpointConfig ep2_cfg = { + .Name = "unused", + .Subject = "svc.unused", + .Handler = _microHandleRequestNoisy42, + .MetadataLen = 3, + .Metadata = ep_md, + }; + microEndpointConfig *eps[] = { + &ep1_cfg, + &ep2_cfg, + }; + microServiceConfig cfg = { + .Version = "1.0.0", + .Name = "CoolService", + .Description = "returns 42", + .MetadataLen = 2, + .Metadata = service_md, + }; + natsMsg *reply = NULL; + microServiceInfo *info = NULL; + int i; + char buf[1024]; + char *subject = NULL; + natsInbox *inbox = NULL; + natsSubscription *sub = NULL; + nats_JSON *js = NULL; + nats_JSON *md = NULL; + int num_requests = 0; + int num_errors = 0; + int n; + nats_JSON **array; + int array_len; + const char *str; + + srand((unsigned int)nats_NowInNanoSeconds()); + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + if ((opts == NULL) + || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK) + || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect to server: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startManyMicroservices(svcs, NUM_MICRO_SERVICES, nc, &cfg, eps, sizeof(eps)/sizeof(eps[0]), &arg); + + // Now send 50 requests. + test("Send 50 requests (no matter response): "); + for (i = 0; i < 50; i++) + { + s = natsConnection_Request(&reply, nc, "svc.do", NULL, 0, 1000); + if (NATS_OK != s) + FAIL("Unable to send request"); + natsMsg_Destroy(reply); + } + testCond(NATS_OK == s); + + // Make sure we can request valid info with local API. + for (i = 0; i < NUM_MICRO_SERVICES; i++) + { + snprintf(buf, sizeof(buf), "Check local info #%d: ", i); + test(buf); + err = microService_GetInfo(&info, svcs[i]); + testCond((err == NULL) && + (strcmp(info->Name, "CoolService") == 0) && + (strlen(info->Id) > 0) && + (strcmp(info->Description, "returns 42") == 0) && + (strcmp(info->Version, "1.0.0") == 0) && + (info->MetadataLen == 2)); + microServiceInfo_Destroy(info); + } + + // Make sure we can request valid info with $SRV.INFO request. + test("Create INFO inbox: "); + testCond(NATS_OK == natsInbox_Create(&inbox)); + micro_new_control_subject(&subject, MICRO_INFO_VERB, "CoolService", NULL); + test("Subscribe to INFO inbox: "); + testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox)); + test("Publish INFO request: "); + testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0)); + for (i = 0;; i++) + { + snprintf(buf, sizeof(buf), "Receive INFO response #%d: ", i); + test(buf); + reply = NULL; + s = natsSubscription_NextMsg(&reply, sub, 250); + if (s == NATS_TIMEOUT) + { + testCond(i == NUM_MICRO_SERVICES); + break; + } + testCond(NATS_OK == s); + + snprintf(buf, sizeof(buf), "Parse INFO response#%d: ", i); + test(buf); + js = NULL; + testCond(NATS_OK == nats_JSONParse(&js, reply->data, reply->dataLen)) ; + + snprintf(buf, sizeof(buf), "Validate INFO response strings#%d: ", i); + test(buf); + testCond( + (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) && (strcmp(str, "CoolService") == 0) + && (NATS_OK == nats_JSONGetStrPtr(js, "description", &str)) && (strcmp(str, "returns 42") == 0) + && (NATS_OK == nats_JSONGetStrPtr(js, "version", &str)) && (strcmp(str, "1.0.0") == 0) + && (NATS_OK == nats_JSONGetStrPtr(js, "id", &str)) && (strlen(str) > 0) + ); + + snprintf(buf, sizeof(buf), "Validate INFO service metadata#%d: ", i); + test(buf); + md = NULL; + testCond( + (NATS_OK == nats_JSONGetObject(js, "metadata", &md)) + && (NATS_OK == nats_JSONGetStrPtr(md, "skey1", &str)) && (strcmp(str, "svalue1") == 0) + && (NATS_OK == nats_JSONGetStrPtr(md, "skey2", &str)) && (strcmp(str, "svalue2") == 0) + ); + + test("Validate INFO has 2 endpoints: "); + array = NULL; + array_len = 0; + s = nats_JSONGetArrayObject(js, "endpoints", &array, &array_len); + testCond((NATS_OK == s) && (array != NULL) && (array_len == 2)); + + test("Validate INFO svc.do endpoint: "); + md = NULL; + testCond( + (NATS_OK == nats_JSONGetStrPtr(array[0], "name", &str)) && (strcmp(str, "do") == 0) + && (NATS_OK == nats_JSONGetStrPtr(array[0], "subject", &str)) && (strcmp(str, "svc.do") == 0) + && (NATS_OK == nats_JSONGetObject(array[0], "metadata", &md)) && (md == NULL) + ); + + test("Validate INFO unused endpoint with metadata: "); + md = NULL; + testCond( + (NATS_OK == nats_JSONGetStrPtr(array[1], "name", &str)) && (strcmp(str, "unused") == 0) + && (NATS_OK == nats_JSONGetStrPtr(array[1], "subject", &str)) && (strcmp(str, "svc.unused") == 0) + && (NATS_OK == nats_JSONGetObject(array[1], "metadata", &md)) + && (NATS_OK == nats_JSONGetStrPtr(md, "key1", &str)) && (strcmp(str, "value1") == 0) + && (NATS_OK == nats_JSONGetStrPtr(md, "key2", &str)) && (strcmp(str, "value2") == 0) + && (NATS_OK == nats_JSONGetStrPtr(md, "key3", &str)) && (strcmp(str, "value3") == 0) + ); + + nats_JSONDestroy(js); + natsMsg_Destroy(reply); + NATS_FREE(array); + } + natsSubscription_Destroy(sub); + natsInbox_Destroy(inbox); + NATS_FREE(subject); + + // Make sure we can request SRV.PING. + test("Create PING inbox: "); + testCond(NATS_OK == natsInbox_Create(&inbox)); + micro_new_control_subject(&subject, MICRO_PING_VERB, "CoolService", NULL); + test("Subscribe to PING inbox: "); + testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox)); + test("Publish PING request: "); + testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0)); + for (i = 0;; i++) + { + snprintf(buf, sizeof(buf), "Receive PING response #%d: ", i); + test(buf); + reply = NULL; + s = natsSubscription_NextMsg(&reply, sub, 250); + if (s == NATS_TIMEOUT) + { + testCond(i == NUM_MICRO_SERVICES); + break; + } + testCond(NATS_OK == s); + snprintf(buf, sizeof(buf), "Validate PING response #%d: ", i); + test(buf); + js = NULL; + testCond((NATS_OK == nats_JSONParse(&js, reply->data, reply->dataLen)) && + (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) && + (strcmp(str, "CoolService") == 0)); + nats_JSONDestroy(js); + natsMsg_Destroy(reply); + } + natsSubscription_Destroy(sub); + natsInbox_Destroy(inbox); + NATS_FREE(subject); + + // Get and validate $SRV.STATS from all service instances. + test("Create STATS inbox: "); + testCond(NATS_OK == natsInbox_Create(&inbox)); + micro_new_control_subject(&subject, MICRO_STATS_VERB, "CoolService", NULL); + test("Subscribe to STATS inbox: "); + testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox)); + test("Publish STATS request: "); + testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0)); + + for (i = 0;; i++) + { + snprintf(buf, sizeof(buf), "Receive STATS response #%d: ", i); + test(buf); + reply = NULL; + s = natsSubscription_NextMsg(&reply, sub, 250); + if (s == NATS_TIMEOUT) + { + testCond(i == NUM_MICRO_SERVICES); + break; + } + testCond(NATS_OK == s); + + test("Parse STATS response: "); + js = NULL; + s = nats_JSONParse(&js, reply->data, reply->dataLen); + testCond((NATS_OK == s) && (js != NULL)); + + test("Ensure STATS has 2 endpoints: "); + array = NULL; + array_len = 0; + s = nats_JSONGetArrayObject(js, "endpoints", &array, &array_len); + testCond((NATS_OK == s) && (array != NULL) && (array_len == 2)) + + test("Ensure endpoint 0 has num_requests: "); + n = 0; + s = nats_JSONGetInt(array[0], "num_requests", &n); + testCond(NATS_OK == s); + num_requests += n; + + test("Ensure endpoint 0 has num_errors: "); + n = 0; + s = nats_JSONGetInt(array[0], "num_errors", &n); + testCond(NATS_OK == s); + num_errors += n; + + NATS_FREE(array); + nats_JSONDestroy(js); + natsMsg_Destroy(reply); + } + test("Check that STATS total request counts add up (50): "); + testCond(num_requests == 50); + test("Check that STATS total error count is positive, depends on how many instances: "); + testCond(num_errors > 0); + + natsSubscription_Destroy(sub); + natsInbox_Destroy(inbox); + NATS_FREE(subject); + + _destroyMicroservicesAndWaitForAllDone(svcs, NUM_MICRO_SERVICES, &arg); + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + +static void +test_MicroStartStop(void) +{ + natsStatus s = NATS_OK; + struct threadArg arg; + natsOptions *opts = NULL; + natsConnection *nc = NULL; + natsPid serverPid = NATS_INVALID_PID; + microService *svcs[NUM_MICRO_SERVICES]; + microEndpointConfig ep_cfg = { + .Name = "do", + .Subject = "svc.do", + .Handler = _microHandleRequest42, + }; + microServiceConfig cfg = { + .Version = "1.0.0", + .Name = "CoolService", + .Description = "returns 42", + .Endpoint = &ep_cfg, + }; + natsMsg *reply = NULL; + int i; + + srand((unsigned int)nats_NowInNanoSeconds()); + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + if ((opts == NULL) + || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK) + || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK) + || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect to server: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startManyMicroservices(svcs, NUM_MICRO_SERVICES, nc, &cfg, NULL, 0, &arg); + + // Now send some requests. + test("Send requests: "); + for (i = 0; i < 20; i++) + { + reply = NULL; + s = natsConnection_Request(&reply, nc, "svc.do", NULL, 0, 2000); + if (NATS_OK != s) + FAIL("Unable to send request"); + if (reply == NULL || reply->dataLen != 2 || memcmp(reply->data, "42", 2) != 0) + FAIL("Unexpected reply"); + natsMsg_Destroy(reply); + } + testCond(NATS_OK == s); + + _destroyMicroservicesAndWaitForAllDone(svcs, NUM_MICRO_SERVICES, &arg); + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + +static void +test_MicroServiceStopsOnClosedConn(void) +{ + natsStatus s; + natsConnection *nc = NULL; + natsOptions *opts = NULL; + natsPid serverPid = NATS_INVALID_PID; + struct threadArg arg; + microService *m = NULL; + microServiceConfig cfg = { + .Name = "test", + .Version = "1.0.0", + }; + natsMsg *reply = NULL; + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + + if ((opts == NULL) || + (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK) || + (natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg)) || + (natsOptions_SetAllowReconnect(opts, false) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect for microservice: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startMicroservice(&m, nc, &cfg, NULL, 0, &arg); + + test("Test microservice is running: "); + testCond(!microService_IsStopped(m)) + + test("Test microservice is responding to PING: "); + testCond(NATS_OK == natsConnection_RequestString(&reply, nc, "$SRV.PING.test", "", 500)); + natsMsg_Destroy(reply); + reply = NULL; + + test("Close the connection: "); + testCond(NATS_OK == natsConnection_Drain(nc)); + natsConnection_Close(nc); + + test("Wait for it: "); + testCond(_waitForConnClosed(&arg) == NATS_OK); + + test("Ensure the connection has closed: "); + testCond(natsConnection_IsClosed(nc)); + + test("Wait for the service to stop: "); + natsMutex_Lock(arg.m); + while ((s != NATS_TIMEOUT) && !arg.microAllDone) + s = natsCondition_TimedWait(arg.c, arg.m, 1000); + natsMutex_Unlock(arg.m); + testCond(arg.microAllDone); + + test("Test microservice is stopped: "); + testCond(microService_IsStopped(m)); + + test("Destroy microservice (final): "); + testCond(NULL == microService_Destroy(m)) + _waitForMicroservicesAllDone(&arg); + + natsOptions_Destroy(opts); + natsConnection_Destroy(nc); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + +static void +test_MicroServiceStopsWhenServerStops(void) +{ + natsStatus s; + natsConnection *nc = NULL; + natsOptions *opts = NULL; + natsPid serverPid = NATS_INVALID_PID; + struct threadArg arg; + microService *m = NULL; + microServiceConfig cfg = { + .Name = "test", + .Version = "1.0.0", + }; + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s == NATS_OK) + opts = _createReconnectOptions(); + + if ((opts == NULL) + || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK) + || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK) + || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK)) + { + FAIL("Unable to setup test for MicroConnectionEvents!"); + } + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect for microservice: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startMicroservice(&m, nc, &cfg, NULL, 0, &arg); + + test("Test microservice is running: "); + testCond(!microService_IsStopped(m)) + + test("Stop the server: "); + testCond((_stopServer(serverPid), true)); + + test("Wait for the service to stop: "); + natsMutex_Lock(arg.m); + while ((s != NATS_TIMEOUT) && !arg.microAllDone) + s = natsCondition_TimedWait(arg.c, arg.m, 1000); + natsMutex_Unlock(arg.m); + testCond(arg.microAllDone); + + test("Test microservice is not running: "); + testCond(microService_IsStopped(m)) + + microService_Destroy(m); + _waitForMicroservicesAllDone(&arg); + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); +} + +void _microAsyncErrorHandler(microService *m, microEndpoint *ep, natsStatus s) +{ + struct threadArg *arg = (struct threadArg*) microService_GetState(m); + + natsMutex_Lock(arg->m); + // release the pending test request that caused the error + arg->closed = true; + + // set the data to verify + arg->status = s; + natsCondition_Broadcast(arg->c); + natsMutex_Unlock(arg->m); +} + +microError * +_microAsyncErrorRequestHandler(microRequest *req) +{ + struct threadArg *arg = microRequest_GetServiceState(req); + + natsMutex_Lock(arg->m); + + arg->msgReceived = true; + natsCondition_Signal(arg->c); + + while (!arg->closed) + natsCondition_Wait(arg->c, arg->m); + + natsMutex_Unlock(arg->m); + + return NULL; +} + +static void +test_MicroAsyncErrorHandler(void) +{ + natsStatus s; + struct threadArg arg; + natsConnection *nc = NULL; + natsOptions *opts = NULL; + natsPid serverPid = NATS_INVALID_PID; + microService *m = NULL; + microEndpoint *ep = NULL; + microEndpointConfig ep_cfg = { + .Name = "do", + .Subject = "async_test", + .Handler = _microAsyncErrorRequestHandler, + }; + microServiceConfig cfg = { + .Name = "test", + .Version = "1.0.0", + .ErrHandler = _microAsyncErrorHandler, + .State = &arg, + .DoneHandler = _microServiceDoneHandler, + }; + + s = _createDefaultThreadArgsForCbTests(&arg); + if (s != NATS_OK) + FAIL("Unable to setup test!"); + + s = natsOptions_Create(&opts); + IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL)); + IFOK(s, natsOptions_SetMaxPendingMsgs(opts, 10)); + if (s != NATS_OK) + FAIL("Unable to create options for test AsyncErrHandler"); + + serverPid = _startServer("nats://127.0.0.1:4222", NULL, true); + CHECK_SERVER_STARTED(serverPid); + + test("Connect to NATS: "); + testCond(NATS_OK == natsConnection_Connect(&nc, opts)); + + _startMicroservice(&m, nc, &cfg, NULL, 0, &arg); + + test("Test microservice is running: "); + testCond(!microService_IsStopped(m)) + + test("Add test endpoint: "); + testCond(NULL == micro_add_endpoint(&ep, m, NULL, &ep_cfg, true)); + + natsMutex_Lock(arg.m); + arg.status = NATS_OK; + natsMutex_Unlock(arg.m); + + test("Cause an error by sending too many messages: "); + for (int i=0; + (s == NATS_OK) && (i < (opts->maxPendingMsgs + 100)); i++) + { + s = natsConnection_PublishString(nc, "async_test", "hello"); + } + testCond((NATS_OK == s) + && (NATS_OK == natsConnection_Flush(nc))); + + test("Wait for async err callback: "); + natsMutex_Lock(arg.m); + while ((s != NATS_TIMEOUT) && !arg.closed) + s = natsCondition_TimedWait(arg.c, arg.m, 1000); + natsMutex_Unlock(arg.m); + testCond((s == NATS_OK) && arg.closed && (arg.status == NATS_SLOW_CONSUMER)); + + microService_Destroy(m); + _waitForMicroservicesAllDone(&arg); + + test("Destroy the test connection: "); + natsConnection_Destroy(nc); + testCond(NATS_OK == _waitForConnClosed(&arg)); + + natsOptions_Destroy(opts); + _destroyDefaultThreadArgs(&arg); + _stopServer(serverPid); +} + #if defined(NATS_HAS_STREAMING) static int @@ -34705,6 +35833,15 @@ static testInfo allTests[] = {"KeyValueMirrorDirectGet", test_KeyValueMirrorDirectGet}, {"KeyValueMirrorCrossDomains", test_KeyValueMirrorCrossDomains}, + {"MicroMatchEndpointSubject", test_MicroMatchEndpointSubject}, + {"MicroAddService", test_MicroAddService}, + {"MicroGroups", test_MicroGroups}, + {"MicroBasics", test_MicroBasics}, + {"MicroStartStop", test_MicroStartStop}, + {"MicroServiceStopsOnClosedConn", test_MicroServiceStopsOnClosedConn}, + {"MicroServiceStopsWhenServerStops", test_MicroServiceStopsWhenServerStops}, + {"MicroAsyncErrorHandler", test_MicroAsyncErrorHandler}, + #if defined(NATS_HAS_STREAMING) {"StanPBufAllocator", test_StanPBufAllocator}, {"StanConnOptions", test_StanConnOptions},