Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(366)

Unified Diff: sdk/lib/io/http_impl.dart

Issue 11410003: Refactoring of the HTTP library (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Addressed review comments Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | sdk/lib/io/http_parser.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sdk/lib/io/http_impl.dart
diff --git a/sdk/lib/io/http_impl.dart b/sdk/lib/io/http_impl.dart
index 24aa1f8d7ad802a9ab5710dd7a48378be4071053..f33b8b7991533dfa3db3516737909d417bf789ef 100644
--- a/sdk/lib/io/http_impl.dart
+++ b/sdk/lib/io/http_impl.dart
@@ -605,6 +605,78 @@ class _Cookie implements Cookie {
}
+// The close queue handles graceful closing of HTTP connections. When
+// a connection is added to the queue it will enter a wait state
+// waiting for all data written and possibly socket shutdown from
+// peer.
+class _CloseQueue {
+ _CloseQueue() : _q = new Set<_HttpConnectionBase>();
+
+ void add(_HttpConnectionBase connection) {
+ void closeIfDone() {
+ // We only check for write closed here. This means that we are
+ // not waiting for the client to half-close the socket before
+ // fully closing the socket.
+ if (!connection._isWriteClosed) return;
+ _q.remove(connection);
+ connection._socket.close();
+ if (connection.onClosed != null) connection.onClosed();
+ }
+
+ // If the connection is already fully closed don't insert it into the queue.
+ if (connection._isFullyClosed) {
+ connection._socket.close();
+ if (connection.onClosed != null) connection.onClosed();
+ return;
+ }
+
+ _q.add(connection);
+
+ // If output stream is not closed for writing close it now and
+ // wait for callback when closed.
+ if (!connection._isWriteClosed) {
+ connection._socket.outputStream.close();
+ connection._socket.outputStream.onClosed = () {
+ connection._state |= _HttpConnectionBase.WRITE_CLOSED;
+ closeIfDone();
+ };
+ } else {
+ connection._socket.outputStream.onClosed = () { assert(false); };
+ }
+
+ // If socket is not closed for reading wait for callback.
+ if (!connection._isReadClosed) {
+ connection._socket.onClosed = () {
+ connection._state |= _HttpConnectionBase.READ_CLOSED;
+ // This is a nop, as we are not using the read closed
+ // information for anything. For both server and client
+ // connections the inbound message have been read to
+ // completion when the socket enters the close queue.
+ };
+ } else {
+ connection._socket.onClosed = () { assert(false); };
+ }
+
+ // Ignore any data on a socket in the close queue.
+ connection._socket.onData = connection._socket.read;
+
+ // If an error occurs immediately close the socket.
+ connection._socket.onError = (e) {
+ connection._state |= _HttpConnectionBase.WRITE_CLOSED;
+ closeIfDone();
+ };
+ }
+
+ void shutdown() {
+ _q.forEach((_HttpConnectionBase connection) {
+ connection._socket.close();
+ });
+ }
+
+ final Set<_HttpConnectionBase> _q;
+}
+
+
class _HttpRequestResponseBase {
final int START = 0;
final int HEADER_SENT = 1;
@@ -696,14 +768,6 @@ class _HttpRequestResponseBase {
}
assert(_headResponse || _bodyBytesWritten == _contentLength);
}
- // If we are done writing the response, and either the client has
- // closed or the connection is not persistent, we can close. Also
- // if using HTTP 1.0 and the content length was not known we must
- // close to indicate end of body.
- if (!persistentConnection || _httpConnection._closing ||
- (_protocolVersion == "1.0" && _contentLength < 0)) {
- _httpConnection._close();
- }
return allWritten;
}
@@ -1013,17 +1077,6 @@ class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
return _httpConnection._detachSocket();
}
- void _responseEnd() {
- _ensureHeadersSent();
- _state = DONE;
- // Stop tracking no pending write events.
- _httpConnection._onNoPendingWrites = null;
- // Ensure that any trailing data is written.
- _writeDone();
- // Indicate to the connection that the response handling is done.
- _httpConnection._responseDone();
- }
-
// Delegate functions for the HttpOutputStream implementation.
bool _streamWrite(List<int> buffer, bool copyBuffer) {
if (_done) throw new HttpException("Response closed");
@@ -1040,7 +1093,14 @@ class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
}
void _streamClose() {
- _responseEnd();
+ _ensureHeadersSent();
+ _state = DONE;
+ // Stop tracking no pending write events.
+ _httpConnection._onNoPendingWrites = null;
+ // Ensure that any trailing data is written.
+ _writeDone();
+ // Indicate to the connection that the response handling is done.
+ _httpConnection._responseClosed();
}
void _streamSetNoPendingWriteHandler(callback()) {
@@ -1257,32 +1317,49 @@ class _HttpOutputStream extends _BaseOutputStream implements OutputStream {
abstract class _HttpConnectionBase {
- _HttpConnectionBase() : _httpParser = new _HttpParser(),
- hashCode = _nextHashCode {
+ static const int IDLE = 0;
+ static const int ACTIVE = 1;
+ static const int REQUEST_DONE = 2;
+ static const int RESPONSE_DONE = 4;
+ static const int ALL_DONE = REQUEST_DONE | RESPONSE_DONE;
+ static const int READ_CLOSED = 8;
+ static const int WRITE_CLOSED = 16;
+ static const int FULLY_CLOSED = READ_CLOSED | WRITE_CLOSED;
+
+ _HttpConnectionBase() : hashCode = _nextHashCode {
_nextHashCode = (_nextHashCode + 1) & 0xFFFFFFF;
}
+ bool get _isRequestDone => (_state & REQUEST_DONE) == REQUEST_DONE;
+ bool get _isResponseDone => (_state & RESPONSE_DONE) == RESPONSE_DONE;
+ bool get _isAllDone => (_state & ALL_DONE) == ALL_DONE;
+ bool get _isReadClosed => (_state & READ_CLOSED) == READ_CLOSED;
+ bool get _isWriteClosed => (_state & WRITE_CLOSED) == WRITE_CLOSED;
+ bool get _isFullyClosed => (_state & FULLY_CLOSED) == FULLY_CLOSED;
+
void _connectionEstablished(Socket socket) {
_socket = socket;
- // Register handler for socket events.
- _socket.onData = _onData;
- _socket.onClosed = _onClosed;
- _socket.onError = _onError;
+ // Register handlers for socket events. All socket events are
+ // passed to the HTTP parser.
+ _socket.onData = () {
+ List<int> buffer = _socket.read();
+ if (buffer != null) {
+ _httpParser.streamData(buffer);
+ }
+ };
+ _socket.onClosed = _httpParser.streamDone;
+ _socket.onError = _httpParser.streamError;
// Ignore errors in the socket output stream as this is getting
// the same errors as the socket itself.
_socket.outputStream.onError = (e) => null;
}
bool _write(List<int> data, [bool copyBuffer = false]) {
- if (!_error && !_closing) {
- return _socket.outputStream.write(data, copyBuffer);
- }
+ return _socket.outputStream.write(data, copyBuffer);
}
bool _writeFrom(List<int> buffer, [int offset, int len]) {
- if (!_error && !_closing) {
- return _socket.outputStream.writeFrom(buffer, offset, len);
- }
+ return _socket.outputStream.writeFrom(buffer, offset, len);
}
bool _flush() {
@@ -1290,36 +1367,13 @@ abstract class _HttpConnectionBase {
}
bool _close() {
- _closing = true;
_socket.outputStream.close();
}
bool _destroy() {
- _closing = true;
_socket.close();
}
- void _onData() {
- List<int> buffer = _socket.read();
- if (buffer != null) {
- _httpParser.writeList(buffer, 0, buffer.length);
- }
- }
-
- void _onClosed() {
- _closing = true;
- _onConnectionClosed(null);
- }
-
- void _onError(e) {
- // If an error occurs, make sure to close the socket if one is associated.
- _error = true;
- if (_socket != null) {
- _socket.close();
- }
- _onConnectionClosed(e);
- }
-
DetachedSocket _detachSocket() {
_socket.onData = null;
_socket.onClosed = null;
@@ -1332,7 +1386,7 @@ abstract class _HttpConnectionBase {
}
HttpConnectionInfo get connectionInfo {
- if (_socket == null || _closing || _error) return null;
+ if (_socket == null) return null;
try {
_HttpConnectionInfo info = new _HttpConnectionInfo();
info.remoteHost = _socket.remoteHost;
@@ -1343,21 +1397,18 @@ abstract class _HttpConnectionBase {
return null;
}
- void _onConnectionClosed(e);
- void _responseDone();
-
void set _onNoPendingWrites(void callback()) {
- if (!_error) {
- _socket.outputStream.onNoPendingWrites = callback;
- }
+ _socket.outputStream.onNoPendingWrites = callback;
}
+ int _state = IDLE;
+
Socket _socket;
- bool _closing = false; // Is the socket closed by the client?
- bool _error = false; // Is the socket closed due to an error?
_HttpParser _httpParser;
+ // Callbacks.
Function onDetach;
+ Function onClosed;
// Hash code for HTTP connection. Currently this is just a counter.
final int hashCode;
@@ -1368,63 +1419,38 @@ abstract class _HttpConnectionBase {
// HTTP server connection over a socket.
class _HttpConnection extends _HttpConnectionBase {
_HttpConnection(HttpServer this._server) {
+ _httpParser = new _HttpParser.requestParser();
// Register HTTP parser callbacks.
- _httpParser.requestStart =
- (method, uri, version) => _onRequestStart(method, uri, version);
- _httpParser.responseStart =
- (statusCode, reasonPhrase, version) =>
- _onResponseStart(statusCode, reasonPhrase, version);
- _httpParser.headerReceived =
- (name, value) => _onHeaderReceived(name, value);
- _httpParser.headersComplete = () => _onHeadersComplete();
- _httpParser.dataReceived = (data) => _onDataReceived(data);
- _httpParser.dataEnd = (close) => _onDataEnd(close);
- _httpParser.error = (e) => _onError(e);
- }
-
- void _onConnectionClosed(e) {
- // Don't report errors when HTTP parser is in idle state. Clients
- // can close the connection and cause a connection reset by peer
- // error which is OK.
- if (e != null && !_httpParser.isIdle) {
- onError(e);
- // Propagate the error to the streams.
- if (_request != null && _request._streamErrorHandler != null) {
- _request._streamErrorHandler(e);
- }
- if (_response != null && _response._streamErrorHandler != null) {
- _response._streamErrorHandler(e);
- }
- }
+ _httpParser.requestStart = _onRequestStart;
+ _httpParser.headerReceived = _onHeaderReceived;
+ _httpParser.headersComplete = _onHeadersComplete;
+ _httpParser.dataReceived = _onDataReceived;
+ _httpParser.dataEnd = _onDataEnd;
+ _httpParser.error = _onError;
+ _httpParser.closed = _onClosed;
+ _httpParser.responseStart = (statusCode, reasonPhrase, version) {
+ assert(false);
+ };
+ }
- // If currently not processing any request close the socket when
- // we are done writing the response.
- if (_httpParser.isIdle) {
- // If the httpParser is idle and we get an error from the
- // connection we deal with that as a closed connection and not
- // as an error. When the client disappears we get a connection
- // reset by peer and that is OK.
- if (e != null) {
- onClosed();
- } else {
- _socket.outputStream.onClosed = () {
- _destroy();
- onClosed();
- };
- // If the client closes and we are done writing the response
- // the connection should be closed.
- if (_response == null) _close();
- }
- } else {
- // Processing a request.
- if (e == null) {
- // Indicate connection close to the HTTP parser.
- _httpParser.connectionClosed();
- }
+ void _onClosed() {
+ _state |= _HttpConnectionBase.READ_CLOSED;
+ }
+
+ void _onError(e) {
+ onError(e);
+ // Propagate the error to the streams.
+ if (_request != null && _request._streamErrorHandler != null) {
+ _request._streamErrorHandler(e);
}
+ if (_response != null && _response._streamErrorHandler != null) {
+ _response._streamErrorHandler(e);
+ }
+ if (_socket != null) _socket.close();
}
void _onRequestStart(String method, String uri, String version) {
+ _state = _HttpConnectionBase.ACTIVE;
// Create new request and response objects for this request.
_request = new _HttpRequest(this);
_response = new _HttpResponse(this);
@@ -1434,10 +1460,6 @@ class _HttpConnection extends _HttpConnectionBase {
_response._headResponse = method == "HEAD";
}
- void _onResponseStart(int statusCode, String reasonPhrase, String version) {
- // TODO(sgjesse): Error handling.
- }
-
void _onHeaderReceived(String name, String value) {
_request._onHeaderReceived(name, value);
}
@@ -1454,20 +1476,34 @@ class _HttpConnection extends _HttpConnectionBase {
_request._onDataReceived(data);
}
+ void _checkDone() {
+ if (_isAllDone) {
+ // If we are done writing the response, and either the client
+ // has closed or the connection is not persistent, we must
+ // close. Also if using HTTP 1.0 and the content length was not
+ // known we must close to indicate end of body.
+ bool close =
+ !_response.persistentConnection ||
+ (_response._protocolVersion == "1.0" && _response._contentLength < 0);
+ _request = null;
+ _response = null;
+ if (_isReadClosed || close) {
+ _server._closeQueue.add(this);
+ } else {
+ _state = _HttpConnectionBase.IDLE;
+ }
+ }
+ }
+
void _onDataEnd(bool close) {
_request._onDataEnd();
+ _state |= _HttpConnectionBase.REQUEST_DONE;
+ _checkDone();
}
- void _responseDone() {
- // If the connection is closing then close the output stream to
- // fully close the socket.
- if (_closing) {
- _socket.outputStream.onClosed = () {
- _socket.close();
- onClosed();
- };
- }
- _response = null;
+ void _responseClosed() {
+ _state |= _HttpConnectionBase.RESPONSE_DONE;
+ _checkDone();
}
HttpServer _server;
@@ -1476,7 +1512,6 @@ class _HttpConnection extends _HttpConnectionBase {
// Callbacks.
Function onRequestReceived;
- Function onClosed;
Function onError;
}
@@ -1491,7 +1526,8 @@ class _RequestHandlerRegistration {
// managed by the server and as requests are received the request.
class _HttpServer implements HttpServer {
_HttpServer() : _connections = new Set<_HttpConnection>(),
- _handlers = new List<_RequestHandlerRegistration>();
+ _handlers = new List<_RequestHandlerRegistration>(),
+ _closeQueue = new _CloseQueue();
void listen(String host, int port, {int backlog: 128}) {
listenOn(new ServerSocket(host, port, backlog));
@@ -1532,6 +1568,7 @@ class _HttpServer implements HttpServer {
}
void close() {
+ _closeQueue.shutdown();
if (_sessionManagerInstance != null) {
_sessionManagerInstance.close();
_sessionManagerInstance = null;
@@ -1602,6 +1639,7 @@ class _HttpServer implements HttpServer {
List<_RequestHandlerRegistration> _handlers;
Object _defaultHandler;
Function _onError;
+ _CloseQueue _closeQueue;
_HttpSessionManager _sessionManagerInstance;
}
@@ -1659,7 +1697,7 @@ class _HttpClientRequest
_httpConnection._onNoPendingWrites = null;
// Ensure that any trailing data is written.
_writeDone();
- _connection._requestDone();
+ _connection._requestClosed();
}
void _streamSetNoPendingWriteHandler(callback()) {
@@ -1774,10 +1812,6 @@ class _HttpClientResponse
return _inputStream;
}
- void _onRequestStart(String method, String uri, String version) {
- // TODO(sgjesse): Error handling
- }
-
void _onResponseStart(int statusCode, String reasonPhrase, String version) {
_statusCode = statusCode;
_reasonPhrase = reasonPhrase;
@@ -1906,7 +1940,6 @@ class _HttpClientResponse
}
void _onDataEnd() {
- _connection._responseDone();
if (_inputStream != null) {
_inputStream._closeReceived();
} else {
@@ -1946,64 +1979,54 @@ class _HttpClientResponse
class _HttpClientConnection
extends _HttpConnectionBase implements HttpClientConnection {
- static const int NONE = 0;
- static const int REQUEST_DONE = 1;
- static const int RESPONSE_DONE = 2;
- static const int ALL_DONE = REQUEST_DONE | RESPONSE_DONE;
- _HttpClientConnection(_HttpClient this._client);
+ _HttpClientConnection(_HttpClient this._client) {
+ _httpParser = new _HttpParser.responseParser();
+ }
void _connectionEstablished(_SocketConnection socketConn) {
super._connectionEstablished(socketConn._socket);
_socketConn = socketConn;
// Register HTTP parser callbacks.
- _httpParser.requestStart =
- (method, uri, version) => _onRequestStart(method, uri, version);
- _httpParser.responseStart =
- (statusCode, reasonPhrase, version) =>
- _onResponseStart(statusCode, reasonPhrase, version);
- _httpParser.headerReceived =
- (name, value) => _onHeaderReceived(name, value);
- _httpParser.headersComplete = () => _onHeadersComplete();
- _httpParser.dataReceived = (data) => _onDataReceived(data);
- _httpParser.dataEnd = (closed) => _onDataEnd(closed);
- _httpParser.error = (e) => _onError(e);
+ _httpParser.responseStart = _onResponseStart;
+ _httpParser.headerReceived = _onHeaderReceived;
+ _httpParser.headersComplete = _onHeadersComplete;
+ _httpParser.dataReceived = _onDataReceived;
+ _httpParser.dataEnd = _onDataEnd;
+ _httpParser.error = _onError;
+ _httpParser.closed = _onClosed;
+ _httpParser.requestStart = (method, uri, version) { assert(false); };
+ _state = _HttpConnectionBase.ACTIVE;
}
- bool get _isRequestDone => (_state & REQUEST_DONE) == REQUEST_DONE;
- bool get _isResponseDone => (_state & RESPONSE_DONE) == RESPONSE_DONE;
- bool get _isAllDone => (_state & ALL_DONE) == ALL_DONE;
-
void _checkSocketDone() {
if (_isAllDone) {
- if (!_closing) {
+ // If we are done writing the response, and either the server
+ // has closed or the connection is not persistent, we must
+ // close.
+ if (_isReadClosed || !_response.persistentConnection) {
+ this.onClosed = () {
+ _client._closedSocketConnection(_socketConn);
+ };
+ _client._closeQueue.add(this);
+ } else {
_client._returnSocketConnection(_socketConn);
- }
- _socket = null;
- _socketConn = null;
- assert(_pendingRedirect == null || _pendingRetry == null);
- if (_pendingRedirect != null) {
- _doRedirect(_pendingRedirect);
- _pendingRedirect = null;
- } else if (_pendingRetry != null) {
- _doRetry(_pendingRetry);
- _pendingRetry = null;
+ _socket = null;
+ _socketConn = null;
+ assert(_pendingRedirect == null || _pendingRetry == null);
+ if (_pendingRedirect != null) {
+ _doRedirect(_pendingRedirect);
+ _pendingRedirect = null;
+ } else if (_pendingRetry != null) {
+ _doRetry(_pendingRetry);
+ _pendingRetry = null;
+ }
}
}
}
- void _requestDone() {
- _state |= REQUEST_DONE;
- _checkSocketDone();
- }
-
- void _responseDone() {
- if (_closing) {
- if (_socket != null) {
- _socket.close();
- }
- }
- _state |= RESPONSE_DONE;
+ void _requestClosed() {
+ _state |= _HttpConnectionBase.REQUEST_DONE;
_checkSocketDone();
}
@@ -2020,32 +2043,24 @@ class _HttpClientConnection
return _detachSocket();
}
- void _onConnectionClosed(e) {
+ void _onClosed() {
+ _state |= _HttpConnectionBase.READ_CLOSED;
+ }
+
+ void _onError(e) {
// Socket is closed either due to an error or due to normal socket close.
- if (e != null) {
- if (_onErrorCallback != null) {
- _onErrorCallback(e);
- } else {
- throw e;
- }
- }
- _closing = true;
- if (e != null) {
- // Propagate the error to the streams.
- if (_response != null && _response._streamErrorHandler != null) {
- _response._streamErrorHandler(e);
- }
- _responseDone();
+ if (_onErrorCallback != null) {
+ _onErrorCallback(e);
} else {
- // If there was no socket error the socket was closed
- // normally. Indicate closing to the HTTP Parser as there might
- // still be an HTTP error.
- _httpParser.connectionClosed();
+ throw e;
+ }
+ // Propagate the error to the streams.
+ if (_response != null && _response._streamErrorHandler != null) {
+ _response._streamErrorHandler(e);
+ }
+ if (_socketConn != null) {
+ _client._closeSocketConnection(_socketConn);
}
- }
-
- void _onRequestStart(String method, String uri, String version) {
- // TODO(sgjesse): Error handling.
}
void _onResponseStart(int statusCode, String reasonPhrase, String version) {
@@ -2065,8 +2080,9 @@ class _HttpClientConnection
}
void _onDataEnd(bool close) {
- if (close) _closing = true;
_response._onDataEnd();
+ _state |= _HttpConnectionBase.RESPONSE_DONE;
+ _checkSocketDone();
}
void set onRequest(void handler(HttpClientRequest request)) {
@@ -2087,7 +2103,7 @@ class _HttpClientConnection
_response = null;
// Retry the URL using the same connection instance.
- _state = NONE;
+ _state = _HttpConnectionBase.IDLE;
_client._openUrl(retry.method, retry.location, this);
}
@@ -2131,8 +2147,6 @@ class _HttpClientConnection
}
}
- int _state = NONE;
-
List<RedirectInfo> get redirects => _redirects;
Function _onRequest;
@@ -2172,6 +2186,13 @@ class _SocketConnection {
_returnTime = new Date.now();
}
+ void _close() {
+ _socket.onData = null;
+ _socket.onClosed = null;
+ _socket.onError = null;
+ _socket.close();
+ }
+
Duration _idleTime(Date now) => now.difference(_returnTime);
int get hashCode => _socket.hashCode;
@@ -2241,6 +2262,7 @@ class _HttpClient implements HttpClient {
_HttpClient() : _openSockets = new Map(),
_activeSockets = new Set(),
+ _closeQueue = new _CloseQueue(),
credentials = new List<_Credentials>(),
_shutdown = false;
@@ -2299,15 +2321,16 @@ class _HttpClient implements HttpClient {
set findProxy(String f(Uri uri)) => _findProxy = f;
void shutdown() {
- _openSockets.forEach((String key, Queue<_SocketConnection> connections) {
- while (!connections.isEmpty) {
- _SocketConnection socketConn = connections.removeFirst();
- socketConn._socket.close();
- }
- });
- _activeSockets.forEach((_SocketConnection socketConn) {
- socketConn._socket.close();
- });
+ _closeQueue.shutdown();
+ _openSockets.forEach((String key, Queue<_SocketConnection> connections) {
+ while (!connections.isEmpty) {
+ _SocketConnection socketConn = connections.removeFirst();
+ socketConn._socket.close();
+ }
+ });
+ _activeSockets.forEach((_SocketConnection socketConn) {
+ socketConn._socket.close();
+ });
if (_evictionTimer != null) _cancelEvictionTimer();
_shutdown = true;
}
@@ -2503,6 +2526,15 @@ class _HttpClient implements HttpClient {
sockets.addFirst(socketConn);
}
+ void _closeSocketConnection(_SocketConnection socketConn) {
+ socketConn._close();
+ _activeSockets.remove(socketConn);
+ }
+
+ void _closedSocketConnection(_SocketConnection socketConn) {
+ _activeSockets.remove(socketConn);
+ }
+
_Credentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) {
// Look for credentials.
_Credentials cr =
@@ -2527,6 +2559,7 @@ class _HttpClient implements HttpClient {
Function _onOpen;
Map<String, Queue<_SocketConnection>> _openSockets;
Set<_SocketConnection> _activeSockets;
+ _CloseQueue _closeQueue;
List<_Credentials> credentials;
Timer _evictionTimer;
Function _findProxy;
« no previous file with comments | « no previous file | sdk/lib/io/http_parser.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698