share / sharedb

Realtime database backend based on Operational Transformation (OT)
Other
6.25k stars 453 forks source link

Invalid op submitted. Op version newer than current snapshot #574

Open PodviaznikovPavlo opened 2 years ago

PodviaznikovPavlo commented 2 years ago

From time to time I receive this error on .submitOp. There are no specific actions. I can submit same action but change value from 1 to 2 fox example and will get this error. Also, it is not possible to reproduce, because the error can appear when i change 1 to 2, 2 to 3, n to m. You never know when this error appears.

Can someone clarify, in what cases this error appears? what exactly happening, so sharedb just says - no, not this time. And is there a way to workaround?

alecgibson commented 2 years ago

Hmm according to code comments, this should never happen.

What does your code look like? Hard to say how this could have happened without a repro case. Are you doing anything exotic in middleware? Which DB driver are you using?

PodviaznikovPavlo commented 2 years ago

thanks for response. so, here is what i have:

  1. the sharedb version is 1.9.2. maybe in 1.9.2 it was possible to get this error. if so, i will update sharedb version;
  2. here is my client code
const context = this.subscriptions[docId];
const ops = [];
ops.push({
  p: ["data", rowId, colId],
  od: oldValue,
  oi: newValue
});

context.submitOp(ops, (error) => {
  error ? resolve({ error }) : resolve({ success: true });
});

this can be called quickly many times. like, at the same time i can call this function 200 times. which means, 200 times context.submitOp will be triggered ( with different coords and data of course ). these ops will go in parallel, and not one-by-one ( maybe there might be an issue with this, i don't know )

  1. this is server side part
class SharedbService {
  constructor() {
    this.shareDBInstance = undefined;
  }

  init() {
    const db = DbService.dbConnection;
    const sharedbRedis = RedisService.sharedbRedis;
    const wss = WebsocketService.websocketServer;

    this.shareDBInstance = new ShareDB({ db, pubsub: sharedbRedis });
    this._setShareDBApplyMiddleware();

    wss.on('connection', (ws, req) => this._onConnection(ws, req));
    wss.on('error', (error) => this._onError({error, errorCode: 4002}));
  }

  async _onConnection(ws, req) {
    const token = HelperService.getTokenFromURL({ urlString: req.url });
    if(!token) return this._onError({ ws, errorCode: 4000 });

    const { user, error } = await AuthService.verify(token);
    if(error) return this._onError({ ws, errorCode: 4001 });

    const stream = new WebSocketJSONStream(ws);
    const agent = this.shareDBInstance.listen(stream);
    agent.custom.user = user;
  }

  _setShareDBApplyMiddleware() {
    this.shareDBInstance
      .use('receive', (req, next) => {
        const data = req.data || {};

        switch(data.a){
          case 'ping': break;
          default: next(); break;
        }
       })
      .use('commit', (context, next) => {
        const ops = context.op.op;
        const { user } = context.agent.custom;

        context.maxRetries = 5;

        if(context.retries >= 5) {
          return next(
            new Error(
              JSON.stringify(ErrorCodes["4003"])
            )
          );
        }

        if(!user) {
          return this._onError({ ws, errorCode: 4001 });
        }

        ops.forEach(op => {
          op.user = { username: user };
        });

        ops.push(...[
          { p: ["updated_by"], oi: user },
          { p: ['updated'], oi: new Date().toISOString() }
        ]);

        next();
      })
      .use('afterWrite', (context, next) => {
        const ops = context.op.op;

        LoggerService.logOps(JSON.stringify(ops));
        next();
      });
  }

  _onError({ws, error, errorCode}) {
    if(ws) {
      ws.send(JSON.stringify({ error: ErrorCodes[errorCode] }));
      ws.close(errorCode, ErrorCodes[errorCode].message);
    }

    LoggerService.onError(`${ErrorCodes[errorCode].message}: ${error || errorCode}`);
  }
}

answering your question about middleware, i think the main thing is that i modify ops there with some info about current user. don't know if this is something "exotic" :)

  1. i'm using sharedb-mongo lib. So, using mongo driver. And redis pub sub. "sharedb-mongo": "^1.0.0", "sharedb-redis-pubsub": "^1.0.0",

the biggest problem is that the issue is not reproducible. it can be one op goes well, and on second i get the versions error.

if what i provided is not enough, maybe you can explain what exactly this error is about, how it can be trigerred and maybe how can i avoid it. Or, maybe it is possible to ignore it, don't know. for the moment this specific issue is very painful.

PodviaznikovPavlo commented 2 years ago

updated sharedb to latest - still can see in some cases this error. so not related to the version

PodviaznikovPavlo commented 2 years ago

also, we have JAVA API, which works with Mongo. so, there are cases, when action happens, and i send request to API and calling sharedb submitOp. I know, that you cannot udpate mongo model from 2 ends at the same time. maybe there is some issue with this. also, since API updates model in mongo, API doesn't change model version. maybe there is some conflict with this. so, it can be that local snapshot might be outdated because API made some changes to this model

alecgibson commented 2 years ago

also, we have JAVA API, which works with Mongo. so, there are cases, when action happens, and i send request to API and calling sharedb submitOp. I know, that you cannot udpate mongo model from 2 ends at the same time. maybe there is some issue with this. also, since API updates model in mongo, API doesn't change model version. maybe there is some conflict with this. so, it can be that local snapshot might be outdated because API made some changes to this model

I'm not sure I follow. You mean you're manually changing ShareDB documents outside of ShareDB by directly updating the underlying Mongo document?

PodviaznikovPavlo commented 2 years ago

I'm not sure I follow. You mean you're manually changing ShareDB documents outside of ShareDB by directly updating the underlying Mongo document?

yes. we have complex system. and the model in Mongo is updated from different sides:

  1. sharedb. this is my code. i subscribe the document, make changes and update document through sharedb
  2. java. these guys update model in mongo directly.

since there is no sharedb for java ( at least it didn't exist, maybe it is now ), java has to update model directly.

so, it can be such situation there can be paste of data ( ~20 ops ). it means, i will call sharedb submitOp 20 times and these will go in parallel. But, at the same time, for each change i will call Java API, for some purpose and Java API will make some additional changes to the model, i'm saving data through sharedb at the moment. So, at the same time, one model can be updated many times and from different sides.

alecgibson commented 2 years ago

I'm not sure ShareDB's behaviour is well-defined if you're mutating its document without its knowledge

PodviaznikovPavlo commented 2 years ago

okay, so that's might be a problem, right? in this case we can't be sure sharedb can handle it? it was designed to be the only source working with model, right?

alecgibson commented 2 years ago

Yes that would be my first guess. ShareDB assumes that it has full control over its own documents. The issue could be – for example – the Java API accidentally writing a modified version of an old snapshot, essentially rewinding the version number. But there's bound to be a wide number of edge cases when doing this sort of thing, which is beyond the scope of this library to debug. As I've said, ShareDB's behaviour in this case is undefined.

My recommendation is that your Java API make a call to a Node.js API and submit ops "properly" via ShareDB. Or your Java API needs to work on a separate collection that's just a "dumb" MongoDB collection (not ShareDB).

PodviaznikovPavlo commented 2 years ago

great, thanks for your response. NodeJS API is one of the ideas i have. i jsut want to be sure, that the case i described is something, that can lead to sharedb behaves the way i don't expect. and looks like this is the case. if you see any workaround how to avoid this error, any param can be provided or something, please, let me know. if no, then i got everything. thanks!