Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WIP / DRAFT : replacing shimmer with mpwrapper #732

Closed
wants to merge 5 commits into from

Conversation

obecny
Copy link
Member

@obecny obecny commented Jan 24, 2020

Which problem is this PR solving?

Short description of the changes

  • It adds a new shimmer which support multiple wrapping and unwrapping no matter what the order was
  • New monkey patch wrapper should work fine when other libraries will be using shimmer
  • Simplified the unpatch for BasePlugin too

EDIT: It is to validate it, test it and see if we want to keep it or leave shimmer - having in mind it cons

@obecny obecny added the enhancement New feature or request label Jan 24, 2020
@obecny obecny self-assigned this Jan 24, 2020
@obecny obecny added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jan 24, 2020
@obecny obecny changed the title replacing shimmer with mpwrapper WIP / DRAFT : replacing shimmer with mpwrapper Jan 24, 2020
@codecov-io
Copy link

codecov-io commented Jan 24, 2020

Codecov Report

Merging #732 into master will decrease coverage by 4.3%.
The diff coverage is 70.83%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #732      +/-   ##
==========================================
- Coverage   92.64%   88.33%   -4.31%     
==========================================
  Files         226      225       -1     
  Lines       10231    10220      -11     
  Branches      939      920      -19     
==========================================
- Hits         9478     9028     -450     
- Misses        753     1192     +439
Impacted Files Coverage Δ
...s/opentelemetry-core/test/trace/BasePlugin.test.ts 96.42% <ø> (+3.32%) ⬆️
packages/opentelemetry-core/src/common/types.ts 100% <ø> (ø) ⬆️
...metry-core/src/trace/instrumentation/BasePlugin.ts 72.34% <10%> (-14.51%) ⬇️
packages/opentelemetry-plugin-dns/src/dns.ts 97.1% <100%> (-0.13%) ⬇️
packages/opentelemetry-plugin-https/src/https.ts 100% <100%> (ø) ⬆️
...ckages/opentelemetry-plugin-ioredis/src/ioredis.ts 100% <100%> (ø) ⬆️
...plugin-https/test/functionals/https-enable.test.ts 96.11% <100%> (ø) ⬆️
packages/opentelemetry-plugin-http/src/http.ts 97.6% <100%> (-0.13%) ⬇️
packages/opentelemetry-plugin-redis/src/redis.ts 100% <100%> (ø) ⬆️
...-plugin-postgres/opentelemetry-plugin-pg/src/pg.ts 91.07% <100%> (-0.46%) ⬇️
... and 30 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d3af8c4...0b25cf8. Read the comment docs.

@draffensperger
Copy link
Contributor

What value does mpwrapper provide over a small custom monkey-patch utility we might write ourselves?

My sense is that if we can reduce the number of NPM dependencies that's good from a security perspective.

@dyladan
Copy link
Member

dyladan commented Jan 24, 2020

What value does mpwrapper provide over a small custom monkey-patch utility we might write ourselves?

My sense is that if we can reduce the number of NPM dependencies that's good from a security perspective.

@obecny wrote mpwrapper. I think the idea is to just test it out and maybe eventually move it into the otel repo

@vmarchaud
Copy link
Member

vmarchaud commented Jan 25, 2020

I would prefer to have it inside the repo (and so inside this PR) so we can review both the modifications made to otel and the code of mpwrapper

@dyladan
Copy link
Member

dyladan commented Jan 25, 2020

A few issues I have with mpWrapper:

  1. does not copy properties like length and native from the source function. some libraries like express and AWS lambda depend on function length to determine behavior
  2. no tsdocs
  3. uses console logger directly. would much prefer this to be a configurable fn
  4. the inputsHasErrors function is somewhat confusingly named, and it could easily be made stricter
function inputsHasErrors(
  obj: any,
  name: string,
  wrapper: Function
): boolean {
  if (typeof obj !== 'object') {
    logger('module cannot be undefined');
    return true;
  }
  if (typeof name !== 'string') {
    logger(`expected name to be type 'string', received '${typeof name}'`);
    return true;
  }
  if (!obj[name]) {
    logger(`no original function ${name} to wrap`);
    return true;
  }
  if (typeof obj[name] !== 'function') {
    logger(`expected obj.${name} to be type 'function', received '${typeof obj[name]}'`);
    return true;
  }
  if (typeof wrapper !== 'function') {
    logger(`expected wrapper to be type 'function', received '${typeof wrapper}'`);
    return true;
  }
  return false;
}

@dyladan
Copy link
Member

dyladan commented Jan 27, 2020

I talked with @Flarna and he identified a few technical issues. Keep in mind that shimmer shares most if not all of these shortcomings as well.

  1. unwrap conflicts with other libs like shimmer. If we wrap before shimmer, it will drop our __mpWrapped property and we will not be able to unwrap. The same is true in reverse. If shimmer wraps before us, we drop the property they use for unwrapping.
  2. the __mpWrapped property should be a Symbol instead of a string. This prevents it from being an enumerable property and ensures it doesn't clash with another lib that may use the same string property name. shimmer also has this issue. IE11 does not support Symbol.
  3. extendObject only copies enumerable string properties and does not copy Symbol keyed properties like util.promisify.custom. shimmer also has this issue.
  4. extendObject only copies property values, and does not copy property metadata like writeable, configurable, etc. shimmer also has this issue.
  5. Function properties like name and length are not copied. One well known library this will break is express. shimmer also has this issue.
  6. There is an inheritance issue that could change user's program behavior. shimmer also has this issue. See below:
import { wrap } from "mpwrapper"

function myFunc(original: Function) {
    return original;
}

class Base {
    foo() { return 'foo' }
}
class Derived extends Base {
    bar() { return 'bar' }
}

const d = new Derived();

d.constructor.prototype.hasOwnProperty("foo") // false
console.log( // ['constructor', 'bar']
    Object.getOwnPropertyNames(Derived.prototype)
);

wrap(Derived.prototype, "foo", myFunc);

// here we have changed the properties on `Derived.prototype`
d.constructor.prototype.hasOwnProperty("foo") // true
console.log( // ['constructor', 'bar', 'foo']
    Object.getOwnPropertyNames(Derived.prototype)
);

I think there are a few use cases that mpWrapper supports that we probably don't need.

  1. unwrap. I know we currently support disabling plugins, but this is not required by spec and increases the complexity of implementation for both mpWrapper and for our plugins. Currently unwrap is used in tests, but it might be worth the decrease in complexity to drop this and adapt the tests. The primary use case for unwrap for us is to disable a plugin This is the same use case that Named Tracers solves, and Named Tracers are an API/spec level feature.
  2. multiple wrapping. We only need our wrapper to be able to wrap once, to be able to wrap functions that have already been wrapped by shimmer, and to not interfere with shimmer wrapping. I don't know a use case where we would want to be able to wrap multiple times with mpWrapper. This increases implementation complexity of mpWrapper
  3. mass wrap/unwrap. I don't think we need this

@obecny
Copy link
Member Author

obecny commented Jan 27, 2020

  1. no tsdocs

yes the library is not a final so it doesn't have yet docs and unit tests - the idea was to show that with regards to this

Is unpatch really a requirement? It will be really hard to implement a bullet prove patch library which works in all cases. Monkey patching is used more often then someone would expect and there are numerous variants how this is done in detail.
Besides the question of restoring the correct function you end up also with in flight objects created with patched version of a module but have to finish their work in an unpatched world.
Using unpatch for testing is fine but I would not allow live unpatch.

So here it is the library which you can safely unpatch or patch whenever you want and you don't have flight objects created with patched version of a module but have to finish their work in an unpatched world

  1. uses console logger directly. would much prefer this to be a configurable fn

it's exactly the same what shimmer has, but we can always change it

  1. the inputsHasErrors function is somewhat confusingly named, and it could easily be made stricter

you can always improve things

  1. unwrap conflicts with other libs like shimmer. If we wrap before shimmer, it will drop our __mpWrapped property and we will not be able to unwrap. The same is true in reverse. If shimmer wraps before us, we drop the property they use for unwrapping.

not it will not - neither shimmer nor mpwrapper will remove each other. But mpwrapper will always be able to remove what it had added. Shimmer saves the reference to "original" and original is the reference for storing all the callbacks in mpwrapper.

  1. the __mpWrapped property should be a Symbol instead of a string. This prevents it from being an enumerable property and ensures it doesn't clash with another lib that may use the same string property name. shimmer also has this issue. IE11 does not support Symbol.

why would you even want such feature - string is good enough for what it does - it just adds information that it ha been patched, you can't write bullet proof solution that will prevent overwriting the same property by any1 else it's js you can overwrite everything.

  1. extendObject only copies enumerable string properties and does not copy Symbol keyed properties like util.promisify.custom. shimmer also has this issue.

yes it doesn't extend all possible thing, but it extent already more then shimmer, adding few more things it just a matter of adding them if we need to - so far it doesn't seems necessary otherwise it would already fail.

  1. extendObject only copies property values, and does not copy property metadata like writeable, configurable, etc. shimmer also has this issue.

the same as previous it doesn't because we don't need that yet, we can easily add it if we need to

  1. Function properties like name and length are not copied. One well known library this will break is express. shimmer also has this issue.

as previously just add more things which should be copied

  1. There is an inheritance issue that could change user's program behavior. shimmer also has this issue. See below:
import { wrap } from "mpwrapper"

function myFunc(original: Function) {
    return original;
}

class Base {
    foo() { return 'foo' }
}
class Derived extends Base {
    bar() { return 'bar' }
}

const d = new Derived();

d.constructor.prototype.hasOwnProperty("foo") // false
console.log( // ['constructor', 'bar']
    Object.getOwnPropertyNames(Derived.prototype)
);

wrap(Derived.prototype, "foo", myFunc);

here again, where do we need this ?, and if that is the case why not add that ?

// here we have changed the properties on Derived.prototype
d.constructor.prototype.hasOwnProperty("foo") // true
console.log( // ['constructor', 'bar', 'foo']
Object.getOwnPropertyNames(Derived.prototype)
);


I think there are a few use cases that `mpWrapper` supports that we probably don't need.

1. unwrap. I know we currently support disabling plugins, but this is not required by spec and increases the complexity of implementation for both `mpWrapper` and for our plugins. Currently unwrap is used in tests, but it might be worth the decrease in complexity to drop this and adapt the tests. The primary use case for unwrap for us is to disable a plugin This is the same use case that Named Tracers solves, and Named Tracers are an API/spec level feature.

we already use this functionality from shimmer, aren't we? Moreover as you can see I have already simplified the BasePlugin so that you don't need to implement unpatch in most cases unless you want to add some extra step. Instead of adding forced limits I would rather let user decide what he needs instead of limiting this from the very beginning without really a reason because technology allows that and implementation is already done.

  1. multiple wrapping. We only need our wrapper to be able to wrap once, to be able to wrap functions that have already been wrapped by shimmer, and to not interfere with shimmer wrapping. I don't know a use case where we would want to be able to wrap multiple times with mpWrapper. This increases implementation complexity of mpWrapper

we will have this problem and any1 that will use the opentelemetry will face this problem too. It will happen in moment when someone is already using a shimmer and we will keep on using shimmer too. But we can be at least safe and give people ability to switch from shimmer to mpwrapper if they face such problem - the solution will be there.
But also I would not be so sure that we won't need such functionality. For example we might have plugins that will patch the same functions and use it for its own purposes. In node it might be for example mongo and mongoose. both plugins will need to have access to the same part of code.
And as last This increases implementation complexity of mpWrapper well it is not so complicated as it sounds and it works.

  1. mass wrap/unwrap. I don't think we need this

This has been already used by grpc and mongo

@Flarna
Copy link
Member

Flarna commented Jan 27, 2020

I tried to unpatch a function first patched with mpwrapper and then with shimmer but it doesn't work:

const shimmer = require("shimmer");
const mpwrapper = require("mpwrapper");

function theOriginal(a, b) {
    console.log("the original", a, b);
}

const obj = {
    theOriginal
};

const mpWrapRc = mpwrapper.wrap(obj, "theOriginal", (orig) => {
    return function mpWrapper() {
        console.log("mpWrapper start")
        return orig.apply(this, arguments);
    }
});

shimmer.wrap(obj, "theOriginal", (orig) => {
    return function shimmerWrapper() {
        console.log("shimmerWrapper start")
        return orig.apply(this, arguments);
    }
});

obj.theOriginal(1, 2);

console.log("\nnow unwrap and call again:")
mpWrapRc.unwrap();
obj.theOriginal(3, 4);

results in

shimmerWrapper start
mpWrapper start
the original 1 2

now unwrap and call again:
function cannot be unwrapped
shimmerWrapper start
mpWrapper start
the original 3 4

Root cause is that unwrapCallback() gets the shimmer wrapped function via obj[name] and this one has no WRAPPED property therefore mpwrapper is not able to unwrap it.

@obecny
Copy link
Member Author

obecny commented Jan 27, 2020

I tried to unpatch a function first patched with mpwrapper and then with shimmer but it doesn't work:

const shimmer = require("shimmer");
const mpwrapper = require("mpwrapper");

function theOriginal(a, b) {
    console.log("the original", a, b);
}

const obj = {
    theOriginal
};

const mpWrapRc = mpwrapper.wrap(obj, "theOriginal", (orig) => {
    return function mpWrapper() {
        console.log("mpWrapper start")
        return orig.apply(this, arguments);
    }
});

shimmer.wrap(obj, "theOriginal", (orig) => {
    return function shimmerWrapper() {
        console.log("shimmerWrapper start")
        return orig.apply(this, arguments);
    }
});

obj.theOriginal(1, 2);

console.log("\nnow unwrap and call again:")
mpWrapRc.unwrap();
obj.theOriginal(3, 4);

results in

shimmerWrapper start
mpWrapper start
the original 1 2

now unwrap and call again:
function cannot be unwrapped
shimmerWrapper start
mpWrapper start
the original 3 4

Root cause is that unwrapCallback() gets the shimmer wrapped function via obj[name] and this one has no WRAPPED property therefore mpwrapper is not able to unwrap it.

good catch, for removing callbacks I will pass the wrapper as well and this way the callback will be removed

@Flarna
Copy link
Member

Flarna commented Jan 27, 2020

To illustrate the issue with adding a function on prototype of derived class:

const mpwrapper = require("mpwrapper");

class Base {
    foo() {
        console.log("foo")
    }
}

class Derived extends Base {
    bar() {
        console.log("bar")
    }
}

// if Base.prototype is patched here it's fine
mpwrapper.wrap(Derived.prototype, "foo", (orig) => {
    return function() {
        console.log("patched Derived#foo")
        return orig.apply(this, arguments)
    }
})

mpwrapper.wrap(Base.prototype, "foo", (orig) => {
    return function() {
        console.log("patched Base#foo")
        return orig.apply(this, arguments)
    }
})

const d = new Derived();
d.foo();

results in

patched Derived#foo
foo

==> The second patch is omitted.

here again, where do we need this ?, and if that is the case why not add that ?

If it is not needed then I recommend to disallow it. If someone tries to patch a function which is not an own property then reject this as not supported and the user should adapt the code to select the base class.

Depending on the complexity of the plugins added we may end up in the need for a more advance handling.
For example in http there are http.ServerResponse and http.ClientRequest which both derive from http.OutgoingMessage which in turn is derives from EventEmitter. There could be conflicts between IncomingHttp plugin, OutgoingHttp plugin and ScopeManager.
It's not the case now so I fully agree that it is not needed to create a complex solution for this now. But we should avoid errors just because someone patches at the wrong place or because the module author moves some API into a base class.

@obecny
Copy link
Member Author

obecny commented Apr 7, 2020

this seems to be outdated and unwanted, closing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

remove shimmer, implement new one
6 participants