Merge pull request #1084 from nasa/transaction-clearing-1059

[Persistence] Clear transactions selectively
This commit is contained in:
Andrew Henry 2016-07-15 16:20:19 -07:00 committed by GitHub
commit 6176ca2260
10 changed files with 344 additions and 135 deletions

View File

@ -42,6 +42,7 @@ define([
"./src/representers/EditToolbarRepresenter", "./src/representers/EditToolbarRepresenter",
"./src/capabilities/EditorCapability", "./src/capabilities/EditorCapability",
"./src/capabilities/TransactionCapabilityDecorator", "./src/capabilities/TransactionCapabilityDecorator",
"./src/services/TransactionManager",
"./src/services/TransactionService", "./src/services/TransactionService",
"./src/creation/CreateMenuController", "./src/creation/CreateMenuController",
"./src/creation/LocatorController", "./src/creation/LocatorController",
@ -80,6 +81,7 @@ define([
EditToolbarRepresenter, EditToolbarRepresenter,
EditorCapability, EditorCapability,
TransactionCapabilityDecorator, TransactionCapabilityDecorator,
TransactionManager,
TransactionService, TransactionService,
CreateMenuController, CreateMenuController,
LocatorController, LocatorController,
@ -222,8 +224,7 @@ define([
"policyService", "policyService",
"dialogService", "dialogService",
"creationService", "creationService",
"copyService", "copyService"
"transactionService"
], ],
"priority": "mandatory" "priority": "mandatory"
}, },
@ -321,7 +322,7 @@ define([
"implementation": TransactionCapabilityDecorator, "implementation": TransactionCapabilityDecorator,
"depends": [ "depends": [
"$q", "$q",
"transactionService" "transactionManager"
], ],
"priority": "fallback" "priority": "fallback"
}, },
@ -406,6 +407,15 @@ define([
"key": "locator", "key": "locator",
"template": locatorTemplate "template": locatorTemplate
} }
],
"services": [
{
"key": "transactionManager",
"implementation": TransactionManager,
"depends": [
"transactionService"
]
}
] ]
} }
}); });

View File

@ -44,7 +44,6 @@ define([
dialogService, dialogService,
creationService, creationService,
copyService, copyService,
transactionService,
context context
) { ) {
this.domainObject = (context || {}).domainObject; this.domainObject = (context || {}).domainObject;
@ -55,7 +54,6 @@ define([
this.dialogService = dialogService; this.dialogService = dialogService;
this.creationService = creationService; this.creationService = creationService;
this.copyService = copyService; this.copyService = copyService;
this.transactionService = transactionService;
} }
/** /**
@ -113,9 +111,8 @@ define([
var self = this, var self = this,
domainObject = this.domainObject, domainObject = this.domainObject,
copyService = this.copyService, copyService = this.copyService,
transactionService = this.transactionService, dialog = new SaveInProgressDialog(this.dialogService),
cancelOldTransaction, toUndirty = [];
dialog = new SaveInProgressDialog(this.dialogService);
function doWizardSave(parent) { function doWizardSave(parent) {
var wizard = self.createWizard(parent); var wizard = self.createWizard(parent);
@ -147,29 +144,33 @@ define([
} }
function allowClone(objectToClone) { function allowClone(objectToClone) {
return (objectToClone.getId() === domainObject.getId()) || var allowed =
objectToClone.getCapability('location').isOriginal(); (objectToClone.getId() === domainObject.getId()) ||
objectToClone.getCapability('location').isOriginal();
if (allowed) {
toUndirty.push(objectToClone);
}
return allowed;
} }
function cloneIntoParent(parent) { function cloneIntoParent(parent) {
return copyService.perform(domainObject, parent, allowClone); return copyService.perform(domainObject, parent, allowClone);
} }
function undirty(object) {
return object.getCapability('persistence').refresh();
}
function undirtyOriginals(object) {
return Promise.all(toUndirty.map(undirty))
.then(resolveWith(object));
}
function commitEditingAfterClone(clonedObject) { function commitEditingAfterClone(clonedObject) {
return domainObject.getCapability("editor").save() return domainObject.getCapability("editor").save()
.then(resolveWith(clonedObject)); .then(resolveWith(clonedObject));
} }
function restartTransaction(object) {
cancelOldTransaction = transactionService.restartTransaction();
return object;
}
function doCancelOldTransaction(object) {
cancelOldTransaction();
return object;
}
function onFailure() { function onFailure() {
hideBlockingDialog(); hideBlockingDialog();
return false; return false;
@ -179,10 +180,9 @@ define([
.then(doWizardSave) .then(doWizardSave)
.then(showBlockingDialog) .then(showBlockingDialog)
.then(getParent) .then(getParent)
.then(restartTransaction)
.then(cloneIntoParent) .then(cloneIntoParent)
.then(undirtyOriginals)
.then(commitEditingAfterClone) .then(commitEditingAfterClone)
.then(doCancelOldTransaction)
.then(hideBlockingDialog) .then(hideBlockingDialog)
.catch(onFailure); .catch(onFailure);
}; };

View File

@ -33,22 +33,21 @@ define(
* called. * called.
* @memberof platform/commonUI/edit/capabilities * @memberof platform/commonUI/edit/capabilities
* @param $q * @param $q
* @param transactionService * @param transactionManager
* @param persistenceCapability * @param persistenceCapability
* @param domainObject * @param domainObject
* @constructor * @constructor
*/ */
function TransactionalPersistenceCapability( function TransactionalPersistenceCapability(
$q, $q,
transactionService, transactionManager,
persistenceCapability, persistenceCapability,
domainObject domainObject
) { ) {
this.transactionService = transactionService; this.transactionManager = transactionManager;
this.persistenceCapability = persistenceCapability; this.persistenceCapability = persistenceCapability;
this.domainObject = domainObject; this.domainObject = domainObject;
this.$q = $q; this.$q = $q;
this.persistPending = false;
} }
/** /**
@ -57,34 +56,14 @@ define(
* @returns {*} * @returns {*}
*/ */
TransactionalPersistenceCapability.prototype.persist = function () { TransactionalPersistenceCapability.prototype.persist = function () {
var self = this; var wrappedPersistence = this.persistenceCapability;
function onCommit() { if (this.transactionManager.isActive()) {
return self.persistenceCapability.persist().then(function (result) { this.transactionManager.addToTransaction(
self.persistPending = false; this.domainObject.getId(),
return result; wrappedPersistence.persist.bind(wrappedPersistence),
}); wrappedPersistence.refresh.bind(wrappedPersistence)
} );
function onCancel() {
if (self.domainObject.getModel().persisted !== undefined) {
//Fetch clean model from persistence
return self.persistenceCapability.refresh().then(function (result) {
self.persistPending = false;
return result;
});
} else {
self.persistPending = false;
//Model is undefined in persistence, so return undefined.
return self.$q.when(undefined);
}
}
if (this.transactionService.isActive()) {
if (!this.persistPending) {
this.transactionService.addToTransaction(onCommit, onCancel);
this.persistPending = true;
}
//Need to return a promise from this function //Need to return a promise from this function
return this.$q.when(true); return this.$q.when(true);
} else { } else {
@ -93,6 +72,8 @@ define(
}; };
TransactionalPersistenceCapability.prototype.refresh = function () { TransactionalPersistenceCapability.prototype.refresh = function () {
this.transactionManager
.clearTransactionsFor(this.domainObject.getId());
return this.persistenceCapability.refresh(); return this.persistenceCapability.refresh();
}; };

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Manages transactions to support the TransactionalPersistenceCapability.
* This assumes that all commit/cancel callbacks for a given domain
* object are equivalent, and only need to be added once to any active
* transaction. Violating this assumption may cause unexpected behavior.
* @constructor
* @memberof platform/commonUI/edit
*/
function TransactionManager(transactionService) {
this.transactionService = transactionService;
this.clearTransactionFns = {};
}
/**
* Check if a transaction is currently active.
* @returns {boolean} true if there is a transaction active
*/
TransactionManager.prototype.isActive = function () {
return this.transactionService.isActive();
};
/**
* Check if callbacks associated with this domain object have already
* been added to the active transaction.
* @private
* @param {string} id the identifier of the domain object to check
* @returns {boolean} true if callbacks have been added
*/
TransactionManager.prototype.isScheduled = function (id) {
return !!this.clearTransactionFns[id];
};
/**
* Add callbacks associated with this domain object to the active
* transaction. Both callbacks are expected to return promises that
* resolve when their associated behavior is complete.
*
* If callbacks associated with this domain object have already been
* added to the active transaction, this call will be ignored.
*
* @param {string} id the identifier of the associated domain object
* @param {Function} onCommit behavior to invoke when committing transaction
* @param {Function} onCancel behavior to invoke when cancelling transaction
*/
TransactionManager.prototype.addToTransaction = function (
id,
onCommit,
onCancel
) {
var release = this.releaseClearFn.bind(this, id);
function chain(promiseFn, nextFn) {
return function () {
return promiseFn().then(nextFn);
};
}
if (!this.isScheduled(id)) {
this.clearTransactionFns[id] =
this.transactionService.addToTransaction(
chain(onCommit, release),
chain(onCancel, release)
);
}
};
/**
* Remove any callbacks associated with this domain object from the
* active transaction.
* @param {string} id the identifier for the domain object
*/
TransactionManager.prototype.clearTransactionsFor = function (id) {
if (this.isScheduled(id)) {
this.clearTransactionFns[id]();
this.releaseClearFn(id);
}
};
/**
* Release the cached "remove from transaction" function that has been
* stored in association with this domain object.
* @param {string} id the identifier for the domain object
* @private
*/
TransactionManager.prototype.releaseClearFn = function (id) {
delete this.clearTransactionFns[id];
};
return TransactionManager;
});

View File

@ -81,6 +81,15 @@ define(
//Log error because this is a programming error if it occurs. //Log error because this is a programming error if it occurs.
this.$log.error("No transaction in progress"); this.$log.error("No transaction in progress");
} }
return function () {
this.onCommits = this.onCommits.filter(function (callback) {
return callback !== onCommit;
});
this.onCancels = this.onCancels.filter(function (callback) {
return callback !== onCancel;
});
}.bind(this);
}; };
/** /**
@ -140,38 +149,9 @@ define(
}); });
}; };
/**
* Clear and restart the active transaction.
*
* This neither cancels nor commits the active transaction;
* instead, it returns a function that can be used to cancel that
* transaction.
*
* @returns {Function} a function to cancel the prior transaction
* @private
*/
TransactionService.prototype.restartTransaction = function () {
var oldOnCancels = this.onCancels;
this.onCommits = [];
this.onCancels = [];
return function () {
while (oldOnCancels.length > 0) {
var onCancel = oldOnCancels.pop();
try {
onCancel();
} catch (error) {
this.$log.error("Error cancelling transaction.");
}
}
};
};
TransactionService.prototype.size = function () { TransactionService.prototype.size = function () {
return this.onCommits.length; return this.onCommits.length;
}; };
return TransactionService; return TransactionService;
} });
);

View File

@ -34,7 +34,6 @@ define(
mockCopyService, mockCopyService,
mockParent, mockParent,
mockUrlService, mockUrlService,
mockTransactionService,
actionContext, actionContext,
capabilities = {}, capabilities = {},
action; action;
@ -120,26 +119,11 @@ define(
["urlForLocation"] ["urlForLocation"]
); );
mockTransactionService = jasmine.createSpyObj(
"transactionService",
["restartTransaction"]
);
mockTransactionService.restartTransaction
.andReturn(jasmine.createSpy());
actionContext = { actionContext = {
domainObject: mockDomainObject domainObject: mockDomainObject
}; };
action = new SaveAsAction( action = new SaveAsAction(undefined, undefined, mockDialogService, undefined, mockCopyService, actionContext);
undefined,
undefined,
mockDialogService,
undefined,
mockCopyService,
mockTransactionService,
actionContext
);
spyOn(action, "getObjectService"); spyOn(action, "getObjectService");
action.getObjectService.andReturn(mockObjectService); action.getObjectService.andReturn(mockObjectService);
@ -195,9 +179,15 @@ define(
}); });
it("hides the blocking dialog after saving", function () { it("hides the blocking dialog after saving", function () {
action.perform(); var mockCallback = jasmine.createSpy();
action.perform().then(mockCallback);
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled(); expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
expect(mockDialogService.dismiss).toHaveBeenCalled(); waitsFor(function () {
return mockCallback.calls.length > 0;
});
runs(function () {
expect(mockDialogService.dismiss).toHaveBeenCalled();
});
}); });
}); });

View File

@ -37,77 +37,74 @@ define(
describe("The transactional persistence decorator", function () { describe("The transactional persistence decorator", function () {
var mockQ, var mockQ,
mockTransactionService, mockTransactionManager,
mockPersistence, mockPersistence,
mockDomainObject, mockDomainObject,
testId,
capability; capability;
beforeEach(function () { beforeEach(function () {
testId = "test-id";
mockQ = jasmine.createSpyObj("$q", ["when"]); mockQ = jasmine.createSpyObj("$q", ["when"]);
mockQ.when.andCallFake(function (val) { mockQ.when.andCallFake(function (val) {
return fastPromise(val); return fastPromise(val);
}); });
mockTransactionService = jasmine.createSpyObj( mockTransactionManager = jasmine.createSpyObj(
"transactionService", "transactionService",
["isActive", "addToTransaction"] ["isActive", "addToTransaction", "clearTransactionsFor"]
); );
mockPersistence = jasmine.createSpyObj( mockPersistence = jasmine.createSpyObj(
"persistenceCapability", "persistenceCapability",
["persist", "refresh"] ["persist", "refresh", "getSpace"]
); );
mockPersistence.persist.andReturn(fastPromise()); mockPersistence.persist.andReturn(fastPromise());
mockPersistence.refresh.andReturn(fastPromise()); mockPersistence.refresh.andReturn(fastPromise());
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
"domainObject", "domainObject",
[ ["getModel", "getId"]
"getModel"
]
); );
mockDomainObject.getModel.andReturn({persisted: 1}); mockDomainObject.getModel.andReturn({persisted: 1});
mockDomainObject.getId.andReturn(testId);
capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject); capability = new TransactionalPersistenceCapability(
mockQ,
mockTransactionManager,
mockPersistence,
mockDomainObject
);
}); });
it("if no transaction is active, passes through to persistence" + it("if no transaction is active, passes through to persistence" +
" provider", function () { " provider", function () {
mockTransactionService.isActive.andReturn(false); mockTransactionManager.isActive.andReturn(false);
capability.persist(); capability.persist();
expect(mockPersistence.persist).toHaveBeenCalled(); expect(mockPersistence.persist).toHaveBeenCalled();
}); });
it("if transaction is active, persist and cancel calls are" + it("if transaction is active, persist and cancel calls are" +
" queued", function () { " queued", function () {
mockTransactionService.isActive.andReturn(true); mockTransactionManager.isActive.andReturn(true);
capability.persist(); capability.persist();
expect(mockTransactionService.addToTransaction).toHaveBeenCalled(); expect(mockTransactionManager.addToTransaction).toHaveBeenCalled();
mockTransactionService.addToTransaction.mostRecentCall.args[0](); mockTransactionManager.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.persist).toHaveBeenCalled(); expect(mockPersistence.persist).toHaveBeenCalled();
mockTransactionService.addToTransaction.mostRecentCall.args[1](); mockTransactionManager.addToTransaction.mostRecentCall.args[2]();
expect(mockPersistence.refresh).toHaveBeenCalled(); expect(mockPersistence.refresh).toHaveBeenCalled();
}); });
it("if transaction is active, cancel call is queued that refreshes model when appropriate", function () { it("wraps getSpace", function () {
mockTransactionService.isActive.andReturn(true); mockPersistence.getSpace.andReturn('foo');
capability.persist(); expect(capability.getSpace()).toEqual('foo');
expect(mockTransactionService.addToTransaction).toHaveBeenCalled();
mockDomainObject.getModel.andReturn({});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).not.toHaveBeenCalled();
mockDomainObject.getModel.andReturn({persisted: 1});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).toHaveBeenCalled();
}); });
it("persist call is only added to transaction once", function () { it("clears transactions and delegates refresh calls", function () {
mockTransactionService.isActive.andReturn(true); capability.refresh();
capability.persist(); expect(mockTransactionManager.clearTransactionsFor)
expect(mockTransactionService.addToTransaction).toHaveBeenCalled(); .toHaveBeenCalledWith(testId);
capability.persist(); expect(mockPersistence.refresh)
expect(mockTransactionService.addToTransaction.calls.length).toBe(1); .toHaveBeenCalled();
}); });
}); });

View File

@ -0,0 +1,132 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/services/TransactionManager"],
function (TransactionManager) {
describe("TransactionManager", function () {
var mockTransactionService,
testId,
mockOnCommit,
mockOnCancel,
mockRemoves,
mockPromise,
manager;
beforeEach(function () {
mockRemoves = [];
mockTransactionService = jasmine.createSpyObj(
"transactionService",
["addToTransaction", "isActive"]
);
mockOnCommit = jasmine.createSpy('commit');
mockOnCancel = jasmine.createSpy('cancel');
testId = 'test-id';
mockPromise = jasmine.createSpyObj('promise', ['then']);
mockOnCommit.andReturn(mockPromise);
mockOnCancel.andReturn(mockPromise);
mockTransactionService.addToTransaction.andCallFake(function () {
var mockRemove =
jasmine.createSpy('remove-' + mockRemoves.length);
mockRemoves.push(mockRemove);
return mockRemove;
});
manager = new TransactionManager(mockTransactionService);
});
it("delegates isActive calls", function () {
[false, true].forEach(function (state) {
mockTransactionService.isActive.andReturn(state);
expect(manager.isActive()).toBe(state);
});
});
describe("when addToTransaction is called", function () {
beforeEach(function () {
manager.addToTransaction(
testId,
mockOnCommit,
mockOnCancel
);
});
it("adds callbacks to the active transaction", function () {
expect(mockTransactionService.addToTransaction)
.toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
it("invokes passed-in callbacks from its own callbacks", function () {
expect(mockOnCommit).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.mostRecentCall.args[0]();
expect(mockOnCommit).toHaveBeenCalled();
expect(mockOnCancel).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.mostRecentCall.args[1]();
expect(mockOnCancel).toHaveBeenCalled();
});
it("ignores subsequent calls for the same object", function () {
manager.addToTransaction(
testId,
jasmine.createSpy(),
jasmine.createSpy()
);
expect(mockTransactionService.addToTransaction.calls.length)
.toEqual(1);
});
it("accepts subsequent calls for other objects", function () {
manager.addToTransaction(
'other-id',
jasmine.createSpy(),
jasmine.createSpy()
);
expect(mockTransactionService.addToTransaction.calls.length)
.toEqual(2);
});
it("does not remove callbacks from the transaction", function () {
expect(mockRemoves[0]).not.toHaveBeenCalled();
});
describe("and clearTransactionsFor is subsequently called", function () {
beforeEach(function () {
manager.clearTransactionsFor(testId);
});
it("removes callbacks from the transaction", function () {
expect(mockRemoves[0]).toHaveBeenCalled();
});
});
});
});
}
);

View File

@ -152,6 +152,10 @@ define(
}, modified); }, modified);
} }
if (domainObject.getModel().persisted === undefined) {
return this.$q.when(true);
}
return this.persistenceService.readObject( return this.persistenceService.readObject(
this.getSpace(), this.getSpace(),
this.getKey() this.getKey()

View File

@ -74,7 +74,7 @@ define(
); );
mockQ = jasmine.createSpyObj( mockQ = jasmine.createSpyObj(
"$q", "$q",
["reject"] ["reject", "when"]
); );
mockNofificationService = jasmine.createSpyObj( mockNofificationService = jasmine.createSpyObj(
"notificationService", "notificationService",
@ -103,6 +103,7 @@ define(
mockIdentifierService.parse.andReturn(mockIdentifier); mockIdentifierService.parse.andReturn(mockIdentifier);
mockIdentifier.getSpace.andReturn(SPACE); mockIdentifier.getSpace.andReturn(SPACE);
mockIdentifier.getKey.andReturn(key); mockIdentifier.getKey.andReturn(key);
mockQ.when.andCallFake(asPromise);
persistence = new PersistenceCapability( persistence = new PersistenceCapability(
mockCacheService, mockCacheService,
mockPersistenceService, mockPersistenceService,
@ -156,6 +157,7 @@ define(
}); });
it("refreshes the domain object model from persistence", function () { it("refreshes the domain object model from persistence", function () {
var refreshModel = {someOtherKey: "some other value"}; var refreshModel = {someOtherKey: "some other value"};
model.persisted = 1;
mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
persistence.refresh(); persistence.refresh();
expect(model).toEqual(refreshModel); expect(model).toEqual(refreshModel);