Revision: 7267 Author: [email protected] Date: Mon Dec 7 16:46:41 2009 Log: Adding retries for async reloading.
http://code.google.com/p/google-web-toolkit/source/detail?r=7267 Added: /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/AsyncFragmentLoader.gwt.xml /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java /branches/snapshot-2009.10.23-r6446/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java Modified: /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/Core.gwt.xml /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java /branches/snapshot-2009.10.23-r6446/user/test/com/google/gwt/core/CoreSuite.java ======================================= --- /dev/null +++ /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/AsyncFragmentLoader.gwt.xml Mon Dec 7 16:46:41 2009 @@ -0,0 +1,29 @@ +<!-- --> +<!-- Copyright 2008 Google Inc. --> +<!-- Licensed under the Apache License, Version 2.0 (the "License"); you --> +<!-- may not use this file except in compliance with the License. You may --> +<!-- 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. License for the specific language governing permissions and --> +<!-- limitations under the License. --> + +<!-- Types associated with GWT.runAsync() and its fragment loader --> +<module> + + <replace-with class="com.google.gwt.core.client.impl.XhrLoadingStrategy"> + <when-type-is + class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy"/> + </replace-with> + + <replace-with + class="com.google.gwt.core.client.impl.AsyncFragmentLoader.StandardLogger"> + <when-type-is + class="com.google.gwt.core.client.impl.AsyncFragmentLoader.Logger"/> + </replace-with> + +</module> ======================================= --- /dev/null +++ /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java Mon Dec 7 16:46:41 2009 @@ -0,0 +1,240 @@ +/* + * Copyright 2009 Google Inc. + * + * 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. + */ +package com.google.gwt.core.client.impl; + +import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure; +import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure; +import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler; +import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy; +import com.google.gwt.xhr.client.ReadyStateChangeHandler; +import com.google.gwt.xhr.client.XMLHttpRequest; + +/** + * The standard loading strategy used in a web browser. + */ +public class XhrLoadingStrategy implements LoadingStrategy { + + /** + * A {...@link MockableXMLHttpRequest} that is really just a vanilla + * XMLHttpRequest. This wrapper (and thus {...@code MockableXMLHttpRequest) is + * needed because so much of {...@link XMLHttpRequest} is final, which in turn + * is because it extends {...@code JavaScriptObject} and is subject to its + * restrictions. + * + * It is important that these methods be simple enough to be inlined away. + */ + class DelegatingXMLHttpRequest implements MockableXMLHttpRequest { + private final XMLHttpRequest delegate; + + public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) { + delegate = xmlHttpRequest; + } + + public void clearOnReadyStateChange() { + delegate.clearOnReadyStateChange(); + } + + public int getReadyState() { + return delegate.getReadyState(); + } + + public String getResponseText() { + return delegate.getResponseText(); + } + + public int getStatus() { + return delegate.getStatus(); + } + + public String getStatusText() { + return delegate.getStatusText(); + } + + public void open(String method, String url) { + delegate.open(method, url); + } + + public void send() { + delegate.send(); + } + + public void setOnReadyStateChange(ReadyStateChangeHandler handler) { + delegate.setOnReadyStateChange(handler); + } + + public void setRequestHeader(String header, String value) { + delegate.setRequestHeader(header, value); + } + } + + /** + * Delegates to the real XMLHttpRequest, except in test when we make a mock + * to jump through error/retry hoops. + */ + interface MockableXMLHttpRequest { + void clearOnReadyStateChange(); + int getReadyState(); + String getResponseText(); + int getStatus(); + String getStatusText(); + void open(String method, String url); + void send(); + void setOnReadyStateChange(ReadyStateChangeHandler handler); + void setRequestHeader(String header, String value); + } + + /** + * Since LoadingStrategy must support concurrent requests, including figuring + * which is which in the onLoadError handling, we need to keep track of this + * data for each outstanding request, which we index by xhr object. + */ + protected class RequestData { + String url; + int retryCount; + LoadErrorHandler errorHandler = null; + + public RequestData(String url, LoadErrorHandler errorHandler) { + this.url = url; + this.errorHandler = errorHandler; + this.retryCount = 0; + } + } + + static final String HTTP_GET = "GET"; + + /** + * Some UA's like Safari will have a "0" status code when loading from file: + * URLs. Additionally, the "0" status code is used sometimes if the server + * does not respond, e.g. if there is a connection refused. + */ + static final int HTTP_STATUS_NON_HTTP = 0; + + static final int HTTP_STATUS_OK = 200; + + /** + * For error logging, max length of fragment response text to include in + * failed-to-install exception message. + */ + private static final int MAX_LOG_LENGTH = 200; + + /** + * Number of retry attempts for a single fragment. If a fragment download + * fails, we try again this many times before "really" failing out to user + * error-handling code. If a fragment downloads but doesn't install, we + * don't retry at all. + */ + private static final int MAX_RETRY_COUNT = 3; + + public void startLoadingFragment(int fragment, + final LoadErrorHandler loadErrorHandler) { + String url = gwtStartLoadingFragment(fragment, loadErrorHandler); + if (url == null) { + // The download has already started; nothing more to do + return; + } + + RequestData request = new RequestData(url, loadErrorHandler); + tryLoad(request); + } + + /** + * Overridable for tests. + */ + protected MockableXMLHttpRequest createXhr() { + return new DelegatingXMLHttpRequest(XMLHttpRequest.create()); + } + + /** + * Call the linker-supplied <code>__gwtInstallCode</code> method. See the + * {...@link AsyncFragmentLoader class comment} for more details. + */ + protected native void gwtInstallCode(String text) /*-{ + __gwtInstallCode(text); + }-*/; + + /** + * Call the linker-supplied __gwtStartLoadingFragment function. It should + * either start the download and return null or undefined, or it should + * return a URL that should be downloaded to get the code. If it starts the + * download itself, it can synchronously load it, e.g. from cache, if that + * makes sense. + */ + protected native String gwtStartLoadingFragment(int fragment, + LoadErrorHandler loadErrorHandler) /*-{ + function loadFailed(e) { + loaderrorhandl...@com.google.gwt.core.client.impl.asyncfragmentloader$loaderrorhandler::loadFailed(Ljava/lang/Throwable;)(e); + } + return __gwtStartLoadingFragment(fragment, loadFailed); + }-*/; + + /** + * Error recovery from loading or installing code. + * @param request the requestData of this request + * @param e exception of the error + * @param mayRetry {...@code true} if retrying might be helpful + */ + protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) { + if (mayRetry) { + request.retryCount++; + if (request.retryCount < MAX_RETRY_COUNT) { + tryLoad(request); + return; + } + } + request.errorHandler.loadFailed(e); + } + + /** + * Makes a single load-and-install attempt. + */ + protected void tryLoad(final RequestData request) { + final MockableXMLHttpRequest xhr = createXhr(); + + xhr.open(HTTP_GET, request.url); + if (request.retryCount > 0) { + // disable caching if we have to retry; one cause could be bad cache + xhr.setRequestHeader("Cache-Control", "no-cache"); + } + + xhr.setOnReadyStateChange(new ReadyStateChangeHandler() { + public void onReadyStateChange(XMLHttpRequest ignored) { + if (xhr.getReadyState() == XMLHttpRequest.DONE) { + xhr.clearOnReadyStateChange(); + if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP) + && xhr.getResponseText() != null + && xhr.getResponseText().length() != 0) { + try { + gwtInstallCode(xhr.getResponseText()); + } catch (RuntimeException e) { + String textIntro = xhr.getResponseText(); + if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) { + textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "..."; + } + onLoadError(request, + new HttpInstallFailure(request.url, textIntro, e), false); + } + } else { + onLoadError(request, + new HttpDownloadFailure(request.url, xhr.getStatus(), + xhr.getStatusText()), true); + } + } + } + }); + + xhr.send(); + } +} ======================================= --- /dev/null +++ /branches/snapshot-2009.10.23-r6446/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java Mon Dec 7 16:46:41 2009 @@ -0,0 +1,277 @@ +/* + * Copyright 2009 Google Inc. + * + * 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. + */ +package com.google.gwt.core.client.impl; + +import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler; +import com.google.gwt.core.client.impl.XhrLoadingStrategy.MockableXMLHttpRequest; +import com.google.gwt.xhr.client.ReadyStateChangeHandler; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +/** + * Tests the default loading strategy and its retry behavior. + */ +public class XhrLoadingStrategyTest extends TestCase { + + static class MockXhr implements MockableXMLHttpRequest { + public static final String SUCCESSFUL_RESPONSE_TEXT = + "successful response text"; + public static final String INSTALL_FAILED_RESPONSE_TEXT = + "install failed response text"; + + private ReadyStateChangeHandler handler; + private int httpStatus; + private int state; + private String statusText; + private String text; + private HashMap<String,String> headers; + + public MockXhr(int status, String statusText, boolean loads, + boolean installs, String... headers) { + this.httpStatus = status; + this.statusText = statusText; + if (installs) { + text = SUCCESSFUL_RESPONSE_TEXT; + } else if (loads) { + text = INSTALL_FAILED_RESPONSE_TEXT; + } else { + text = null; + } + handler = null; + state = 0; + assert headers.length % 2 == 0; + this.headers = new HashMap<String,String>(); + for (int i = 0; i < headers.length; i += 2) { + this.headers.put(headers[i], headers[i + 1]); + } + } + + public void clearOnReadyStateChange() { + handler = null; + } + + public int getReadyState() { + return state; + } + + public String getResponseText() { + return state > 3 ? text : null; + } + + public int getStatus() { + return state > 1 ? httpStatus : 0; + } + + public String getStatusText() { + return state > 1 ? statusText : null; + } + + public void open(String method, String url) { + state = 1; + } + + public void send() { + state = 4; + if (headers.size() != 0) { + throw new IllegalStateException("not all expected headers set"); + } + if (handler != null) { + // TODO(fabbott): this is brittle, but I don't have a better idea. + // The problem is that onReadyStateChange takes a REAL XMLHttpRequest, + // which I can't mock because it's all final. I don't want to open + // ReadyStateChangeHandler's long-standing API to let it take a + // non-real XMLHttpRequest, just for my wee test here, so instead I + // admit that null works 'cause the handler won't *use* its argument. + handler.onReadyStateChange(null); + } + } + + public void setOnReadyStateChange(ReadyStateChangeHandler handler) { + this.handler = handler; + } + + public void setRequestHeader(String header, String value) { + String val = headers.get(header); + if (val == null) { + throw new IllegalArgumentException("set of unexpected header " + + header); + } + if (!val.equals(value)) { + throw new IllegalArgumentException("set of header " + + header + " to unexpected value " + value + ", not " + val); + } + headers.remove(header); + } + } + + /** + * {...@link XhrLoadingStrategy}, but without actual live XHRs. + */ + static class MockXhrLoadingStrategy extends XhrLoadingStrategy { + private static final String FRAGMENT_URL = "http://nowhere.net/fragment"; + private ArrayList<MockXhr> xhrs; + + public MockXhrLoadingStrategy(MockXhr... input) { + xhrs = new ArrayList<MockXhr>(Arrays.asList(input)); + } + + public void assertDone() { + if (xhrs.size() != 0) { + throw new IllegalStateException("leftover createXhr() data" + + " (too few load retries?)"); + } + } + + /** + * Test stub; install succeeds unless text says otherwise. + */ + @Override + protected void gwtInstallCode(String text) { + if (MockXhr.INSTALL_FAILED_RESPONSE_TEXT.equals(text)) { + throw new RuntimeException(text); + } + } + + /** + * Test stub; bypass the JSNI, but we're returning a (mock) URL. + */ + @Override + protected String gwtStartLoadingFragment(int fragment, + LoadErrorHandler loadErrorHandler) { + return FRAGMENT_URL; + } + + @Override + protected MockableXMLHttpRequest createXhr() { + if (xhrs.size() == 0) { + throw new IllegalStateException("createXhr() underflow" + + " (too many load retries?)"); + } + return xhrs.remove(0); + } + } + + /** + * Basic succeeds-on-first-try case. + */ + public void testNoRetrySucceeds() { + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(200, "200 Ok", true, true)); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + fail(); + } + }); + xls.assertDone(); + } + + /** + * Fails irrevocably on first try; doesn't retry. + */ + public void testNoRetryFails() { + final boolean loadFailedCalled[] = new boolean[1]; + loadFailedCalled[0] = false; + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(200, "Ok", true, false)); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + loadFailedCalled[0] = true; + } + }); + xls.assertDone(); + if (!loadFailedCalled[0]) { + fail("should have failed to install, but didn't"); + } + } + + /** + * Needs some retries, but succeeds. + */ + public void testRetrySucceeds() { + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(0, "Could not connect", false, false), + new MockXhr(200, "Ok", true, true, "Cache-Control", "no-cache")); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + fail(); + } + }); + xls.assertDone(); + } + + /** + * Needs retries, and never succeeds. + */ + public void testRetryFails() { + final boolean loadFailedCalled[] = new boolean[1]; + loadFailedCalled[0] = false; + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(0, "Could not connect", false, false), + new MockXhr(0, "Could not connect", false, false, + "Cache-Control", "no-cache"), + new MockXhr(0, "Could not connect", false, false, + "Cache-Control", "no-cache")); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + loadFailedCalled[0] = true; + } + }); + xls.assertDone(); + if (!loadFailedCalled[0]) { + fail("should have failed to install, but didn't"); + } + } + + /** + * A bizarre case we've seen in the wild... + */ + public void testNull200Case() { + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(200, "Ok", false, false), + new MockXhr(200, "Ok", false, false, + "Cache-Control", "no-cache"), + new MockXhr(200, "Ok", true, true, + "Cache-Control", "no-cache")); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + fail(); + } + }); + xls.assertDone(); + } + + /** + * Check some HTTP status codes.... + */ + public void testRetryCodes() { + MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy( + new MockXhr(500, "Server Error", false, false), + new MockXhr(404, "Not Found", false, false, + "Cache-Control", "no-cache"), + new MockXhr(200, "Ok", true, true, + "Cache-Control", "no-cache")); + xls.startLoadingFragment(1, new LoadErrorHandler() { + public void loadFailed(Throwable reason) { + fail(); + } + }); + xls.assertDone(); + } +} ======================================= --- /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/Core.gwt.xml Mon Aug 17 09:47:48 2009 +++ /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/Core.gwt.xml Mon Dec 7 16:46:41 2009 @@ -23,6 +23,7 @@ <inherits name="com.google.gwt.xhr.XMLHttpRequest" /> <inherits name="com.google.gwt.core.CompilerParameters" /> <inherits name="com.google.gwt.core.EmulateJsStack" /> + <inherits name="com.google.gwt.core.AsyncFragmentLoader" /> <super-source path="translatable" /> @@ -37,4 +38,5 @@ <add-linker name="soycReport" /> <add-linker name="symbolMaps" /> + </module> ======================================= --- /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java Wed Oct 21 13:20:55 2009 +++ /branches/snapshot-2009.10.23-r6446/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java Mon Dec 7 16:46:41 2009 @@ -16,8 +16,6 @@ package com.google.gwt.core.client.impl; import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.xhr.client.ReadyStateChangeHandler; -import com.google.gwt.xhr.client.XMLHttpRequest; import java.util.ArrayList; import java.util.HashMap; @@ -153,11 +151,12 @@ /** * An exception indicating than at HTTP download failed. */ - private static class HttpDownloadFailure extends RuntimeException { + static class HttpDownloadFailure extends RuntimeException { private final int statusCode; - public HttpDownloadFailure(int statusCode) { - super("HTTP download failed with status " + statusCode); + public HttpDownloadFailure(String url, int statusCode, String statusText) { + super("Download of " + url + " failed with status " + statusCode + "(" + + statusText + ")"); this.statusCode = statusCode; } @@ -165,6 +164,16 @@ return statusCode; } } + + /** + * An exception indicating than at HTTP download succeeded, but installing + * its body failed. + */ + static class HttpInstallFailure extends RuntimeException { + public HttpInstallFailure(String url, String text, Throwable rootCause) { + super("Install of " + url + " failed with text " + text, rootCause); + } + } /** * Handles a failure to download a fragment in the initial sequence. @@ -261,90 +270,15 @@ return !!$stats; }-*/; } - - /** - * The standard loading strategy used in a web browser. - */ - private static class XhrLoadingStrategy implements LoadingStrategy { - public void startLoadingFragment(int fragment, - final LoadErrorHandler loadErrorHandler) { - String fragmentUrl = gwtStartLoadingFragment(fragment, loadErrorHandler); - - if (fragmentUrl == null) { - // The download has already started; nothing more to do - return; - } - - // use XHR to download it - - final XMLHttpRequest xhr = XMLHttpRequest.create(); - - xhr.open(HTTP_GET, fragmentUrl); - - xhr.setOnReadyStateChange(new ReadyStateChangeHandler() { - public void onReadyStateChange(XMLHttpRequest ignored) { - if (xhr.getReadyState() == XMLHttpRequest.DONE) { - xhr.clearOnReadyStateChange(); - if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP) - && xhr.getResponseText() != null - && xhr.getResponseText().length() != 0) { - try { - gwtInstallCode(xhr.getResponseText()); - } catch (RuntimeException e) { - loadErrorHandler.loadFailed(e); - } - } else { - loadErrorHandler.loadFailed(new HttpDownloadFailure( - xhr.getStatus())); - } - } - } - }); - - xhr.send(); - } - - /** - * Call the linker-supplied <code>__gwtInstallCode</code> method. See the - * {...@link AsyncFragmentLoader class comment} for more details. - */ - private native void gwtInstallCode(String text) /*-{ - __gwtInstallCode(text); - }-*/; - - /** - * Call the linker-supplied __gwtStartLoadingFragment function. It should - * either start the download and return null or undefined, or it should return - * a URL that should be downloaded to get the code. If it starts the download - * itself, it can synchronously load it, e.g. from cache, if that makes sense. - */ - private native String gwtStartLoadingFragment(int fragment, - LoadErrorHandler loadErrorHandler) /*-{ - function loadFailed(e) { - loaderrorhandl...@com.google.gwt.core.client.impl.asyncfragmentloader$loaderrorhandler::loadFailed(Ljava/lang/Throwable;)(e); - } - return __gwtStartLoadingFragment(fragment, loadFailed); - }-*/; - } /** * The standard instance of AsyncFragmentLoader used in a web browser. The * parameters to this call are filled in by * {...@link com.google.gwt.dev.jjs.impl.ReplaceRunAsyncs}. */ - public static AsyncFragmentLoader BROWSER_LOADER = new AsyncFragmentLoader(1, - new int[] {}, new XhrLoadingStrategy(), new StandardLogger()); - - private static final String HTTP_GET = "GET"; - - /** - * Some UA's like Safari will have a "0" status code when loading from file: - * URLs. Additionally, the "0" status code is used sometimes if the server - * does not respond, e.g. if there is a connection refused. - */ - private static final int HTTP_STATUS_NON_HTTP = 0; - - private static final int HTTP_STATUS_OK = 200; + public static AsyncFragmentLoader BROWSER_LOADER = + new AsyncFragmentLoader(1, new int[] {}, // compiler replaces these two + new XhrLoadingStrategy(), new StandardLogger()); /** * A helper static method that invokes ======================================= --- /branches/snapshot-2009.10.23-r6446/user/test/com/google/gwt/core/CoreSuite.java Wed Nov 11 20:06:00 2009 +++ /branches/snapshot-2009.10.23-r6446/user/test/com/google/gwt/core/CoreSuite.java Mon Dec 7 16:46:41 2009 @@ -23,6 +23,7 @@ import com.google.gwt.core.client.impl.EmulatedStackTraceTest; import com.google.gwt.core.client.impl.SchedulerImplTest; import com.google.gwt.core.client.impl.StackTraceCreatorTest; +import com.google.gwt.core.client.impl.XhrLoadingStrategyTest; import com.google.gwt.junit.tools.GWTTestSuite; import junit.framework.Test; @@ -42,6 +43,7 @@ suite.addTestSuite(StackTraceCreatorTest.class); suite.addTestSuite(EmulatedStackTraceTest.class); suite.addTestSuite(AsyncFragmentLoaderTest.class); + suite.addTestSuite(XhrLoadingStrategyTest.class); suite.addTestSuite(SchedulerImplTest.class); // $JUnit-END$ -- http://groups.google.com/group/Google-Web-Toolkit-Contributors
