Closed hhchristian closed 1 year ago
I tried your reproduce steps, but no issue.
I believe that I observed the issue both with broker.loadService
and broker.createService
, but now it's only reproducible with broker.createService
. Maybe there's another precondition I haven't recognized, I'm sorry for this confusion.
The following code (modified the GreeterService index.ts
TypeScript test script) should trigger the issue:
import * as path from "path";
import { ServiceBroker } from "../../../";
import GreeterService from "./greeter.service";
(async function() {
try {
const broker = new ServiceBroker({logger: true});
await broker.start();
// broker.loadService(path.join(__dirname, "greeter.service.ts"));
broker.createService(new GreeterService(broker));
await new Promise((res) => setTimeout(res, 50));
const res = await broker.call("greeter.welcome", { name: "Typescript" });
broker.logger.info("Result: ", res);
if (res != "Welcome, TYPESCRIPT!") throw new Error("Result is mismatch!");
await broker.stop();
} catch(err) {
console.log(err);
process.exit(1);
}
})();
It's using ordinary TypeScript import GreeterService from "./greeter.service";
module loading mechanism and new GreeterService(broker)
to construct service instance. We are using Webpack to create a bundle which contains the service modules and therefore cannot dynamically load these modules by their file name or use broker.loadService(fileName)
, so afaik we have to use broker.createService(serviceInstance)
.
Note: During my experiments I sometimes wasn't able to access members of service schema, service instance, service class (TypeScript) or service base (ServiceFactory). To access information/members/functions of these objects, it's probably necessary to connect JS prototypes - as it's already done in broker.normalizeSchemaConstructor()
for service schema and ServiceFactory.
In the "TypeScript use case" there's the service class and the object passed to parseServiceSchema()
- both of them seem to be relevant. Maybe there's another more elegant solution, but no of my other approaches provides access to all information sources.
Note: To workaround the issue in our code, we are currently the following code in service classes:
constructor(public broker: ServiceBroker) {
super(broker);
const self = this;
// eslint-disable-next-line no-proto
this.started = function started() { self.__proto__.__proto__ = this; };
this.actions = <ServiceActionsSchema>{
myActionFoo: (ctx: Context<ActionFooParams, any>):
Promise<ActionFooResponse> => this.myActionFooHandler(ctx),
...
} as ServiceActions;
this.events = <ServiceEvents> {
myEventBar: {
handler: (ctx: Context<EventBar, any>): void => {
this.myEventBarHandler(ctx);
},
},
...
};
}
This class hierarchy manipulation by changing __proto__
of TypeScript service class from Moleculer ServiceFactory/base class to Moleculer service class provides access to members of all involved classes/objects.
I'm closing this issue because we don't know, how we can solve it inside Moleculer. If you have an idea or solution please reopen or open a PR.
Prerequisites
Please answer the following questions for yourself before submitting an issue.
Current Behavior
When
Service.parseServiceSchema()
is called by used code, the service gets registered twice, but the second instance of the service is corrupted and passes incorrect arguments to action/event implementations of the service. This is causing issues when using the hot-reload feature (registering services after broker has been started). In this case, middleware methods are also called twice for each service action call.Expected Behavior
The
Service.parseServiceSchema()
method shouldn't cause side effects when beeing called by user code. Service schema and service instance should be distinguished by Moleculer methodsBroker.createService()
andService.constructor()
. When the Moleculer API allows either to be passed toBroker.createService()
, a service instance which probably already hasactions
,events
and_serviceSpecification
fields must not be handled similar to a service schema. I'd also appreciate sanity checks at a few significant code locations in Moleculer to detect incorrect calls or wrong arguments and prevent Moleculer from operating incorrectly in such situations.Failure Information
This issue was observed when using Moleculer with TypeScript and creating services on demand when the Moleculer broker already has been started. The effect of undefined action parameters causing errors in user code is a side effect of the actual issue.
Steps to Reproduce
This issue can be reproduced by modifying the
GreeterService
TypeScript test script/master/test/typescript/hello-world/index.ts
a little bit: Move thebroker.loadService(...)
call in the async function afterawait broker.start();
and add a small delay (to internally allowBroker._restartService()
to complete), so the first five statements of theasync function() {
are:This modification triggers the issue and is causing the test to fail because the
greeter.welcome
action implementation ingreeter.service.ts
and itsbefore
hook with method argumentctx: Context<GreeterWelcomeParams>
is called withctx.params = ctx
incorrect parameters. This causes thewelcome
actionbefore
hookctx.params.name = ctx.params.name.toUpperCase();
to raiseTypeError: Cannot read property 'toUpperCase' of undefined
(becausectx
has noname
field).Context
This strange behavior is caused by method
Service.parseServiceSchema(schema)
being called twice for the/each service:greeter.service.ts
),Broker.loadService()
➜Broker.createService()
➜new this.ServiceFactory(this, s)
➜Service.constructor()
➜Service.parseServiceSchema()
.Note:
The
class Service
has aconstructor(broker, schema)
with two arguments, but in constructor ofclass GreeterService
(extendsService
) it's called assuper(broker);
with only one argument. When the constructor ofclass GreeterService
would call its base class constructor assuper(broker, schema);
, methodService.parseServiceSchema(schema)
would even be called three times.Effect of
ctx.params
beingundefined
The implementation of
Service.parseServiceSchema(schema)
setsthis.actions[name] = (params, opts) => { ...
andthis.events[innerEvent.name] = (params, opts) => {
.actions
andevents
fields of the service instance with lambda functions having two arguments(params, opts)
and callingwrappedHandler(ctx)
.schema
, again wrapping theactions
andevents
fields (wogether with all middleware calls). This seems to work when having a brief look at theGreeterService
test / example, but is causing confusion and strange effects like thectx
argument of service action implementation to havectx.params
set toctx
in some cases - depending on the time/order of service registration.Dependency on hot-(re)loaded services
The different behavior when registering according services before or after the Moleculer broker has been started seems to be related with the following observation: When the service is registered before the broker is started, the additional service and its endpoints isn't used and only seem to affect metrices. But if a service is registered when the broker already has been started, the second
parseServiceSchema()
call "wins" and the incorrectly wrapped Service instance is registered by the following function call chain:Sanity checks:
At the end of
Service.parseServiceSchema()
methodService._init()
andthis.broker.addLocalService(this)
is called, causingBroker.services[]
to contain multiple entries for the same service. This would be a good location for a simple and cheap sanity check which would have detected the issue much earlier than theTypeError: Cannot read property 'toUpperCase' of undefined
in external code.Advantage of strict code and precise expectations
Because both service schema and service instance have
actions
, and the code inService._createAction(actionDef, name)
can handle theactionDef
argument both as object and as function (by wrapping the function into an object), the service instance seems to also work when being used as service schema. Its probably better to be more strict and don't allow allfunction
-or-object
-or-array
byfixing
the argument variation internally. This also would have allowed the issue to be detected earlier. Also being strict instead of allowing variations keeps the code more concise, clear to developers and helps JIT compilers to generate faster code with less variations.Possible workarounds
Until a fix for this issue is available, it can be workarounded by:
Fixing action handler arguments by patching
ContextFactory.create()
from "outside" of Moleculer:Note that the duplicate service registration may still cause problems in this case.
Don't call
this.parseServiceSchema
in constructor of service classes (likeGreeterService
). Instead, assign according members ofthis
service instance: